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:

python
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.nama

Dua 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:

FieldWujud di databaseKeterangan
CharField(max_length=100)varchar(100)teks pendek, max_length wajib
IntegerField()integerbilangan bulat
DateTimeField(auto_now_add=True)datetimeterisi otomatis saat objek dibuat
ForeignKey(..., on_delete=models.CASCADE)foreign keyrelasi 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:

bash
python3 manage.py makemigrations katalog

Output yang Dihasilkan:

text
Migrations for 'katalog':
  katalog/migrations/0001_initial.py
    + Create model Kategori
    + Create model Produk

Berkas katalog/migrations/0001_initial.py benar-benar ada dan bisa dibaca — komentar header berisi versi dan waktu pembuatan, sisanya rencana berbentuk deklaratif:

python
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:

bash
python3 manage.py migrate

Output yang Dihasilkan:

text
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... OK

Konsepnya 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):

python
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:

text
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:

  1. Baris 3-4 (Kategori.objects.create(...)): membuat satu baris baru sekaligus mengembalikan objeknya — pengganti INSERT.
  2. Baris 15 (filter(harga__lt=100000)): tanda dua garis bawah berarti operator; harga__lt menjadi harga < 100000. Daftar lengkapnya dari exact sampai icontains ada di dokumentasi resmi Django.
  3. Baris 18 (get(nama=...)): mengembalikan tepat satu objek; bila nol atau lebih dari satu, get() melempar exception.
  4. Baris 23 (filter(kategori__nama="Buku")): filter melompati relasi ForeignKey — Django menyusun JOIN secara 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:

python
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:

text
- Buku Flask
- Buku Python
- Keyboard Mekanik
- Mouse Wireless
Monitor ada? False

Keyboard 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.