Tujuan Praktikum
Setelah menyelesaikan praktikum ini, mahasiswa mampu:
- Menjelaskan apa itu CRUD dan mengapa dipisahkan ke dalam lapisan tersendiri.
-
Membuat class
ProdukRepositorysebagai jembatan objek dengan database. -
Menerapkan operasi Create (
simpan) dan Read (ambil_semua,ambil_by_id,cari). - Menerapkan operasi Update dan Delete.
- Membangun aplikasi menu sederhana di terminal untuk mengelola data produk.
Persiapan
Kebutuhan praktikum:
- 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:
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:
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:
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.pydari Pertemuan 11 tidak perlu diubah sama sekali. -
main.pycukup memanggilrepo.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:
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_PATHditulis 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()
# ---------- 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()
# ---------- 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 objekProduk, bukan tuple mentah. Method dari Pertemuan 11 langsung bisa dipakai. -
ambil_by_id()mengembalikanNonebila produk tidak ditemukan — ini wajib diperiksa sebelum dipakai. -
LIKE ?dengan%kata%mencari produk yang mengandung kata kunci (miripstr.contains()pada Pandas).
Uji di main.py:
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:
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
# ---------- 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 = 0akan mengubah seluruh produk, danDELETE FROM produkakan menghapus semua data. Ini kesalahan yang sangat sering terjadi dan akibatnya fatal.cursor.rowcountmemberi tahu berapa baris yang terpengaruh. Bila0, berarti id yang dicari tidak ada — sehingga kita bisa mengembalikanFalsesebagai tanda gagal.
Uji alur lengkap:
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:
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:
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:
==============================================================
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:
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:
python main.py
Output:
==============================
APLIKASI DATA PRODUK
==============================
1. Tampilkan semua produk
2. Tambah produk
3. Ubah produk
4. Hapus produk
5. Cari produk
0. Keluar
Pilih menu:
Perhatikanmain.py: tidak ada satu pun perintah SQL di sini. Seluruh urusan database diserahkan kerepo. 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:
- Menu tampil dan dapat dipilih berulang kali (tidak langsung berhenti).
-
Create: menambah produk baru — produk muncul di
daftar dan memiliki
id_produk. - Read: menampilkan seluruh produk dalam bentuk tabel rapi.
- Update: mengubah harga sebuah produk — perubahan tetap ada setelah program ditutup dan dibuka kembali.
- Delete: menghapus produk, dengan konfirmasi terlebih dahulu.
- Memasukkan ID yang tidak ada menampilkan pesan "Produk tidak ditemukan", bukan error.
Verifikasi cepat:
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:
-
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().) -
Tambahkan menu "Tambah stok" —
pengguna memasukkan ID produk dan jumlah tambahan, lalu stok
bertambah. Gunakan method
tambah_stok()dari classProduk(Pertemuan 11), lalu simpan denganrepo.update(). - Tambahkan validasi input: harga dan stok tidak boleh negatif. Bila pengguna memasukkan nilai negatif, tampilkan pesan kesalahan dan batalkan penyimpanan.
-
Tambahkan menu
"Total nilai persediaan" yang
menampilkan jumlah
nilai_persediaan()dari seluruh produk (memakai method dari Pertemuan 11). -
Buat repository baru
repository/pelanggan_repository.pydengan CRUD lengkap untuk tabelpelanggan, 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.