Django Transaction (Part 1)

Django Transaction (Part 1)
Photo by Brecht Corbeel / Unsplash

Menurut saya, tugas utama dari seorang backend engineer adalah mengelola state, dan persistent state yang paling umum adalah database. Tentu saja, sehingga mempelajari database atau tools yang berkomunikasi ke database menjadi sangat penting. Salah satu tools yang cukup sering digunakan untuk berkomunikasi adalah ORM, atau dalam tulisan ini lebih spesifik lagi yaitu Django ORM.

Konfigurasi Bawaan

Konfigurasi bawaan atau default dari Django adalah auto commit, artinya setiap satu proses model adalah satu transaction. Tapi sebelum membahas spesifik Django, mari refreshing terlebih dahulu terkait transaction.

Transaction di database ini cukup penting karena dengan transaction kita bisa cukup aman dalam melakukan sesuatu. Umumnya transaction dimulai dengan BEGIN dan diakhiri dengan COMMIT atau ROLLBACK.

BEGIN;

update users set name = 'john' where id = 1;

COMMIT;

Saat melakukan operasi di atas, sebelum commit data belum tersimpan dan kita bisa membatalkannya dengan ROLLBACK. Untuk lebih detailnya kalian bisa baca terkait transaction di masing-masing dokumentasi database. Intinya transaction membantu dalam integritas data.

Django Transaction

Dalam konteks Django, pintu masuk utama dari sebuah operasional database adalah Django ORM, sehingga sebaiknya kita perlu tahu bagaimana sebenarnya Django ORM bekerja ketika mengubah kode Python menjadi SQL.

Disclaimer: Query SQL ini adalah gambaran saja, bukan berarti terkonversi 1-1, dibuat untuk mempermudah visualisasi antara Django dan database. Database yang digunakan dalam contoh tulisan ini menggunakan PostgreSQL versi 17.

Perhatikan contoh kode di bawah ini:

user = User.objects.create(name=aries)
profile = Profile.objects.create(user=user)

Tidak ada yang aneh kan dalam kode di atas, tapi sebenarnya dua baris kode di atas menghasilkan dua transaction yang berbeda.

BEGIN;
	INSERT INTO user (name) VALUES ('aries') RETURNING id;
COMMIT;

BEGIN
	INSERT INTO profile (user_id) VALUES (1);
COMMIT;

Kenapa ini harus diperhatikan? Karena default Django adalah autocommit, maka kode di atas integritas datanya dipertaruhkan.

user = User.objects.create(name=aries) --> success
profile = Profile.objects.create(user=user) --> error

Misal terjadi skenario seperti yang saya tulis di atas, ketika hendak memproses profile terjadi kendala, entah itu databasenya tiba-tiba timeout, atau server mendadak restart, atau segala jenis interupsi lainnya. Tergantung dalam konteks kebutuhan datanya, tapi dalam kondisi di atas data user sudah tercatat di database sedangkan profile belum, dan jika ada kebutuhan bahwa user dan profile harus selalu ada 1-1, maka kondisi di atas akan mengakibatkan integritas data tidak valid.

Kita bisa melakukan verifikasi dengan cara seperti ini:
Aktifkan log PostgreSQL.

psql -U postgres -c "ALTER SYSTEM SET log_connections = on;" && psql -U postgres -c "ALTER SYSTEM SET log_disconnections = on;" && psql -U postgres -c "SELECT pg_reload_conf();"

Dan kita bisa cek log-nya:

tail -f "/Users/user/Library/Application Support/Postgres/var-17/log/"*.log

Hasilnya:

2026-02-04 08:56:31.539 WIB [55238] LOG:  statement: INSERT INTO "projects_user" ("name") VALUES ('testuser') RETURNING "projects_user"."id"
2026-02-04 08:56:31.543 WIB [55238] LOG:  duration: 5.056 ms
2026-02-04 08:56:31.544 WIB [55238] LOG:  statement: INSERT INTO "projects_profile" ("user_id", "bio") VALUES (4, 'test bio') RETURNING "projects_profile"."id"
2026-02-04 08:56:31.546 WIB [55238] LOG:  duration: 1.878 ms

Lalu bagaimana jika kita ingin agar kode kita berjalan dalam satu transaksi yang sama? Jawabannya adalah menggunakan transaction tentu saja, bisa dalam bentuk decorator atau context manager. Saya sendiri lebih menyukai context manager karena lebih terlihat.

from django.db import transaction

with transaction.atomic():
    user = User.objects.create(name="Alice")
    profile = Profile.objects.create(user=user, bio="Hello")

Kode di atas akan diterjemahkan menjadi:

-- ONE transaction wrapping FOUR operations
BEGIN;
    INSERT INTO user (name) VALUES ('Alice');
    INSERT INTO profile (user_id, bio) VALUES (1, 'Hello');
COMMIT;

Dan log-nya:

2026-02-04 08:56:51.717 WIB [55238] LOG:  statement: BEGIN
2026-02-04 08:56:51.717 WIB [55238] LOG:  duration: 0.172 ms
2026-02-04 08:56:51.718 WIB [55238] LOG:  statement: INSERT INTO "projects_user" ("name") VALUES ('testuser') RETURNING "projects_user"."id"
2026-02-04 08:56:51.718 WIB [55238] LOG:  duration: 0.471 ms
2026-02-04 08:56:51.719 WIB [55238] LOG:  statement: INSERT INTO "projects_profile" ("user_id", "bio") VALUES (5, 'test bio') RETURNING "projects_profile"."id"
2026-02-04 08:56:51.719 WIB [55238] LOG:  duration: 0.229 ms
2026-02-04 08:56:51.720 WIB [55238] LOG:  statement: COMMIT

Masih ada lanjutannya mengenai savepoint, rollback, dan nested transaction.

Read more