Aplikasi yang menyimpan data permanen sebaiknya tidak dimulai dari tampilan, melainkan dari struktur datanya. Keputusan seperti “bolehkah dua produk bernama sama?” atau “bolehkah harga negatif?” paling murah ditegakkan di database, bukan di tiap layar aplikasi. Pada bagian ini kita merancang seluruh struktur penyimpanan aplikasi inventaris dalam satu berkas schema.sql, lalu membuktikan bahwa setiap aturan benar-benar ditegakkan oleh SQLite.
Merancang Tabel produk #
Satu-satunya entitas utama aplikasi ini adalah produk dengan lima kolom. Berikut definisi lengkapnya untuk berkas schema.sql:
CREATE TABLE IF NOT EXISTS produk (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT NOT NULL UNIQUE,
kategori TEXT NOT NULL,
harga INTEGER NOT NULL CHECK (harga >= 0),
stok INTEGER NOT NULL CHECK (stok >= 0)
);Mari kita bedah setiap constraint pada deklarasi di atas:
id INTEGER PRIMARY KEY AUTOINCREMENT: kolomidmenjadi primary key. SQLite memperlakukanINTEGER PRIMARY KEYsebagai alias untukrowid, sehingga nilai id terisi otomatis dan unik.AUTOINCREMENTmencegah SQLite memakai ulang id yang sudah dihapus, sehingga nomor id hanya naik dan riwayat lama tidak tertukar.nama TEXT NOT NULL UNIQUE: nama produk wajib diisi dan tidak boleh duplikat. ConstraintUNIQUEotomatis membuat index pada kolomnama, jadi pencarian berdasarkan nama juga ikut cepat.kategori TEXT NOT NULL: setiap produk wajib punya kategori. Kita tidak memakai tabel kategori terpisah karena daftar kategori pada aplikasi kecil jarang berubah.harga INTEGER NOT NULL CHECK (harga >= 0): harga disimpan sebagaiINTEGERdalam satuan rupiah penuh, bukanREAL. Bilangan pecahan seperti0.1tidak dapat direpresentasikan tepat oleh float, dan pembulatan satu sen yang menumpuk bisa membuat total laporan meleset.stok INTEGER NOT NULL CHECK (stok >= 0): stok negatif adalah data yang mustahil secara bisnis, jadiCHECKmenolaknya di tingkat database.
Tabel riwayat dan Foreign Key #
Penyesuaian stok perlu dicatat: siapa tahu pemilik toko ingin tahu kapan stok berkurang dan mengapa. Catatan tersebut kita simpan di tabel kedua yang merujuk ke produk:
CREATE TABLE IF NOT EXISTS riwayat (
id INTEGER PRIMARY KEY AUTOINCREMENT,
produk_id INTEGER NOT NULL,
perubahan INTEGER NOT NULL,
keterangan TEXT NOT NULL,
waktu TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (produk_id) REFERENCES produk(id) ON DELETE CASCADE
);Tiga hal penting dari definisi tersebut:
produk_id: foreign key yang merujuk keproduk(id). Tabelriwayatdisebut tabel anak, dan tabelprodukadalah tabel induk.FOREIGN KEY ... ON DELETE CASCADE: saat sebuah produk dihapus, SQLite ikut menghapus seluruh barisriwayatmilik produk tersebut. Tanpa opsi ini, riwayat yatim akan menumpuk dan laporan bisa menampilkan angka untuk produk yang sudah tidak ada.waktu ... DEFAULT CURRENT_TIMESTAMP: kolom waktu terisi otomatis saat baris dibuat. SQLite menyimpannya sebagai teks formatYYYY-MM-DD HH:MM:SSdalam zona waktu UTC.
Index untuk Kolom yang Sering Dipakai #
Filter dan laporan kita sering memilih produk berdasarkan kategori. Index mempercepat pencarian itu dengan mengorbankan sedikit ruang penyimpanan:
CREATE INDEX IF NOT EXISTS idx_produk_kategori ON produk(kategori);
CREATE INDEX IF NOT EXISTS idx_riwayat_produk ON riwayat(produk_id);Kedua statement membuat index bernama idx_produk_kategori pada kolom produk(kategori) dan idx_riwayat_produk pada kolom riwayat(produk_id). Index pada produk_id membantu JOIN dan penghapusan berantai yang akan sering kita lakukan pada Bagian 4.
Data Awal dengan INSERT OR IGNORE #
Database kosong membuat setiap demo harus mengetik data dari nol. Kita menambahkan enam produk contoh langsung di akhir schema.sql:
INSERT OR IGNORE INTO produk (nama, kategori, harga, stok) VALUES
('Kopi Tubruk', 'Minuman', 15000, 24),
('Teh Celup', 'Minuman', 8000, 3),
('Roti Tawar', 'Makanan', 12000, 15),
('Keripik Singkong', 'Makanan', 10000, 2),
('Buku Tulis', 'Alat Tulis', 5000, 40),
('Pulpen Gel', 'Alat Tulis', 3500, 4);Kombinasi INSERT OR IGNORE dengan UNIQUE pada kolom nama membuat data awal ini aman diulang: jika sebuah nama sudah ada, SQLite diam-diam melewati baris tersebut alih-alih menimpa atau melaporkan galat. Tiga stok sengaja dibuat di bawah angka 5 supaya laporan stok menipis pada Bagian 5 langsung punya contoh.
Berkas Utuh schema.sql #
Keempat potongan di atas menyusun satu berkas tunggal. Berikut isi lengkap schema.sql yang menjadi hasil akhir bagian ini, siap disalin apa adanya:
-- schema.sql — struktur database aplikasi inventaris.
-- Aman dijalankan berulang: memakai IF NOT EXISTS dan INSERT OR IGNORE.
CREATE TABLE IF NOT EXISTS produk (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT NOT NULL UNIQUE,
kategori TEXT NOT NULL,
harga INTEGER NOT NULL CHECK (harga >= 0),
stok INTEGER NOT NULL CHECK (stok >= 0)
);
CREATE TABLE IF NOT EXISTS riwayat (
id INTEGER PRIMARY KEY AUTOINCREMENT,
produk_id INTEGER NOT NULL,
perubahan INTEGER NOT NULL,
keterangan TEXT NOT NULL,
waktu TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (produk_id) REFERENCES produk(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_produk_kategori ON produk(kategori);
CREATE INDEX IF NOT EXISTS idx_riwayat_produk ON riwayat(produk_id);
INSERT OR IGNORE INTO produk (nama, kategori, harga, stok) VALUES
('Kopi Tubruk', 'Minuman', 15000, 24),
('Teh Celup', 'Minuman', 8000, 3),
('Roti Tawar', 'Makanan', 12000, 15),
('Keripik Singkong', 'Makanan', 10000, 2),
('Buku Tulis', 'Alat Tulis', 5000, 40),
('Pulpen Gel', 'Alat Tulis', 3500, 4);Menguji Skema Sebelum Menulis Aplikasi #
Skema belum bisa dipercaya sebelum aturannya terbukti. Skrip kecil berikut menerapkan schema.sql ke database sementara di memori, lalu mencoba tiga pelanggaran secara sengaja:
"""uji_skema.py - periksa tabel, data awal, dan constraint schema.sql."""
import sqlite3
from pathlib import Path
SKEMA = Path(__file__).with_name("schema.sql").read_text(encoding="utf-8")
conn = sqlite3.connect(":memory:")
conn.execute("PRAGMA foreign_keys = ON")
conn.executescript(SKEMA)
print("Tabel yang terbentuk:")
for (nama,) in conn.execute(
"SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name"
):
print(f" {nama}")
print()
print("Index yang terbentuk:")
for (nama,) in conn.execute(
"SELECT name FROM sqlite_master WHERE type = 'index'"
" AND name LIKE 'idx%' ORDER BY name"
):
print(f" {nama}")
print()
print("Data awal di tabel produk:")
for row in conn.execute("SELECT id, nama, kategori, harga, stok FROM produk ORDER BY id"):
print(f" {tuple(row)}")
print()
try:
conn.execute(
"INSERT INTO produk (nama, kategori, harga, stok) VALUES (?, ?, ?, ?)",
("Kopi Gratis", "Minuman", -100, 5),
)
except sqlite3.IntegrityError as galat:
print(f"Cek harga negatif -> {galat}")
try:
conn.execute(
"INSERT INTO produk (nama, kategori, harga, stok) VALUES (?, ?, ?, ?)",
("Kopi Tubruk", "Minuman", 15000, 5),
)
except sqlite3.IntegrityError as galat:
print(f"Cek nama duplikat -> {galat}")
try:
conn.execute(
"INSERT INTO riwayat (produk_id, perubahan, keterangan) VALUES (?, ?, ?)",
(999, -1, "produk tidak ada"),
)
except sqlite3.IntegrityError as galat:
print(f"Cek foreign key -> {galat}")
print()
conn.executescript(SKEMA) # jalankan skema untuk kedua kalinya
jumlah = conn.execute("SELECT COUNT(*) FROM produk").fetchone()[0]
print(f"Skema dijalankan dua kali, jumlah produk tetap: {jumlah}")
conn.close()Simpan skrip ini di folder yang sama dengan schema.sql, lalu jalankan:
Output yang Dihasilkan:
Tabel yang terbentuk:
produk
riwayat
sqlite_sequence
Index yang terbentuk:
idx_produk_kategori
idx_riwayat_produk
Data awal di tabel produk:
(1, 'Kopi Tubruk', 'Minuman', 15000, 24)
(2, 'Teh Celup', 'Minuman', 8000, 3)
(3, 'Roti Tawar', 'Makanan', 12000, 15)
(4, 'Keripik Singkong', 'Makanan', 10000, 2)
(5, 'Buku Tulis', 'Alat Tulis', 5000, 40)
(6, 'Pulpen Gel', 'Alat Tulis', 3500, 4)
Cek harga negatif -> CHECK constraint failed: harga >= 0
Cek nama duplikat -> UNIQUE constraint failed: produk.nama
Cek foreign key -> FOREIGN KEY constraint failed
Skema dijalankan dua kali, jumlah produk tetap: 6Tiga exception sqlite3.IntegrityError pada output tersebut justru kabar baik: CHECK, UNIQUE, dan foreign key semuanya bekerja, dan skema yang dijalankan dua kali tidak menduplikasi data. Tabel sqlite_sequence muncul otomatis karena pemakaian AUTOINCREMENT; tabel tersebut dipakai SQLite sendiri untuk mengingat nomor id terakhir.
Struktur penyimpanan telah siap dan teruji. Berkas schema.sql yang menjadi hasil akhir bagian ini tersedia pada berkas sumber di bagian bawah halaman. Pada Bagian 2 kita menulis dua module Python pertama: database.py yang membuka koneksi dan menerapkan skema ini secara otomatis, serta main.py berupa kerangka menu aplikasi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
schema.sql
Unduh berkas-- schema.sql — struktur database aplikasi inventaris.
-- Aman dijalankan berulang: memakai IF NOT EXISTS dan INSERT OR IGNORE.
CREATE TABLE IF NOT EXISTS produk (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT NOT NULL UNIQUE,
kategori TEXT NOT NULL,
harga INTEGER NOT NULL CHECK (harga >= 0),
stok INTEGER NOT NULL CHECK (stok >= 0)
);
CREATE TABLE IF NOT EXISTS riwayat (
id INTEGER PRIMARY KEY AUTOINCREMENT,
produk_id INTEGER NOT NULL,
perubahan INTEGER NOT NULL,
keterangan TEXT NOT NULL,
waktu TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (produk_id) REFERENCES produk(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_produk_kategori ON produk(kategori);
CREATE INDEX IF NOT EXISTS idx_riwayat_produk ON riwayat(produk_id);
INSERT OR IGNORE INTO produk (nama, kategori, harga, stok) VALUES
('Kopi Tubruk', 'Minuman', 15000, 24),
('Teh Celup', 'Minuman', 8000, 3),
('Roti Tawar', 'Makanan', 12000, 15),
('Keripik Singkong', 'Makanan', 10000, 2),
('Buku Tulis', 'Alat Tulis', 5000, 40),
('Pulpen Gel', 'Alat Tulis', 3500, 4);