Kerangka dari Bagian 2 belum bisa menyimpan apa pun. Pada bagian ini kita mengisi lima menu pertama dengan operasi CRUD: menambah produk, menampilkan seluruh produk, mengubah harga, mengubah stok, dan menghapus produk. Satu aturan dipegang sejak baris pertama: setiap nilai dari pengguna masuk ke SQL sebagai parameter ?, tidak pernah digabung ke dalam string query.

Lima Operasi di database.py #

Module database.py bertambah enam function operasi data. Berikut isi lengkapnya setelah perubahan; bagian atas (koneksi dan inisialisasi) tidak berubah dari Bagian 2:

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()


def tambah_produk(nama: str, kategori: str, harga: int, stok: int,
                  db_path: str = NAMA_DATABASE) -> int:
    """Simpan produk baru, kembalikan id-nya."""
    sql = "INSERT INTO produk (nama, kategori, harga, stok) VALUES (?, ?, ?, ?)"
    conn = koneksi(db_path)
    try:
        kursor = conn.execute(sql, (nama, kategori, harga, stok))
        conn.commit()
        return int(kursor.lastrowid)
    finally:
        conn.close()


def lihat_produk(db_path: str = NAMA_DATABASE) -> list[sqlite3.Row]:
    """Ambil seluruh produk, urut berdasarkan nama."""
    conn = koneksi(db_path)
    try:
        return conn.execute("SELECT * FROM produk ORDER BY nama").fetchall()
    finally:
        conn.close()


def ambil_produk(produk_id: int,
                 db_path: str = NAMA_DATABASE) -> sqlite3.Row | None:
    """Ambil satu produk berdasarkan id; None bila tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute("SELECT * FROM produk WHERE id = ?", (produk_id,))
        return kursor.fetchone()
    finally:
        conn.close()


def perbarui_harga(produk_id: int, harga_baru: int,
                   db_path: str = NAMA_DATABASE) -> bool:
    """Set harga produk menjadi nilai baru. False bila id tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute(
            "UPDATE produk SET harga = ? WHERE id = ?", (harga_baru, produk_id)
        )
        conn.commit()
        return kursor.rowcount > 0
    finally:
        conn.close()


def perbarui_stok(produk_id: int, stok_baru: int,
                  db_path: str = NAMA_DATABASE) -> bool:
    """Set stok produk menjadi nilai baru. False bila id tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute(
            "UPDATE produk SET stok = ? WHERE id = ?", (stok_baru, produk_id)
        )
        conn.commit()
        return kursor.rowcount > 0
    finally:
        conn.close()


def hapus_produk(produk_id: int, db_path: str = NAMA_DATABASE) -> bool:
    """Hapus produk beserta riwayatnya. False bila id tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute("DELETE FROM produk WHERE id = ?", (produk_id,))
        conn.commit()
        return kursor.rowcount > 0
    finally:
        conn.close()

Mari kita bedah pola yang berulang pada keenam function tersebut:

  1. conn.execute(sql, (nilai, ...)): query dan data dikirim terpisah. Driver sqlite3 menyusun statement final setelah string SQL selesai di-parse, sehingga masukan pengguna tidak pernah bisa mengubah makna query — inilah parameterized query yang dibahas pada materi Parameterized Queries.
  2. return int(kursor.lastrowid) pada tambah_produk(): attribute lastrowid berisi id baris yang baru saja di-insert, lalu id tersebut dikembalikan ke pemanggil sebagai konfirmasi.
  3. return kursor.rowcount > 0 pada ketiga operasi tulis UPDATE dan DELETE: attribute rowcount menghitung baris yang benar-benar terpengaruh. Jika pengguna memasukkan id yang tidak ada, rowcount bernilai 0 dan function mengembalikan False — bukan error, hanya kabar “tidak ada yang berubah”.
  4. conn.commit() setelah setiap tulisan: tanpa commit, perubahan hanya hidup di transaksi yang terbuka dan hilang saat koneksi ditutup.
  5. finally: conn.close(): setiap function membuka dan menutup koneksinya sendiri. Untuk aplikasi menu kecil ini sederhana dan aman; pola ini akan kita tinjau ulang di Bagian 6.
  6. .fetchall() dan .fetchone(): fetchall() mengembalikan seluruh baris sebagai list, sedangkan fetchone() mengembalikan satu baris atau None saat tidak ada hasil.

Perhatikan juga bahwa hapus_produk() tidak menghapus tabel riwayat secara eksplisit. Constraint ON DELETE CASCADE dari Bagian 1 yang mengerjakannya — itulah gunanya mendesain database sebelum menulis kode.

Menyambungkan Menu #

Module main.py kini benar-benar bekerja. Setiap menu punya satu function handler sendiri sehingga isi match tetap pendek:

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

import database


def format_rupiah(nilai: int) -> str:
    """Format angka menjadi teks rupiah, contoh 15000 menjadi Rp15.000."""
    return "Rp" + f"{nilai:,}".replace(",", ".")


def cetak_tabel(rows: list) -> None:
    """Cetak daftar produk sebagai tabel rapi."""
    print(f"{'ID':<3} {'Nama':<18} {'Kategori':<10} {'Harga':>10} {'Stok':>4}")
    print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 10} {'-' * 4}")
    for row in rows:
        print(
            f"{row['id']:<3} {row['nama']:<18} {row['kategori']:<10} "
            f"{format_rupiah(row['harga']):>10} {row['stok']:>4}"
        )


def baca_angka(prompt: str) -> int:
    """Minta masukan angka bulat, ulangi sampai masukan sah."""
    while True:
        teks = input(prompt).strip()
        try:
            return int(teks)
        except ValueError:
            print("Masukkan angka bulat, misalnya 15000.")


def menu_tambah() -> None:
    nama = input("Nama produk: ").strip()
    kategori = input("Kategori: ").strip()
    harga = baca_angka("Harga (Rp): ")
    stok = baca_angka("Stok awal: ")
    id_baru = database.tambah_produk(nama, kategori, harga, stok)
    print(f"Produk '{nama}' tersimpan dengan id {id_baru}.")


def menu_lihat() -> None:
    rows = database.lihat_produk()
    if not rows:
        print("Belum ada produk.")
        return
    cetak_tabel(rows)


def menu_ubah_harga() -> None:
    produk_id = baca_angka("ID produk: ")
    harga_baru = baca_angka("Harga baru (Rp): ")
    if database.perbarui_harga(produk_id, harga_baru):
        print("Harga berhasil diperbarui.")
    else:
        print(f"Tidak ada produk dengan id {produk_id}.")


def menu_ubah_stok() -> None:
    produk_id = baca_angka("ID produk: ")
    stok_baru = baca_angka("Stok baru: ")
    if database.perbarui_stok(produk_id, stok_baru):
        print("Stok berhasil diperbarui.")
    else:
        print(f"Tidak ada produk dengan id {produk_id}.")


def menu_hapus() -> None:
    produk_id = baca_angka("ID produk: ")
    if database.hapus_produk(produk_id):
        print("Produk berhasil dihapus.")
    else:
        print(f"Tidak ada produk dengan id {produk_id}.")


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

        match pilihan:
            case "1":
                menu_tambah()
            case "2":
                menu_lihat()
            case "3":
                menu_ubah_harga()
            case "4":
                menu_ubah_stok()
            case "5":
                menu_hapus()
            case _:
                print("Pilihan tidak dikenal.")


if __name__ == "__main__":
    main()

Tiga helper di bagian atas layak dijelaskan lebih dekat:

  1. format_rupiah(): f"{nilai:,}" menambah pemisah ribuan gaya Inggris (15,000), lalu .replace(",", ".") mengubahnya menjadi gaya Indonesia (15.000).
  2. cetak_tabel(): setiap kolom diberi lebar tetap — nama rata kiri 18 karakter (:<18), harga dan stok rata kanan (:>10, :>4) — sehingga angka sejajar ke kanan seperti layaknya tabel keuangan.
  3. baca_angka(): mengulang prompt sampai int(teks) berhasil; masukan seperti tujuh ribu menimbulkan ValueError yang ditangani di tempat, bukan meneruskan data rusak ke database.

Mencoba Seluruh Alur CRUD #

Kita mulai dari database bersih berisi enam data awal, menambah satu produk baru, lalu menampilkan seluruh tabel:

bash
printf '1\nSusu Kotak\nMinuman\n6000\n12\n2\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: Nama produk: Kategori: Harga (Rp): Stok awal: Produk 'Susu Kotak' tersimpan dengan id 7.

=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Ubah stok
5. Hapus produk
0. Keluar
Pilih menu: ID  Nama               Kategori        Harga Stok
--- ------------------ ---------- ---------- ----
5   Buku Tulis         Alat Tulis    Rp5.000   40
4   Keripik Singkong   Makanan      Rp10.000    2
1   Kopi Tubruk        Minuman      Rp15.000   24
6   Pulpen Gel         Alat Tulis    Rp3.500    4
3   Roti Tawar         Makanan      Rp12.000   15
7   Susu Kotak         Minuman       Rp6.000   12
2   Teh Celup          Minuman       Rp8.000    3

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

Susu Kotak mendapat id 7 karena AUTOINCREMENT tidak memakai ulang nomor yang sudah pernah terpakai. Selanjutnya kita mengubah harga Susu Kotak — sengaja dengan satu masukan salah lebih dulu untuk melihat baca_angka() bekerja — lalu mengubah stoknya:

bash
printf '3\n7\ntujuh ribu\n6500\n4\n7\n20\n2\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: ID produk: Harga baru (Rp): Masukkan angka bulat, misalnya 15000.
Harga baru (Rp): Harga berhasil diperbarui.

=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Ubah stok
5. Hapus produk
0. Keluar
Pilih menu: ID produk: Stok baru: Stok berhasil diperbarui.

=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Ubah stok
5. Hapus produk
0. Keluar
Pilih menu: ID  Nama               Kategori        Harga Stok
--- ------------------ ---------- ---------- ----
5   Buku Tulis         Alat Tulis    Rp5.000   40
4   Keripik Singkong   Makanan      Rp10.000    2
1   Kopi Tubruk        Minuman      Rp15.000   24
6   Pulpen Gel         Alat Tulis    Rp3.500    4
3   Roti Tawar         Makanan      Rp12.000   15
7   Susu Kotak         Minuman       Rp6.500   20
2   Teh Celup          Minuman       Rp8.000    3

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

Terakhir, masih melanjutkan kondisi database yang sama (Susu Kotak kini ber-id 7 dengan harga Rp6.500 dan stok 20), kita menghapus Susu Kotak lalu mencoba menghapus id yang tidak ada:

bash
printf '5\n7\n5\n99\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: ID produk: Produk berhasil dihapus.

=== Aplikasi Inventaris ===
1. Tambah produk
2. Lihat semua produk
3. Ubah harga
4. Ubah stok
5. Hapus produk
0. Keluar
Pilih menu: ID produk: Tidak ada produk dengan id 99.

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

Kedua versi module hasil bagian ini tersedia pada berkas sumber di bawah halaman. Aplikasi sudah bisa menyimpan data permanen, tetapi masih percaya sepenuhnya pada masukan pengguna. Bagian 4 menghadapi kenyataan itu: validasi, nama duplikat, dan transaksi yang gagal di tengah jalan.

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()


def tambah_produk(nama: str, kategori: str, harga: int, stok: int,
                  db_path: str = NAMA_DATABASE) -> int:
    """Simpan produk baru, kembalikan id-nya."""
    sql = "INSERT INTO produk (nama, kategori, harga, stok) VALUES (?, ?, ?, ?)"
    conn = koneksi(db_path)
    try:
        kursor = conn.execute(sql, (nama, kategori, harga, stok))
        conn.commit()
        return int(kursor.lastrowid)
    finally:
        conn.close()


def lihat_produk(db_path: str = NAMA_DATABASE) -> list[sqlite3.Row]:
    """Ambil seluruh produk, urut berdasarkan nama."""
    conn = koneksi(db_path)
    try:
        return conn.execute("SELECT * FROM produk ORDER BY nama").fetchall()
    finally:
        conn.close()


def ambil_produk(produk_id: int,
                 db_path: str = NAMA_DATABASE) -> sqlite3.Row | None:
    """Ambil satu produk berdasarkan id; None bila tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute("SELECT * FROM produk WHERE id = ?", (produk_id,))
        return kursor.fetchone()
    finally:
        conn.close()


def perbarui_harga(produk_id: int, harga_baru: int,
                   db_path: str = NAMA_DATABASE) -> bool:
    """Set harga produk menjadi nilai baru. False bila id tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute(
            "UPDATE produk SET harga = ? WHERE id = ?", (harga_baru, produk_id)
        )
        conn.commit()
        return kursor.rowcount > 0
    finally:
        conn.close()


def perbarui_stok(produk_id: int, stok_baru: int,
                  db_path: str = NAMA_DATABASE) -> bool:
    """Set stok produk menjadi nilai baru. False bila id tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute(
            "UPDATE produk SET stok = ? WHERE id = ?", (stok_baru, produk_id)
        )
        conn.commit()
        return kursor.rowcount > 0
    finally:
        conn.close()


def hapus_produk(produk_id: int, db_path: str = NAMA_DATABASE) -> bool:
    """Hapus produk beserta riwayatnya. False bila id tidak ada."""
    conn = koneksi(db_path)
    try:
        kursor = conn.execute("DELETE FROM produk WHERE id = ?", (produk_id,))
        conn.commit()
        return kursor.rowcount > 0
    finally:
        conn.close()

main.py

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

import database


def format_rupiah(nilai: int) -> str:
    """Format angka menjadi teks rupiah, contoh 15000 menjadi Rp15.000."""
    return "Rp" + f"{nilai:,}".replace(",", ".")


def cetak_tabel(rows: list) -> None:
    """Cetak daftar produk sebagai tabel rapi."""
    print(f"{'ID':<3} {'Nama':<18} {'Kategori':<10} {'Harga':>10} {'Stok':>4}")
    print(f"{'---':<3} {'-' * 18} {'-' * 10} {'-' * 10} {'-' * 4}")
    for row in rows:
        print(
            f"{row['id']:<3} {row['nama']:<18} {row['kategori']:<10} "
            f"{format_rupiah(row['harga']):>10} {row['stok']:>4}"
        )


def baca_angka(prompt: str) -> int:
    """Minta masukan angka bulat, ulangi sampai masukan sah."""
    while True:
        teks = input(prompt).strip()
        try:
            return int(teks)
        except ValueError:
            print("Masukkan angka bulat, misalnya 15000.")


def menu_tambah() -> None:
    nama = input("Nama produk: ").strip()
    kategori = input("Kategori: ").strip()
    harga = baca_angka("Harga (Rp): ")
    stok = baca_angka("Stok awal: ")
    id_baru = database.tambah_produk(nama, kategori, harga, stok)
    print(f"Produk '{nama}' tersimpan dengan id {id_baru}.")


def menu_lihat() -> None:
    rows = database.lihat_produk()
    if not rows:
        print("Belum ada produk.")
        return
    cetak_tabel(rows)


def menu_ubah_harga() -> None:
    produk_id = baca_angka("ID produk: ")
    harga_baru = baca_angka("Harga baru (Rp): ")
    if database.perbarui_harga(produk_id, harga_baru):
        print("Harga berhasil diperbarui.")
    else:
        print(f"Tidak ada produk dengan id {produk_id}.")


def menu_ubah_stok() -> None:
    produk_id = baca_angka("ID produk: ")
    stok_baru = baca_angka("Stok baru: ")
    if database.perbarui_stok(produk_id, stok_baru):
        print("Stok berhasil diperbarui.")
    else:
        print(f"Tidak ada produk dengan id {produk_id}.")


def menu_hapus() -> None:
    produk_id = baca_angka("ID produk: ")
    if database.hapus_produk(produk_id):
        print("Produk berhasil dihapus.")
    else:
        print(f"Tidak ada produk dengan id {produk_id}.")


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

        match pilihan:
            case "1":
                menu_tambah()
            case "2":
                menu_lihat()
            case "3":
                menu_ubah_harga()
            case "4":
                menu_ubah_stok()
            case "5":
                menu_hapus()
            case _:
                print("Pilihan tidak dikenal.")


if __name__ == "__main__":
    main()