Pertemuan 12 - CRUD Produk

Pertemuan 12 - CRUD Produk

Membuat operasi Create, Read, Update, dan Delete untuk data produk menggunakan pola repository.

Tujuan Praktikum

Setelah menyelesaikan praktikum ini, mahasiswa mampu:

  1. Menjelaskan apa itu CRUD dan mengapa dipisahkan ke dalam lapisan tersendiri.
  2. Membuat class ProdukRepository sebagai jembatan objek dengan database.
  3. Menerapkan operasi Create (simpan) dan Read (ambil_semua, ambil_by_id, cari).
  4. Menerapkan operasi Update dan Delete.
  5. Membangun aplikasi menu sederhana di terminal untuk mengelola data produk.

Persiapan

Kebutuhan praktikum:

text
- VS Code
- Project toko-elektronik
- Database database/toko.db (dari Pertemuan 10)
- File models/produk.py (dari Pertemuan 11)
- Virtual environment aktif (venv)

Buka project dan aktifkan venv:

bash
venv\Scripts\activate
macOS / Linux: gunakan source venv/bin/activate.

Buat folder baru bernama repository, lalu di dalamnya buat file produk_repository.py. Struktur project kini menjadi:

text
toko-elektronik/
├── database/
│   └── toko.db
├── models/
│   ├── produk.py
│   ├── pelanggan.py
│   └── transaksi.py
├── repository/
│   └── produk_repository.py     <- baru
└── main.py

Konsep Singkat

Apa itu CRUD?

CRUD adalah empat operasi dasar pengelolaan data. Hampir semua aplikasi (kasir, perpustakaan, media sosial) pada dasarnya melakukan keempat hal ini.

Singkatan Arti Perintah SQL
Create Menambah data baru INSERT
Read Membaca data SELECT
Update Mengubah data UPDATE
Delete Menghapus data DELETE

Masalah dari Pertemuan 11

Pada Pertemuan 11 kita sudah bisa mengubah baris database menjadi objek — tetapi kodenya bercampur: SQL, objek, dan tampilan menumpuk dalam satu file. Bila SQL ditulis di banyak tempat, sekali nama kolom berubah, kita harus memperbaikinya di mana-mana.

Solusi: Pola Repository

Kita pisahkan menjadi beberapa lapisan, masing-masing dengan satu tugas jelas:

Lapisan aplikasi

Repository adalah satu-satunya lapisan yang boleh menulis SQL. Ia menjadi "penerjemah" dua arah:

  • Database → Objek saat membaca data.
  • Objek → Database saat menyimpan data.

Keuntungannya:

  • models/produk.py dari Pertemuan 11 tidak perlu diubah sama sekali.
  • main.py cukup memanggil repo.ambil_semua() — tidak perlu tahu SQL sedikit pun.
  • Repository yang sama nanti dipakai ulang ketika membuat aplikasi web dengan Flask (Pertemuan 15).

Langkah 1 - Kerangka Repository

Buka repository/produk_repository.py. Mulai dengan kerangka dan dua method pembantu:

python
import sqlite3
from models.produk import Produk

DB_PATH = "database/toko.db"


class ProdukRepository:
    """Jembatan antara objek Produk (Python) dan tabel produk (database)."""

    def _koneksi(self):
        return sqlite3.connect(DB_PATH)

    def _ke_objek(self, baris):
        """Mengubah satu baris database menjadi objek Produk."""
        return Produk(
            id_produk=baris[0],
            nama_produk=baris[1],
            kategori=baris[2],
            harga=baris[3],
            stok=baris[4],
        )

Penjelasan:

  • DB_PATH ditulis satu kali di sini. Bila lokasi database berubah, cukup ubah satu baris ini.
  • _koneksi() dan _ke_objek() diawali garis bawah (_) sebagai penanda: hanya dipakai di dalam class ini, bukan dari luar.
  • _ke_objek() sangat penting — inilah penerjemah baris database menjadi objek.

Langkah 2 - Create dan Read

Tambahkan method berikut ke dalam class ProdukRepository.

Create - simpan()

python
    # ---------- CREATE ----------
    def simpan(self, produk):
        conn = self._koneksi()
        cursor = conn.cursor()
        cursor.execute(
            "INSERT INTO produk (nama_produk, kategori, harga, stok) VALUES (?, ?, ?, ?)",
            (produk.nama_produk, produk.kategori, produk.harga, produk.stok),
        )
        produk.id_produk = cursor.lastrowid      # objek mendapat id dari database
        conn.commit()
        conn.close()
        return produk
Perhatikan: objek yang tadinya id_produk = None kini terisi setelah disimpan. Inilah cara objek Python "mengenal" dirinya di database.

Read - ambil_semua(), ambil_by_id(), cari()

python
    # ---------- READ ----------
    def ambil_semua(self):
        conn = self._koneksi()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT id_produk, nama_produk, kategori, harga, stok FROM produk ORDER BY id_produk"
        )
        hasil = [self._ke_objek(b) for b in cursor.fetchall()]
        conn.close()
        return hasil

    def ambil_by_id(self, id_produk):
        conn = self._koneksi()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT id_produk, nama_produk, kategori, harga, stok FROM produk WHERE id_produk = ?",
            (id_produk,),
        )
        baris = cursor.fetchone()
        conn.close()
        return self._ke_objek(baris) if baris else None      # None bila tidak ketemu

    def cari(self, kata_kunci):
        conn = self._koneksi()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT id_produk, nama_produk, kategori, harga, stok FROM produk WHERE nama_produk LIKE ?",
            (f"%{kata_kunci}%",),
        )
        hasil = [self._ke_objek(b) for b in cursor.fetchall()]
        conn.close()
        return hasil

Penjelasan penting:

  • ambil_semua() mengembalikan daftar objek Produk, bukan tuple mentah. Method dari Pertemuan 11 langsung bisa dipakai.
  • ambil_by_id() mengembalikan None bila produk tidak ditemukan — ini wajib diperiksa sebelum dipakai.
  • LIKE ? dengan %kata% mencari produk yang mengandung kata kunci (mirip str.contains() pada Pandas).

Uji di main.py:

python
from models.produk import Produk
from repository.produk_repository import ProdukRepository

repo = ProdukRepository()

# READ
for p in repo.ambil_semua():
    print(p)

# CREATE
baru = Produk("Speaker", "Elektronik", 750000, 20)
repo.simpan(baru)
print("Tersimpan dengan id:", baru.id_produk)

Output:

text
Laptop (Elektronik) - Rp 8,500,000 | stok: 10
Monitor (Elektronik) - Rp 2,000,000 | stok: 15
Mouse (Aksesoris) - Rp 150,000 | stok: 50
Tersimpan dengan id: 4

Langkah 3 - Update dan Delete

python
    # ---------- UPDATE ----------
    def update(self, produk):
        conn = self._koneksi()
        cursor = conn.cursor()
        cursor.execute(
            "UPDATE produk SET nama_produk = ?, kategori = ?, harga = ?, stok = ? WHERE id_produk = ?",
            (produk.nama_produk, produk.kategori, produk.harga, produk.stok, produk.id_produk),
        )
        conn.commit()
        berhasil = cursor.rowcount > 0
        conn.close()
        return berhasil

    # ---------- DELETE ----------
    def hapus(self, id_produk):
        conn = self._koneksi()
        cursor = conn.cursor()
        cursor.execute("DELETE FROM produk WHERE id_produk = ?", (id_produk,))
        conn.commit()
        berhasil = cursor.rowcount > 0
        conn.close()
        return berhasil
WHERE id_produk = ? itu WAJIB. Bila lupa, UPDATE produk SET harga = 0 akan mengubah seluruh produk, dan DELETE FROM produk akan menghapus semua data. Ini kesalahan yang sangat sering terjadi dan akibatnya fatal.

cursor.rowcount memberi tahu berapa baris yang terpengaruh. Bila 0, berarti id yang dicari tidak ada — sehingga kita bisa mengembalikan False sebagai tanda gagal.

Uji alur lengkap:

python
repo = ProdukRepository()

# ambil, ubah, simpan kembali
p = repo.ambil_by_id(4)
p.harga = 800000
p.stok = 25
print("Update berhasil:", repo.update(p))
print("Hasil:", repo.ambil_by_id(4))

# hapus
print("Hapus id 4  :", repo.hapus(4))
print("Hapus id 999:", repo.hapus(999))     # tidak ada -> False

Output:

text
Update berhasil: True
Hasil: Speaker (Elektronik) - Rp 800,000 | stok: 25
Hapus id 4  : True
Hapus id 999: False
Pola baku Update: ambil objek → ubah atributnya → simpan kembali. Bukan menulis SQL langsung dari main.py.

Langkah 4 - Menampilkan Data dengan Rapi

Buka main.py dan buat fungsi untuk menampilkan produk dalam bentuk tabel:

python
from models.produk import Produk
from repository.produk_repository import ProdukRepository

repo = ProdukRepository()


def tampilkan_produk():
    daftar = repo.ambil_semua()
    print("\n" + "=" * 62)
    print(f"{'ID':<4}{'NAMA':<14}{'KATEGORI':<13}{'HARGA':>13}{'STOK':>7}")
    print("=" * 62)
    for p in daftar:
        print(f"{p.id_produk:<4}{p.nama_produk:<14}{p.kategori:<13}{p.harga:>13,}{p.stok:>7}")
    print("=" * 62)
    print(f"Jumlah produk: {len(daftar)}")


tampilkan_produk()

Output:

text
==============================================================
ID  NAMA          KATEGORI             HARGA   STOK
==============================================================
1   Laptop        Elektronik       8,500,000     10
2   Monitor       Elektronik       2,000,000     15
3   Mouse         Aksesoris          150,000     50
==============================================================
Jumlah produk: 3
Penjelasan format: {p.nama_produk:<14} berarti rata kiri selebar 14 karakter; {p.harga:>13,} berarti rata kanan selebar 13 karakter dengan pemisah ribuan. Inilah cara membuat tabel rapi di terminal.

Langkah 5 - Aplikasi Menu CRUD

Sekarang kita rangkai menjadi aplikasi utuh. Lengkapi main.py:

python
from models.produk import Produk
from repository.produk_repository import ProdukRepository

repo = ProdukRepository()


def tampilkan_produk():
    daftar = repo.ambil_semua()
    print("\n" + "=" * 62)
    print(f"{'ID':<4}{'NAMA':<14}{'KATEGORI':<13}{'HARGA':>13}{'STOK':>7}")
    print("=" * 62)
    for p in daftar:
        print(f"{p.id_produk:<4}{p.nama_produk:<14}{p.kategori:<13}{p.harga:>13,}{p.stok:>7}")
    print("=" * 62)
    print(f"Jumlah produk: {len(daftar)}")


def tambah_produk():
    print("\n--- Tambah Produk ---")
    nama = input("Nama produk : ")
    kategori = input("Kategori    : ")
    harga = int(input("Harga       : "))
    stok = int(input("Stok        : "))

    p = Produk(nama, kategori, harga, stok)
    repo.simpan(p)
    print(f"Produk '{p.nama_produk}' tersimpan dengan id {p.id_produk}.")


def ubah_produk():
    print("\n--- Ubah Produk ---")
    id_produk = int(input("ID produk yang diubah: "))
    p = repo.ambil_by_id(id_produk)

    if p is None:                          # WAJIB diperiksa
        print("Produk tidak ditemukan.")
        return

    print("Data saat ini:", p)
    print("(kosongkan bila tidak ingin mengubah)")

    nama = input(f"Nama [{p.nama_produk}]: ")
    kategori = input(f"Kategori [{p.kategori}]: ")
    harga = input(f"Harga [{p.harga}]: ")
    stok = input(f"Stok [{p.stok}]: ")

    if nama:
        p.nama_produk = nama
    if kategori:
        p.kategori = kategori
    if harga:
        p.harga = int(harga)
    if stok:
        p.stok = int(stok)

    repo.update(p)
    print("Produk berhasil diperbarui.")


def hapus_produk():
    print("\n--- Hapus Produk ---")
    id_produk = int(input("ID produk yang dihapus: "))
    p = repo.ambil_by_id(id_produk)

    if p is None:
        print("Produk tidak ditemukan.")
        return

    print("Akan dihapus:", p)
    if input("Yakin? (y/n): ").lower() == "y":       # konfirmasi
        repo.hapus(id_produk)
        print("Produk berhasil dihapus.")
    else:
        print("Dibatalkan.")


def cari_produk():
    print("\n--- Cari Produk ---")
    kata = input("Kata kunci: ")
    hasil = repo.cari(kata)

    if not hasil:
        print("Tidak ada produk yang cocok.")
        return

    for p in hasil:
        print(" -", p)


def menu():
    while True:
        print("\n" + "=" * 30)
        print("   APLIKASI DATA PRODUK")
        print("=" * 30)
        print("1. Tampilkan semua produk")
        print("2. Tambah produk")
        print("3. Ubah produk")
        print("4. Hapus produk")
        print("5. Cari produk")
        print("0. Keluar")

        pilihan = input("Pilih menu: ")

        if pilihan == "1":
            tampilkan_produk()
        elif pilihan == "2":
            tambah_produk()
        elif pilihan == "3":
            ubah_produk()
        elif pilihan == "4":
            hapus_produk()
        elif pilihan == "5":
            cari_produk()
        elif pilihan == "0":
            print("Terima kasih.")
            break
        else:
            print("Pilihan tidak dikenal.")


if __name__ == "__main__":
    menu()

Jalankan:

bash
python main.py

Output:

text
==============================
   APLIKASI DATA PRODUK
==============================
1. Tampilkan semua produk
2. Tambah produk
3. Ubah produk
4. Hapus produk
5. Cari produk
0. Keluar
Pilih menu:
Perhatikan main.py: tidak ada satu pun perintah SQL di sini. Seluruh urusan database diserahkan ke repo. Inilah manfaat pemisahan lapisan — dan alasan mengapa file ini nanti dapat diganti tampilan web (Flask) tanpa menyentuh repository.

Referensi Fungsi Penting

Method Repository

Method CRUD Mengembalikan
repo.simpan(produk) Create Objek Produk (dengan id_produk terisi)
repo.ambil_semua() Read List objek Produk
repo.ambil_by_id(id) Read Objek Produk, atau None bila tidak ada
repo.cari(kata) Read List objek Produk yang cocok
repo.update(produk) Update True / False
repo.hapus(id) Delete True / False

SQL yang Dipakai

SQL Fungsi
INSERT INTO produk (...) VALUES (?, ?, ?, ?) Menambah data
SELECT ... FROM produk ORDER BY id_produk Membaca semua
SELECT ... FROM produk WHERE id_produk = ? Membaca satu
SELECT ... FROM produk WHERE nama_produk LIKE ? Mencari
UPDATE produk SET ... WHERE id_produk = ? Mengubah
DELETE FROM produk WHERE id_produk = ? Menghapus

Format Tampilan Terminal

Penulisan Arti
f"{teks:<14}" Rata kiri, lebar 14 karakter
f"{teks:>13}" Rata kanan, lebar 13 karakter
f"{angka:>13,}" Rata kanan + pemisah ribuan
"=" * 62 Membuat garis pemisah

Pengujian

Pastikan hasil praktikum sesuai berikut:

  1. Menu tampil dan dapat dipilih berulang kali (tidak langsung berhenti).
  2. Create: menambah produk baru — produk muncul di daftar dan memiliki id_produk.
  3. Read: menampilkan seluruh produk dalam bentuk tabel rapi.
  4. Update: mengubah harga sebuah produk — perubahan tetap ada setelah program ditutup dan dibuka kembali.
  5. Delete: menghapus produk, dengan konfirmasi terlebih dahulu.
  6. Memasukkan ID yang tidak ada menampilkan pesan "Produk tidak ditemukan", bukan error.

Verifikasi cepat:

python
from models.produk import Produk
from repository.produk_repository import ProdukRepository

repo = ProdukRepository()
jumlah_awal = len(repo.ambil_semua())

p = Produk("Uji Coba", "Tes", 1000, 1)
repo.simpan(p)
assert p.id_produk is not None
assert len(repo.ambil_semua()) == jumlah_awal + 1

p.harga = 2000
assert repo.update(p) is True
assert repo.ambil_by_id(p.id_produk).harga == 2000

assert repo.hapus(p.id_produk) is True
assert repo.hapus(999999) is False
assert len(repo.ambil_semua()) == jumlah_awal
print("Semua pengujian CRUD lolos.")

Troubleshooting

Masalah Penyebab Solusi
ModuleNotFoundError: No module named 'repository' Dijalankan dari folder yang salah Jalankan python main.py dari folder utama project
Data hilang setelah program ditutup Lupa conn.commit() Pastikan setiap INSERT/UPDATE/DELETE diikuti commit()
Seluruh data berubah/terhapus Lupa WHERE id_produk = ? Selalu sertakan WHERE pada UPDATE dan DELETE
AttributeError: 'NoneType' object has no attribute Objek dari ambil_by_id() bernilai None Periksa if p is None: sebelum memakai objek
ValueError: invalid literal for int() Pengguna mengetik huruf pada input harga/stok Bungkus dengan try ... except ValueError
database is locked Koneksi tidak ditutup Pastikan conn.close() dipanggil di setiap method
Produk tersimpan dua kali Tombol dipencet berulang Periksa daftar sebelum menyimpan

Tugas Praktikum

Kembangkan aplikasi CRUD produk:

  1. Tambahkan menu "Tampilkan produk stok menipis" yang menampilkan produk dengan stok kurang dari 10, diurutkan dari stok terkecil. (Petunjuk: tambahkan method baru di repository, mis. ambil_stok_menipis().)
  2. Tambahkan menu "Tambah stok" — pengguna memasukkan ID produk dan jumlah tambahan, lalu stok bertambah. Gunakan method tambah_stok() dari class Produk (Pertemuan 11), lalu simpan dengan repo.update().
  3. Tambahkan validasi input: harga dan stok tidak boleh negatif. Bila pengguna memasukkan nilai negatif, tampilkan pesan kesalahan dan batalkan penyimpanan.
  4. Tambahkan menu "Total nilai persediaan" yang menampilkan jumlah nilai_persediaan() dari seluruh produk (memakai method dari Pertemuan 11).
  5. Buat repository baru repository/pelanggan_repository.py dengan CRUD lengkap untuk tabel pelanggan, lalu tambahkan menunya.

Kumpulkan seluruh file .py beserta tangkapan layar hasil menjalankan tiap menu (format PDF).

Kesimpulan

Pada pertemuan ini mahasiswa membangun operasi CRUD lengkap untuk data produk dengan memisahkan kode ke dalam lapisan: models menyimpan bentuk data, repository mengurus seluruh SQL, dan main.py menangani tampilan. Pemisahan ini membuat kode rapi dan aman — main.py sama sekali tidak menyentuh SQL, sementara class Produk dari Pertemuan 11 dipakai apa adanya tanpa perubahan. Mahasiswa juga belajar kebiasaan penting: selalu menyertakan WHERE pada UPDATE/DELETE, memeriksa hasil None, dan meminta konfirmasi sebelum menghapus. Pada pertemuan berikutnya, pola yang sama diterapkan pada kasus yang lebih menantang: CRUD Transaksi dengan relasi master–detail, di mana satu nota memuat banyak barang sekaligus.