Mengenal Database di Python (Bagian 2): Relasi Multi-Tabel (JOIN), Agregasi, dan Keamanan SQL Injection
Pada artikel sebelumnya, kita telah mempelajari cara menyimpan dan memanipulasi data inventaris pada sebuah tabel tunggal. Namun, ketika aplikasi bisnismu mulai berkembang, menyimpan semua informasi ke dalam satu tabel akan menimbulkan masalah serius: pemborosan memori dan data duplikat (*redundancy*).
Bayangkan jika kamu memiliki 1.000 produk aksesoris. Menuliskan teks “Aksesoris Komputer, Lokasi Rak A-01” berulang-ulang sebanyak 1.000 kali di setiap baris produk bukan hanya boros, tetapi juga berisiko: jika lokasi rak berpindah, kamu harus mengubah 1.000 baris data sekaligus!
Di sinilah konsep Relasi Multi-Tabel (Relational Database) menjadi penyelamat. Kita memisahkan entitas data ke tabelnya masing-masing, lalu menghubungkannya menggunakan Foreign Key dan menggabungkannya saat dibutuhkan dengan klausa JOIN.
Artikel ini adalah Bagian 2 dari serial panduan Database di Python:
- Bagian 1: Fondasi SQLite dan Operasi CRUD Pertama (Koneksi, pembuatan tabel, penyimpanan permanen, dan operasi Create, Read, Update, Delete).
- Bagian 2 (Sedang Dibaca): Relasi Multi-Tabel (JOIN), Agregasi & Keamanan Query (Foreign key, query multi-tabel, sqlite3.Row, dan bahaya SQL Injection vs Parameterized Query).
- Bagian 3: Arsitektur Python OOP + Database: Mini Inventory App (Integrasi Class Repository, context manager with, error handling, dan antarmuka menu CLI).
1. Menghubungkan Dua Tabel dengan Foreign Key
Kita akan membagi data menjadi dua tabel:
- Tabel Induk (
kategori): Menyimpan identitas kategori dan lokasi fisik rak penyimpanan (id,nama_kategori,lokasi_rak). - Tabel Anak (
produk): Menyimpan data barang dan mereferensikan kategori melalui kolomkategori_id.
Perhatian Penting tentang SQLite: Secara default, SQLite menonaktifkan fitur pemeriksaan integritas relasi. Agar aturan Foreign Key ditegakkan, kamu wajib menyalakannya di awal skrip dengan perintah PRAGMA foreign_keys = ON;:
# 01-buat-tabel-relasi-foreign-key.py
import sqlite3
conn = sqlite3.connect("toko_relasi.db")
cursor = conn.cursor()
# 1. Wajib aktifkan fitur Foreign Key di SQLite
cursor.execute("PRAGMA foreign_keys = ON;")
# 2. Tabel Induk: Kategori
cursor.execute("""
CREATE TABLE IF NOT EXISTS kategori (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama_kategori TEXT NOT NULL UNIQUE,
lokasi_rak TEXT NOT NULL
);
""")
# 3. Tabel Anak: Produk (merujuk ke tabel kategori)
cursor.execute("""
CREATE TABLE IF NOT EXISTS produk (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kode TEXT UNIQUE NOT NULL,
nama TEXT NOT NULL,
kategori_id INTEGER NOT NULL,
harga REAL NOT NULL,
stok INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (kategori_id) REFERENCES kategori (id) ON DELETE CASCADE
);
""")
# 4. Memasukkan data kategori
data_kategori = [
("Aksesoris Komputer", "Rak A-01"),
("Layar & Monitor", "Rak B-02"),
("Perangkat Audio", "Rak C-01"),
]
cursor.executemany("INSERT OR IGNORE INTO kategori (nama_kategori, lokasi_rak) VALUES (?, ?)", data_kategori)
# 5. Memasukkan data produk dengan kategori_id terkait
data_produk = [
("PRD-101", "Keyboard Mekanikal TKL", 1, 450000.0, 15),
("PRD-102", "Mouse Gaming Nirkabel", 1, 280000.0, 24),
("PRD-103", "Monitor IPS 24 Inci 144Hz", 2, 1850000.0, 8),
("PRD-104", "Headset Bluetooth ANC", 3, 620000.0, 12),
("PRD-105", "Speaker Soundbar Mini", 3, 350000.0, 10),
]
cursor.executemany("INSERT OR IGNORE INTO produk (kode, nama, kategori_id, harga, stok) VALUES (?, ?, ?, ?, ?)", data_produk)
conn.commit()
print("[OK] Database relasional berhasil dibuat dan terhubung!")
conn.close()

Klausa ON DELETE CASCADE memastikan bahwa jika sebuah kategori dihapus, seluruh produk yang bernaung di bawah kategori tersebut akan otomatis terhapus secara rapi tanpa meninggalkan “data yatim” (*orphan data*).
2. Menggabungkan Data dengan INNER JOIN
Tabel produk hanya menyimpan angka ID kategori (misal: kategori_id = 1). Saat menyajikan laporan ke pengguna atau manajer toko, kita ingin menampilkan nama kategori aslinya (*”Aksesoris Komputer”*) dan lokasi raknya (*”Rak A-01″*).
Perintah INNER JOIN mencocokkan baris dari kedua tabel berdasarkan persamaan kunci relasi:
# 02-query-inner-join.py
import sqlite3
conn = sqlite3.connect("toko_relasi.db")
conn.row_factory = sqlite3.Row # Membaca kolom dengan nama
cursor = conn.cursor()
# Menggabungkan produk (p) dan kategori (k)
sql_join = """
SELECT
p.kode,
p.nama AS nama_barang,
k.nama_kategori,
k.lokasi_rak,
p.harga,
p.stok
FROM produk p
INNER JOIN kategori k ON p.kategori_id = k.id
ORDER BY k.nama_kategori ASC, p.harga ASC;
"""
cursor.execute(sql_join)
hasil_join = cursor.fetchall()
print("\n--- LAPORAN INVENTARIS TERPADU (INNER JOIN) ---")
for row in hasil_join:
harga_fmt = f"{int(row['harga']):,}".replace(",", ".")
print(f"[{row['kode']}] {row['nama_barang']:<26} | {row['nama_kategori']:<18} | {row['lokasi_rak']:<8} | Rp {harga_fmt}")
conn.close()

💡 Coba Query JOIN Ini Langsung di Browser:
Kamu bisa mempraktikkan query multi-tabel di atas secara instan tanpa mengoding dari awal di SQL Playground Interaktif Skenario Toko Relasi (Klik di Sini)!
3. Ringkasan Statistik: Agregasi dan GROUP BY
Dalam bisnis, kita sering memerlukan jawaban atas pertanyaan analitis seperti: “Berapa total nilai rupiah dari seluruh stok layar monitor kita?” atau “Berapa harga rata-rata aksesoris komputer?”.
SQL menyediakan fungsi agregasi andalan:
COUNT(): Menghitung total banyaknya barang.SUM(): Menjumlahkan seluruh angka (misal total stok atau total valuasi harga).AVG(): Menghitung nilai rata-rata.GROUP BY: Mengelompokkan hasil perhitungan berdasarkan kategori tertentu.
# 03-agregasi-dan-group-by.py
import sqlite3
conn = sqlite3.connect("toko_relasi.db")
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
sql_agregasi = """
SELECT
k.nama_kategori,
COUNT(p.id) AS total_jenis_barang,
SUM(p.stok) AS total_unit_stok,
AVG(p.harga) AS rata_rata_harga,
SUM(p.harga * p.stok) AS total_nilai_aset
FROM kategori k
LEFT JOIN produk p ON k.id = p.kategori_id
GROUP BY k.id, k.nama_kategori
ORDER BY total_nilai_aset DESC;
"""
cursor.execute(sql_agregasi)
for r in cursor.fetchall():
aset_fmt = f"{int(r['total_nilai_aset']):,}".replace(",", ".")
print(f"Kategori: {r['nama_kategori']:<18} | Total Stok: {r['total_unit_stok']:<4} | Valuasi: Rp {aset_fmt}")
conn.close()

4. Uji Coba Nyata: Bahaya SQL Injection dan Mitigasinya
Banyak pemrogram pemula membuat kesalahan fatal saat mencari data: mereka menggabungkan input pengguna secara langsung ke dalam string SQL menggunakan string concatenation atau f-string Python:
# SANGAT BERBAHAYA! JANGAN PERNAH MENULIS SEPERTI INI:
query = f"SELECT * FROM produk WHERE nama = '{input_pengguna}'"
Mari kita buktikan secara nyata apa yang terjadi jika peretas (*hacker*) memasukkan teks manipulasi logika: ' OR '1'='1:
# 04-demonstrasi-sql-injection-keamanan.py
import sqlite3
conn = sqlite3.connect("toko_relasi.db")
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# Input jahat dari pengguna
input_hacker = "' OR '1'='1"
print("--- UJI 1: METODE RENTAN (f-string) ---")
# String menyatu menjadi: WHERE nama = '' OR '1'='1' (selalu bernilai True!)
query_rentan = f"SELECT kode, nama, harga FROM produk WHERE nama = '{input_hacker}'"
cursor.execute(query_rentan)
bocor = cursor.fetchall()
print(f"Hasil: {len(bocor)} data bocor keluar! Filter berhasil dibobol hacker.")
print("\n--- UJI 2: METODE AMAN (Parameterized Query '?') ---")
# Tanda tanya (?) memperlakukan input hacker sebagai teks pencarian biasa, bukan kode SQL!
query_aman = "SELECT kode, nama, harga FROM produk WHERE nama = ?"
cursor.execute(query_aman, (input_hacker,))
aman = cursor.fetchall()
print(f"Hasil: {len(aman)} data ditemukan. Sistem 100% aman!")
conn.close()

Mengapa Parameterized Query Kebal terhadap Serangan?
Saat menggunakan tanda tanya ?, skrip SQL dikompilasi terlebih dahulu oleh mesin database sebelum nilai variabel dimasukkan. Karakter petik tunggal dan kata kunci OR dianggap sebagai karakter teks biasa (*literal string*), sehingga tidak mungkin membelokkan logika query!
Ringkasan Materi
- Gunakan Relasi Multi-Tabel (Foreign Key) untuk menormalisasi data, menghindari duplikasi teks berulang, dan mempermudah pemeliharaan database.
- Klausa
INNER JOINmenyatukan baris yang cocok antara dua tabel, sedangkanLEFT JOINtetap menyertakan data tabel kiri meskipun belum memiliki relasi di tabel kanan. - Fungsi agregasi seperti
COUNT(),SUM(), danAVG()dipadukan denganGROUP BYsangat ampuh untuk menghasilkan laporan analitik bisnis. - Hukum Wajib Keamanan Database: Jangan pernah menggabungkan variabel pengguna langsung ke dalam string query SQL. Selalu gunakan Parameterized Query (
?) untuk memutus celah kejahatan SQL Injection.
Langkah Selanjutnya: Menuju Arsitektur OOP & Aplikasi Terminal
Di artikel penutup serial database ini, kita akan menggabungkan seluruh keahlian SQL ini dengan paradigma Pemrograman Berorientasi Objek (OOP): membuat class arsitektur repositori, mengelola transaksi atomik aman, dan membangun antarmuka menu CLI interaktif yang siap dipakai!
👉 Lanjut membaca artikel penutup serial: Bagian 3: Arsitektur Python OOP + Database: Mini Inventory App

Leave a Reply