Post Detail

September 13, 2026 in Python Lanjutan

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:

1. Menghubungkan Dua Tabel dengan Foreign Key

Kita akan membagi data menjadi dua tabel:

  1. Tabel Induk (kategori): Menyimpan identitas kategori dan lokasi fisik rak penyimpanan (id, nama_kategori, lokasi_rak).
  2. Tabel Anak (produk): Menyimpan data barang dan mereferensikan kategori melalui kolom kategori_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()
Tangkapan Layar Terminal: Membuat tabel kategori dan produk dengan relasi Foreign Key
Tangkapan layar terminal: Berhasil membuat tabel kategori dan produk yang saling terhubung dengan Foreign Key.

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()
Tangkapan Layar Terminal: Hasil query INNER JOIN menggabungkan data produk dan kategori
Tangkapan layar terminal: Data dari tabel produk dan kategori berhasil disatukan dalam satu query.

💡 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()
Tangkapan Layar Terminal: Menghitung statistik inventaris dengan COUNT, SUM, AVG, dan GROUP BY
Tangkapan layar terminal: Laporan ringkasan valuasi aset dan total stok per kategori menggunakan klausa GROUP BY.

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()
Animasi Terminal: Demonstrasi eksekusi uji coba serangan SQL Injection dan proteksinya di Python
Animasi langsung hasil eksekusi terminal: Perbandingan nyata filter yang bobol pada f-string vs netralisasi total pada parameterized query.

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 JOIN menyatukan baris yang cocok antara dua tabel, sedangkan LEFT JOIN tetap menyertakan data tabel kiri meskipun belum memiliki relasi di tabel kanan.
  • Fungsi agregasi seperti COUNT(), SUM(), dan AVG() dipadukan dengan GROUP BY sangat 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

Your email address will not be published. Required fields are marked *