Pengalaman dengan Django Migrations
Salah satu fitur django yang menurut saya disepelekan adlah fitur migrations di django, fitur ini hanya diserderhanakan sebagai pengganti import sql saja, padahal fitur migration ini sangat berguna apalagi jika kita cukup aware terhadap sql DDL karena migrations ini cukup erat dengan perintah-perintah DDL di SQL.
SQL DDL
Apa itu DDL? DDL atau Data Definition Language adalah bagian sql yang bertanggung jawab dalam mendefisikan strukt tabel, makanya perintah-perintah DDL itu berkutat di:
- Create
- Alter
- Drop
Django Models
Django migrations itu bekerja cukup erat dengan models, karena pada dasarnya models di django selain dekat dengan orm, django models juga bisa kita anggap sebagai blueprint atau skema tabel yang nantinya tertuang di database.
Contoh bagaimana django models bersifat sebagai skema adalah seperti ini, jika kita punya struktur models seperti ini:
class GuestBook(models.Model):
KTP = 1
SIM = 2
DLL = 3
IDENTITY_CHOICE = (
(KTP, "KTP"),
(SIM, "SIM"),
(DLL, "Dll"),
)
name = models.CharField(max_length=100, validators=[MinLengthValidator(25)])
identity_type = models.PositiveIntegerField(choices=IDENTITY_CHOICE)
identity_number = models.CharField(max_length=100, validators=[DissalowSpace])
reason = models.TextField()
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
Itu sama dengan bikin perintah create table seperti ini
CREATE TABLE "guestbook_guestbook" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(100) NOT NULL,
"identity_type" integer unsigned NOT NULL CHECK ("identity_type" >= 0),
"identity_number" varchar(100) NOT NULL,
"reason" text NOT NULL,
"date_created" datetime NOT NULL,
"date_updated" datetime NOT NULL
);
Tapi membuat django model tidak otomatis membuat seperti itu, itu bisa terjadi ketika kita menjalankan perintah "makemigrations"
Makemigrations
Saat setelah mendesain model, kita lakukan perintah `makemigrations` nantinya django akan membuatkan 1 berkas di folder migrations, jika itu pertama kali, django akan membuatkan berkas dengan nama `0001_initial.py`, contoh berkas migrations dari models di atas di berkas `0001
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='GuestBook',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, validators=[django.core.validators.MinLengthValidator(25)])),
('identity_type', models.PositiveIntegerField(choices=[(1, 'KTP'), (2, 'SIM'), (3, 'Dll')])),
('identity_number', models.CharField(max_length=100, validators=[guestbook.validator.DissalowSpace])),
('reason', models.TextField()),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
],
),
]
Untuk tahu query yang dihasilkan kita bisa menggunakan perintah python manage.py sqlmigrate appname filename
CREATE TABLE "guestbook_guestbook" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(100) NOT NULL,
"identity_type" integer unsigned NOT NULL CHECK ("identity_type" >= 0),
"identity_number" varchar(100) NOT NULL,
"reason" text NOT NULL,
"date_created" datetime NOT NULL,
"date_updated" datetime NOT NULL
);
Perintah makemigrations harus kita jalankan setiap ada perubahan di models, nantinya di django akan menghasilkan berkas baru di folder migrations, contoh jika kita menambahkan 1 field baru di model seperti ini
class GuestBook(models.Model):
...
new_field = models.BooleanField(default=False)
...
jalankan kembali `makemigrations` hasilnya akan menghasilkan berkas seperti ini `0002_auto_20230903_1457.py`
Di django versi 3 ke bawah strukturnya seperti itu no_urut_auto_tanggal_waktu, di vesi terbaru namanya lebih jelas.
Tapi kita bisa memberi nama dengan cara makemigration appname --name your_name
, hasilnya akan `0002_your_name.py`.
Problem
Dengan kemudahan seperti itu ada satu hal yang menurut saya kadang menjadi masalah atau mungkin diremehkan, django migrations bisa dibilang tidak peduli seberapa banyak berkas migration yang dipunya dia mampu untuk menjalankan puluhan atau ratusan berkas migrations, tapi bagi saya kadang hal yang dihasilkan menjadi tidak efisien dan bisa bepengaruh kepada konteks performa terlebih saat test.
Contoh nyata adalah seperti ini, misal kita membuat field baru untuk menyelesaikan sebuah masalah
class GuestBook(models.Model):
...
new_field = models.BooleanField(default=False)
...
ini akan diterjemahkan jadi berkas `0002_auto...` yang sql nya seperti berikut
BEGIN;
--
-- Add field new_field to guestbook
--
ALTER TABLE "guestbook_guestbook" ADD COLUMN "new_field" boolean DEFAULT false NOT NULL;
ALTER TABLE "guestbook_guestbook" ALTER COLUMN "new_field" DROP DEFAULT;
COMMIT;
lalu di tengah jalan ternyata merasa butuh satu field lagi lalu bikin satu field
otherfield = models.BooleanField(default=False)
Ini sama dengan :
BEGIN;
--
-- Add field otherfield to guestbook
--
ALTER TABLE "guestbook_guestbook" ADD COLUMN "otherfield" boolean DEFAULT false NOT NULL;
ALTER TABLE "guestbook_guestbook" ALTER COLUMN "otherfield" DROP DEFAULT;
COMMIT;
Lalu menjelang akhir merasa gak perlu field terakhir, lalu dihapus lah si other field
BEGIN;
--
-- Remove field otherfield from guestbook
--
ALTER TABLE "guestbook_guestbook" DROP COLUMN "otherfield" CASCADE;
COMMIT;
Dari sini saya merasa ini mejadi permasalahan, jika kita anggap model sebagai blueprint dari sebuah tabel maka ini tidak sesuai, di model kita seolah-olah nambah 1 field yaitu "new_field" tapi saat migration berjalan yang dilakukan:
- jalankan migration 0002 create field
- jalankan migration 0003 create field- jalankan migration 0004 remove field
Applying guestbook.0002_new_field... OK (0.023s) Applying guestbook.0003_guestbook_otherfield... OK (0.010s) Applying guestbook.0004_remove_guestbook_otherfield... OK (0.012s)
Padahal kan kita hanya butuh 1 field aja di akhir, untuk menyelesaikan issue ini kita bisa menggunakan 2 cara, reset migration atau squash.
Squash
Squash adalah menggabungkan perintah migration mejadi satu, caranya dengan:
./manage.py squashmigrations guestbook 0002 0004
Will squash the following migrations:
- 0002_new_field
- 0003_guestbook_otherfield
- 0004_remove_guestbook_otherfield
Do you wish to proceed? [yN]
Saat kita jalankan perintah migrations lagi hasilnya hanya satu yang dijalankan
Applying guestbook.0002_new_field_squashed_0004_remove_guestbook_otherfield... OK (0.029s)
Squash itu:
- akan medeteksi migration awal dan tujuannya
- akan membuat satu berkas baru migrations
- akan ignore migrations yang didaftarkan- tetap input informasi migrations ke table django_migrations
Terlihat kan perintah pertama menghasilkan 3 perintah dengan total waktu 0.045s dan satu hanya 0.029s, terlihat tidak signifikan tapi ketika bekerja dengan banyak app bisa lumayan menganggu apalagi ketika menjalankan `manage.py test` di CI/CD, karena saat manage.py test
django akan migrate dari awal db.
Setiap kali kita melakukan migrations, berkas migration yang dieksekusi akna dicatat di tabel django_migrations
Reset migrations
Cara kedua yang bisa dilakukan adalah reset migrations, tapi hati-hati dengan ini karena ini artinya revert sql yang jika kalian udah data bisa hilang. Pertama yang kita lakukan adalah ./manage.py migrate appname start
Contoh, jika kita memiliki berkas 0001, 0002, ... dan ingin reset dan memulai ulang dari 0002 maka perintahnya:
./manage.py migrate appname 0001
Running migrations:
Rendering model states... DONE
Unapplying guestbook.0004_remove_guestbook_otherfield... OK
Unapplying guestbook.0003_guestbook_otherfield... OK
Unapplying guestbook.0002_new_field... OK
Setelah berhasil hapus saja berkas yang tadi dan jalankan kembali perintah makemigration
./manage.py makemigrations guestbook
Migrations for 'guestbook':
guestbook/migrations/0002_guestbook_new_field.py
- Add field new_field to guestbook
jalankan kembali migrate seperti di awal.
(My) Rule of Thumb:
Kapan saat menggunakan Reset dan squash? Saya sendiri punya aturan tersendiri:
- Gunakan RESET JIKA DAN HANYA JIKA DI LOKAL karena di lokal development saja, kehilangan data dan sebagaianya karena reset karena hapus kolom dan sebagainya tidak terlalu bermasalah.
- Gunakan squash jika sudah terlanjur di serve staging atau development, "sedikit" kotor karena berulang kali melakukan migration tak masalah tapi ketika sebelum masuk prod diharapkan jalan perintah squash dahulu agar di prod cukup 1 perintah saja.
referensi: