Desain Model Django untuk API
Gambaran Umum
Sebelumnya kita sudah membuat model Book dengan pendekatan yang natural dari perspektif database. Pada development API, kita perlu sedikit merubah mindset: setiap model yang kita desain, pada akhirnya akan di-serialize menjadi JSON dan dikonsumsi / digunakan oleh frontend, mobile app, atau third-party service. Ini akan merubah keputusan kita mengenai field yang digunakan, cara penamaannya, dan metadata apa yang perlu ada.
Kali ini, kita akan membuat app baru bernama articles untuk dapat memahaminya. Apps tentang artikel ini cocok karena mencakup banyak konsep penting: ownership, timestamps, status workflow, dan slug untuk URL.
Membuat App articles
| |
Lalu register di config/settings.py:
| |
User Relationship (ForeignKey)
Relasi paling umum di API adalah: “data ini milik siapa?” Dalam kasus kita: Article memiliki author, dan author adalah User.
Django sudah menyediakan model User atau (django.contrib.auth.models.User). Kita tidak perlu membuatnya lagi, dan hanya perlu mereferensi model ini.
| |
Ada 3 hal yang perlu diperhatikan:
Kenapa menggunakan settings.AUTH_USER_MODEL dibandingkan import User secara langsung?
Kita mungkin pernah melihat kode seperti ini pada tutorial:
| |
Ini dapat berfungsi, tetapi cara tersebut meng-hardcode ke model User. Kalau nanti kita mau merubah menggunakan model custom untuk user,, setiap ForeignKey yang diimport dari User akan menjadi bermasalah. settings.AUTH_USER_MODEL adalah string reference (yang secara default adalah "auth.User") yang ditentukan / diproses oleh Django pada saat runtime. Menggunakan settings.AUTH_USER_MODEL adalah cara yang paling direkomendasikan.
Penting diingat: Selalu gunakan settings.AUTH_USER_MODEL untuk ForeignKey/OneToOne ke User. Jangan langsung import model User secara langsung di models.py.
Apa artinya on_delete=models.CASCADE?
Ini menjawab Apa yang terjadi pada artikel jika akun user author dihapus?
| Option | Behaviour | Kapan Digunakan |
|---|---|---|
CASCADE | Artikel ikut dihapus | Artikel tidak masuk akal jika tidak ada author |
PROTECT | Tidak dapat dihapus — menimbulkan error | Data perlu selalu ada (contoh: invoices) |
SET_NULL | Set author jadi NULL (perlu null=True) | Simpan artikel, tapi ditandai “author sudah dihapus” |
SET_DEFAULT | Set ke value default | Reassign ke akun “deleted user” |
DO_NOTHING | Tidak terjadi apa-apa (berbahaya — dapat membuat DB bermasalah) | Hampir tidak pernah |
Untuk artikel, CASCADE adalah pilihan yang tepat: jika akun user-nya dihapus, artikel-artikelnya juga mengikuti author-nya. Untuk sesuatu yang bersifat seperti Order di sistem e-commerce, kita akan menggunakan PROTECT karena kita tidak mau hapus order history secara tidak sengaja.
Apa fungsi related_name=‘articles?
related_name='articles mendefinisikan reverse relationship, yaitu cara kita akses artikel-artikel seorang author dari sisi User
| |
Tanpa related_name, Django auto-generate: article_set. Itu dapat digunakan tapi kodenya kurang enak dibaca dan nantinya akan menjadi lebih parah di serializer. Saat kita mau serialize User dan mau include artikel-artikelnya, "articles" itu yang API consumer ekspektasikan, bukan “article_set”.
Penting Diingat: Selalu set related_name secara eksplisit. Beri nama menggunakan bentuk plural dari model-nya, itu yang menjadi ekspektasi dari API consumer.
Field yang Masuk Akal untuk API Consumer
Saat membuat design model untuk API, selalu tanyakan ke diri sendiri: “Apa yang client perlukan untuk menampilkan dan berinteraksi dengan resource ini?”
Ini model Article model yang sudah dikembangkan:
| |
Sekarang mari kita bahas pilihan fieldnya:
CharField vs TextField
| Field | Tipe kolom DB | Constraint |
|---|---|---|
CharField | VARCHAR(n) | Butuh max_length, disimpan dengan alokasi batasan length yang sudah ditentukan |
TextField | TEXT | Tidak perlu ``max_length`, disimpan dengan batasan length variable |
Gunakan CharField untuk yang secara umum ada batasan length (judul, nama, kode). Pakai TextField untuk data yang lebih freeform (isi article, komentar, deskripsi).
Kenapa ini penting untuk API: Serializer kita akan validasi berdasarkan constraint ini. Kalau client kirim judul 500 karakter dan max_length=200, DRF otomatis return 400 Bad Request. Dari definisi model kita mendapatkan validasi input secara otomatis.
blank=True vs null=True — Konsep Django yang Paling Sering Bikin Bingung
Kedua ini adalah dua hal yang berbeda:
| Parameter | Scope | Artinya |
|---|---|---|
blank=True | Validasi form/serializer | “Field ini boleh disubmit sebagai string kosong” |
null=True | Level database | “Kolom ini bisa menyimpan NULL” |
Untuk string field (CharField, TextField), convention untuk Django adalah:
- Hanya gunakan
blank=Truekalau field-nya optional - JANGAN tambahkan
null=Trueuntuk string field
Kenapa begitu? Karena nanti kita punya dua kemungkinan nilai “kosong” di database: NULL dan "" (empty string). Ini bikin ambigu waktu melakukan query. Konvensi Django itu pakai "" untuk “tidak ada value” di string field, dan NULL untuk field yang non-string.
| |
Untuk non-string field (tanggal, integer, ForeignKey), kita gunakan null=True, blank=True jika kedua field-nya optional, karena tipe-tipe data ini tidak berguna menyimpan string kosong.
Timestamp dan Audit Field
Hampir setiap resource API perlu dua field: kapan dibuat dan kapan terakhir dimodifikasi. Field ini dibutuhkan untuk hampir semua case, sehingga sebaiknya kita anggap sebagai mandatory.
| |
Cara kerja auto_now_add dan auto_now
| Parameter | Kapan Nilainya di set | Bisa di-override manual? |
|---|---|---|
auto_now_add=True | Sekali — pas object pertama kali dibuat | Tidak (diabaikan saat save()) |
auto_now=True | Setiap kali save() dipanggil | Tidak (selalu di-overwrite) |
Artinya:
created_atdi-set sekali dan tidak pernah berubahupdated_atotomatis mengikuti modifikasi terakhir
Kenapa ini penting untuk API: Client menggunakan timestamp ini untuk caching, sorting, dan menampilkan data. Aplikasi mobile bisa jadi menampilkan “posted 3 hours ago” atau frontend mungkin melakukan sortir article berdasarkan update terbaru. Tanpa field-field ini, client tidak memili cara untuk mengetahui kapan data berubah.
Pattern Abstract Base Model
Karena created_at dan updated_at diperlukan di hampir setiap model, terus mengulangi hal yang sama akan mubazir. Pattern standar dari Django-nya adalah abstract base model:
| |
abstract = True artinya Django tidak akan membuat tabel database untuk TimestampedModel. Dia ada hanya untuk menjadi parent class yang digunakan sebagai inheritance. Model apapun yang inherit dari sini otomatis dapat kedua timestamp field:
| |
Dimana kita akan meletakkan abstract model-nya? Untuk project belajar seperti ini, kita bisa taruh di atas file articles/models.py. Di project sebenarnya dengan banyak app yang menggunakannya, kita akan meletakkannya di app common atau core dan didefinisikan di sana.
Choice Field untuk Data Status/State
Kebanyakan API resource punya semacam notion of state (konsep tentang kondisi/status suatu resource pada saat tertentu). Maksudnya, hampir semua resource di API itu punya semacam “kondisi” yang menggambarkan di tahap mana dia sekarang dalam lifecycle-nya.
Dalam konteks artikel, notion of statenya bisa berupa draft, published, atau archived. Task bisa pending, in progress, atau done. Order bisa placed, shipped, atau delivered.
Django menangani ini dengan choices:
| |
Penjelasan TextChoices
TextChoices adalah class enum khusus yang diperkenalkan di Django 3.0. Setiap member punya dua bagian:
| |
- Value (
'draft') adalah yang disimpan di database dan yang muncul di JSON API kita - Label (
'Draft') adalah yang muncul di Django admin dan form
Kenapa pakai TextChoices dan bukan tuple?
Style lama seperti ini:
| |
TextChoices lebih baik karena:
- Kita mendapatkan constant:
Article.Status.DRAFTadalah named reference. Tidak ada lagi string yang rawan typo berserakan di codebase. - Kita mendapatkan type safety:
article.status = Article.Status.PUBLISHEDitu self-documenting. - Bisa di-iterasi:
Article.Status.choicesreturn list of tuple secara otomatis. - Merupakan nested class: Membuat choices tetap scoped ke model-nya, tidak mengambang di module level.
Tampilannya di API
Nanti saat kita build serializer, response GET akan include:
| |
Value ('draft') tersimpan itu yang diterima API consumer kita. Label ('Draft') itu untuk human interface. DRF juga secara otomatis memvalidasi value yang masuk. Jika client kirim "status": "banana", mereka akan mendapatkan response 400 Bad Request beserta daftar pilihan yang valid.
Querying menggunakan choices
| |
Tidak ada magic string. Kalau ninta kita rename value dari status, IDE kita bisa menemukan semua reference-nya.
IntegerChoices — alternatif numerik
Untuk kasus di mana value database-nya harus integer (priority level, ranking):
| |
Gunakan TextChoices kalau value-nya bermanfaat untuk dibaca di database atau API ("draft" masuk akal). Gunakan IntegerChoices kalau kita butuh numeric ordering atau value-nya memang angka.
Slug Field untuk Clean URL
Slug itu versi URL-friendly dari sebuah string. Daripada:
| |
Kita dapat:
| |
Ini penting untuk API karena:
- SEO: Search engine lebih suka URL yang readable
- Usability: User bisa baca dan share URL-nya
- Stability: Slug bisa tetap stabil meskipun judul berubah (kalau kita pilih untuk tidak auto-update)
Menambahkan SlugField
| |
Apa sebenarnya SlugField
SlugField hanyalah CharField dengan built-in validator yang cuma memperbolehkan huruf, angka, hyphen, dan underscore. Di balik layar:
| |
max_length-nya secara default nilainya 50. Karena title kita perbolehkan 200 karakter, maka kita set slug-nya juga sama.
Kenapa unique=True di slug?
Jika kita mau menggunakan slug sebagai URL lookup (/articles/my-first-article/), slug-nya harus unique. Tanpa unique=True, dua article berjudul “My First Article” akan sama-sama mendapatkan slug my-first-article, dan URL routing kita tidak tahu yang mana yang harus di-return.
Override save() — auto-generate slug
| |
slugify() convert "My First Article!" menjadi "my-first-article". Perlindungan if not self.slug berfungsi:
- Saat creation: Jika tidak ada slug yang di-provide, generate dari title
- Saat update: Jika slug-nya udah ada, maka diabaikan.
Hal tersebut dibuat sedemikian rupa. Begitu article sudah di-published dan orang-orang udah bookmark URL-nya, kita tidak mau ganti slug hanya karena seseorang edit judulnya. Broken link lebih buruk daripada slug yang sedikit outdated.
Handle slug collision
save() sederhana di atas punya kelemahan: jika dua article punya judul yang sama, yang kedua akan gagal dengan database IntegrityError (karena unique=True). Solusi yang lebih baik itu adalah dengan menambahkan sufiks/akhiran:
| |
Ini akan menghasilkan: my-first-article, my-first-article-1, my-first-article-2, dst. Saat ini kita tidak butuh kerumitan ini untuk project belajar, tapi sebaiknya kita mengerti pola-nya.
Model Lengkap
Ini articles/models.py final yang semuanya sudah digabungkan:
| |
Perhatikan bahwa created_at dan updated_at tidak dituliskan di dalam Article. Mereka datang dari TimestampedModel melalui inheritance.
Model Meta Class (Preview)
Kita bahas ini lebih dalam nanti, tapi ini begini gambarannya karena relevan dengan bagaimana model kita akan berperilaku di API:
| |
| Option | Efek |
|---|---|
ordering = ['-created_at'] | Default sort order — Terbaru duluan. Setiap queryset return article dalam urutan ini kecuali di-override. Prefix - artinya descending. |
verbose_name | Bagaimana model-nya muncul di Django admin dan error message. Django auto-generate ini dari nama class, tapi jika dibuat secara eksplisit lebih baik. |
verbose_name_plural | Bentuk plural. Tanpa ini, Django akan menambahkan “s” secara apa adanya (Contoh yang salah “Category” → “Categorys”). |
Kenapa ordering penting untuk API: Tanpa default ordering, GET /articles/ return baris-baris dalam urutan database di-insert, itu arbitrary dan tidak stabil. Client memiliki ekspektasi urutan yang konsisten dan predictable. Mengatur orderini di model berarti setiap queryset — termasuk yang di-generate oleh generic view DRF, akan return data dalam urutan yang benar tanpa konfigurasi tambahan.
Praktik dan Latihan
Ini yang perlu kita lakukan sekarang:
Buat app articles dan register
| |
Tambahkan 'articles' ke INSTALLED_APPS di config/settings.py.
Tulis model yang lengkap di articles/models.py
Copy model lengkap dari Part 8 di atas. Baca setiap baris dan pastikan pahami kenapa setiap keputusan dibuat.
Buat dan apply migration
| |
Coba di Django shell
| |
| |
Test constraint-nya
| |
Poin terakhir diatas penting: choices itu validation constraint, bukan database constraint. Database menyimpan string apapun yang kita berikan. Enforcement-nya terjadi di DRF serializer dan Django form. Ini kenapa serializer validation itu perlu.
Rangkuman Model
| |
Setiap field yang kita tambahkan ke model itu adalah janji ke API consumer kita. Bayangkan models.py kita sebagai sebuah kontrak: dia mendefinisikan data apa yang ada, apa yang wajib ada, apa yang optional, dan value apa yang valid. Semakin akurat kontrak tersebut, semakin ringan juga kerja dari serializer, view, dan frontend code kita.
