Di Section Database dan SQL kita menulis query SELECT ... WHERE secara manual dan memikirkan koneksi, cursor, serta parameterized query sendiri. Django menawarkan jalan lain: cukup deklarasikan class Python, dan ORM (Object-Relational Mapping) yang menerjemahkan class menjadi tabel serta method menjadi query. Pada chapter ini kita lanjutkan proyek toko dari chapter Pengenalan Django: membuat model produk, menjalankan migrations sungguhan, lalu meng-query data tanpa menulis satu baris SQL pun.
Model adalah Peta Tabel #
Satu class model mewakili satu tabel; satu atribut class mewakili satu kolom. Isi katalog/models.py:
from django.db import models
class Kategori(models.Model):
nama = models.CharField(max_length=100)
def __str__(self):
return self.nama
class Produk(models.Model):
nama = models.CharField(max_length=200)
harga = models.IntegerField()
stok = models.IntegerField(default=0)
kategori = models.ForeignKey(Kategori, on_delete=models.CASCADE)
dibuat_pada = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.namaDua hal penting dari kode di atas: setiap model mewarisi models.Model, dan method __str__ (materi Section Object-Oriented Programming) membuat objek tampil ramah di admin. Field yang dipakai memetakan tipe data dengan aturannya:
| Field | Wujud di database | Keterangan |
|---|---|---|
CharField(max_length=100) | varchar(100) | teks pendek, max_length wajib |
IntegerField() | integer | bilangan bulat |
DateTimeField(auto_now_add=True) | datetime | terisi otomatis saat objek dibuat |
ForeignKey(..., on_delete=models.CASCADE) | foreign key | relasi banyak-produk-ke-satu-kategori |
Kolom id tidak kita tulis — Django menambahkannya otomatis sebagai primary key bertambah. Sedangkan on_delete=models.CASCADE menentukan nasib produk saat kategorinya dihapus: ikut terhapus.
Migrations: Skema yang Bisa Dilacak #
Class model di atas baru berupa kode Python; tabel database belum ada. Perintah makemigrations membaca model lalu menuliskan rencana pembuatan tabel sebagai berkas Python:
python3 manage.py makemigrations katalogOutput yang Dihasilkan:
Migrations for 'katalog':
katalog/migrations/0001_initial.py
+ Create model Kategori
+ Create model ProdukBerkas katalog/migrations/0001_initial.py benar-benar ada dan bisa dibaca — komentar header berisi versi dan waktu pembuatan, sisanya rencana berbentuk deklaratif:
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Kategori',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nama', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Produk',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nama', models.CharField(max_length=200)),
('harga', models.IntegerField()),
('stok', models.IntegerField(default=0)),
('dibuat_pada', models.DateTimeField(auto_now_add=True)),
('kategori', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='katalog.kategori')),
],
),
]Migration dijalankan ke database dengan migrate — migrasi bawaan Django (auth, sessions, admin) ikut diterapkan pada eksekusi pertama:
python3 manage.py migrateOutput yang Dihasilkan:
Operations to perform:
Apply all migrations: admin, auth, contenttypes, katalog, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying katalog.0001_initial... OK
Applying sessions.0001_initial... OKKonsepnya sama dengan chapter Database Migrations: skema database kini punya riwayat berkode, dan mengubah model tinggal diikuti makemigrations + migrate baru.
Query dengan objects Manager #
Setiap model memiliki pengurus bernama objects — pintu masuk semua query. Skrip berikut dijalankan pada database kosong hasil migrate (dengan django.setup() seperti biasa):
from katalog.models import Kategori, Produk
buku = Kategori.objects.create(nama="Buku")
elektronik = Kategori.objects.create(nama="Elektronik")
Produk.objects.create(nama="Buku Python", harga=95000, stok=12, kategori=buku)
Produk.objects.create(nama="Buku Flask", harga=85000, stok=5, kategori=buku)
Produk.objects.create(nama="Mouse Wireless", harga=150000, stok=3, kategori=elektronik)
print("Jumlah produk:", Produk.objects.count())
for produk in Produk.objects.filter(harga__lt=100000).order_by("harga"):
print("-", produk.nama, produk.harga)
mouse = Produk.objects.get(nama="Mouse Wireless")
print("Get satu objek:", mouse.nama, "| kategori:", mouse.kategori.nama)
print(
"Filter relasi:",
list(Produk.objects.filter(kategori__nama="Buku").values_list("nama", flat=True)),
)Output yang Dihasilkan:
Jumlah produk: 3
- Buku Flask 85000
- Buku Python 95000
Get satu objek: Mouse Wireless | kategori: Elektronik
Filter relasi: ['Buku Python', 'Buku Flask']Empat pola query utama tampak pada contoh di atas:
- Baris 3-4 (
Kategori.objects.create(...)): membuat satu baris baru sekaligus mengembalikan objeknya — penggantiINSERT. - Baris 15 (
filter(harga__lt=100000)): tanda dua garis bawah berarti operator;harga__ltmenjadiharga < 100000. Daftar lengkapnya dariexactsampaiicontainsada di dokumentasi resmi Django. - Baris 18 (
get(nama=...)): mengembalikan tepat satu objek; bila nol atau lebih dari satu,get()melempar exception. - Baris 23 (
filter(kategori__nama="Buku")): filter melompati relasiForeignKey— Django menyusunJOINsecara otomatis.
QuerySet itu Lazy #
Method seperti filter() tidak langsung menyentuh database. Method tersebut mengembalikan QuerySet — rencana query yang baru dieksekusi saat datanya benar-benar dibutuhkan. Bukti nyatanya:
queryset = Produk.objects.filter(harga__lt=1000000).order_by("nama")
Produk.objects.create(nama="Keyboard Mekanik", harga=750000, stok=2, kategori=elektronik)
for produk in queryset:
print("-", produk.nama)
print("Monitor ada?", Produk.objects.filter(nama="Monitor").exists())Output yang Dihasilkan:
- Buku Flask
- Buku Python
- Keyboard Mekanik
- Mouse Wireless
Monitor ada? FalseKeyboard Mekanik dibuat setelah baris pertama, tetapi tetap muncul saat queryset dievaluasi pada loop — query SELECT baru berjalan di baris loop tersebut, bukan saat filter() dipanggil. Perilaku ini memungkinkan QuerySet dirantai (filter(...).exclude(...).order_by(...)) tanpa query sia-sia di tengah rantai.
Sebagai penutup: untuk aplikasi Flask, ORM yang paling lazim dipakai bernama SQLAlchemy — konsepnya sejajar (class menjadi tabel, query menjadi method), sehingga pemahaman dari chapter ini tetap terpakai.