Skema di Bagian 1 masih berupa berkas teks; belum ada satu baris pun kode Python yang membacanya. Pada bagian ini kita membangun fondasi aplikasi: module database.py yang memusatkan seluruh urusan koneksi dan penerapan skema, serta main.py berupa kerangka menu yang menyambut pengguna. Sasaran utamanya sederhana namun sering dilupakan: aplikasi harus siap dijalankan berkali-kali tanpa merusak datanya sendiri.

Memisahkan Lapisan Data #

Sebelum menulis kode, kita tetapkan pembagian tanggung jawab dua module ini. Module database.py adalah satu-satunya tempat yang boleh menjalankan SQL; module main.py hanya mengurus interaksi dengan pengguna lewat menu. Pemisahan ini membuat bug data mudah dilacak karena seluruh SQL hidup di satu berkas, dan tampilan bisa berubah tanpa menyentuh logika penyimpanan.

Berikut isi lengkap database.py untuk tahap ini:

python
"""Lapisan akses data untuk aplikasi inventaris."""

import sqlite3
from pathlib import Path

NAMA_DATABASE = "inventaris.db"
BERKAS_SKEMA = Path(__file__).with_name("schema.sql")


def koneksi(db_path: str = NAMA_DATABASE) -> sqlite3.Connection:
    """Buka koneksi baru ke database inventaris."""
    conn = sqlite3.connect(db_path)
    conn.execute("PRAGMA foreign_keys = ON")
    conn.row_factory = sqlite3.Row
    return conn


def inisialisasi(db_path: str = NAMA_DATABASE) -> None:
    """Terapkan schema.sql ke database. Aman dijalankan berulang."""
    conn = koneksi(db_path)
    try:
        conn.executescript(BERKAS_SKEMA.read_text(encoding="utf-8"))
        conn.commit()
    finally:
        conn.close()

Meskipun pendek, setiap baris function koneksi() membawa satu keputusan penting:

  1. sqlite3.connect(db_path): membuka koneksi ke berkas database; jika berkasnya belum ada, SQLite membuat berkas baru secara otomatis. Inilah alasan kita tidak perlu membuat inventaris.db secara manual.
  2. conn.execute("PRAGMA foreign_keys = ON"): menyalakan pemeriksaan foreign key untuk koneksi ini. Pengaturan tersebut berlaku per koneksi dan selalu nonaktif di awal, jadi setiap koneksi baru wajib menyalakannya sendiri.
  3. conn.row_factory = sqlite3.Row: mengubah bentuk hasil query dari tuple menjadi objek sqlite3.Row yang bisa diakses berdasarkan nama kolom, misalnya row["nama"]. Kode pembaca menjadi jauh lebih jelas dibanding mengingat urutan kolom seperti row[1].
  4. return conn: koneksi dikembalikan ke pemanggil; function ini sengaja tidak menutup koneksi karena penutupan adalah tanggung jawab pemanggil.

Sementara itu, inisialisasi() menerapkan skema lewat empat langkah:

  1. koneksi(db_path): membuka koneksi seperti biasa.
  2. BERKAS_SKEMA.read_text(encoding="utf-8"): membaca isi schema.sql. Path berkas skema dicari di sebelah database.py sendiri memakai Path(__file__).with_name(...), sehingga aplikasi tetap bekerja terlepas dari folder mana ia dijalankan.
  3. conn.executescript(...): menjalankan seluruh isi skrip SQL sekaligus. Method executescript() memang executescript (bukan execute) karena berkas skema berisi banyak statement, bukan satu.
  4. finally: conn.close(): koneksi ditutup baik setelah sukses maupun setelah galat. Karena schema.sql memakai IF NOT EXISTS dan INSERT OR IGNORE, menjalankan function ini berkali-kali tidak pernah merusak data — sifat inilah yang disebut idempoten.

Kerangka Menu di main.py #

Module main.py pada tahap ini hanya menyiapkan alur: panggil inisialisasi(), lalu putar menu sampai pengguna memilih keluar.

python
"""Aplikasi Inventaris SQLite - antarmuka pengguna."""

import database


def tampilkan_menu() -> None:
    print()
    print("=== Aplikasi Inventaris ===")
    print("1. Tambah produk")
    print("2. Lihat semua produk")
    print("3. Ubah harga")
    print("4. Ubah stok")
    print("5. Hapus produk")
    print("0. Keluar")


def main() -> None:
    database.inisialisasi()
    print("Database siap. Selamat datang!")

    while True:
        tampilkan_menu()
        try:
            pilihan = input("Pilih menu: ").strip()
        except EOFError:
            print()
            break

        if pilihan == "0":
            print("Sampai jumpa!")
            break

        print("Fitur ini dibangun pada bagian selanjutnya.")


if __name__ == "__main__":
    main()

Beberapa keputusan pada kerangka tersebut layak dijelaskan:

  1. database.inisialisasi() di awal main(): setiap kali aplikasi dijalankan, skema diterapkan lebih dulu. Pada menjalankan pertama tabel dan data awal terbentuk; pada menjalankan berikutnya, statement idempoten melewati yang sudah ada tanpa efek samping.
  2. try: ... except EOFError: function input() mengangkat EOFError saat masukan habis, misalnya ketika program dijalankan dengan pipa dan datanya telanjuri habis. Tanpa penanganan ini, program berakhir dengan traceback yang menakutkan.
  3. if __name__ == "__main__":: blok main() hanya berjalan saat berkas dieksekusi langsung, sehingga main.py kelak bisa diimpor oleh test tanpa memicu menu interaktif.

Menjalankan Aplikasi Pertama Kali #

Pastikan schema.sql, database.py, dan main.py berada dalam satu folder, lalu jalankan:

bash
python3 main.py

Interaksi berikut kami jalankan dengan mengirim masukan lewat printf agar transkripnya dapat direproduksi; hasil yang sama akan Anda lihat saat mengetik manual:

bash
printf '2\n0\n' | python3 main.py

Output yang Dihasilkan:

text
Database siap. Selamat datang!

=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Ubah stok
5. Hapus produk
0. Keluar
Pilih menu: Fitur ini dibangun pada bagian selanjutnya.

=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Ubah stok
5. Hapus produk
0. Keluar
Pilih menu: Sampai jumpa!

Setelah program berhenti, folder proyek memuat berkas baru inventaris.db berukuran sekitar 28 KB. Mari kita buktikan sifat idempoten dengan menjalankan aplikasi sekali lagi, lalu memeriksa isinya:

bash
python3 -c "
import sqlite3
conn = sqlite3.connect('inventaris.db')
print('Jumlah produk setelah main.py dijalankan dua kali:',
      conn.execute('SELECT COUNT(*) FROM produk').fetchone()[0])
print('Data awal:', [r[0] for r in conn.execute('SELECT nama FROM produk ORDER BY id')])
conn.close()
"

Output yang Dihasilkan:

text
Jumlah produk setelah main.py dijalankan dua kali: 6
Data awal: ['Kopi Tubruk', 'Teh Celup', 'Roti Tawar', 'Keripik Singkong', 'Buku Tulis', 'Pulpen Gel']

Jumlah produk tetap enam walaupun inisialisasi() sudah dua kali menjalankan INSERT: INSERT OR IGNORE bekerja persis seperti yang dirancang pada Bagian 1. Fondasi telah berdiri dengan satu berkas database, dua tabel, dan data awal yang utuh; kedua module hasil bagian ini tersedia pada berkas sumber di bawah halaman. Pada Bagian 3 kita mengisi menu 1 sampai 5 dengan operasi CRUD sungguhan.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

database.py

Unduh berkas
python
"""Lapisan akses data untuk aplikasi inventaris."""

import sqlite3
from pathlib import Path

NAMA_DATABASE = "inventaris.db"
BERKAS_SKEMA = Path(__file__).with_name("schema.sql")


def koneksi(db_path: str = NAMA_DATABASE) -> sqlite3.Connection:
    """Buka koneksi baru ke database inventaris."""
    conn = sqlite3.connect(db_path)
    conn.execute("PRAGMA foreign_keys = ON")
    conn.row_factory = sqlite3.Row
    return conn


def inisialisasi(db_path: str = NAMA_DATABASE) -> None:
    """Terapkan schema.sql ke database. Aman dijalankan berulang."""
    conn = koneksi(db_path)
    try:
        conn.executescript(BERKAS_SKEMA.read_text(encoding="utf-8"))
        conn.commit()
    finally:
        conn.close()

main.py

Unduh berkas
python
"""Aplikasi Inventaris SQLite - antarmuka pengguna."""

import database


def tampilkan_menu() -> None:
    print()
    print("=== Aplikasi Inventaris ===")
    print("1. Tambah produk")
    print("2. Lihat semua produk")
    print("3. Ubah harga")
    print("4. Ubah stok")
    print("5. Hapus produk")
    print("0. Keluar")


def main() -> None:
    database.inisialisasi()
    print("Database siap. Selamat datang!")

    while True:
        tampilkan_menu()
        try:
            pilihan = input("Pilih menu: ").strip()
        except EOFError:
            print()
            break

        if pilihan == "0":
            print("Sampai jumpa!")
            break

        print("Fitur ini dibangun pada bagian selanjutnya.")


if __name__ == "__main__":
    main()