Satu pesanan buku menyentuh lebih dari satu document: stok dikurangi, penjualan dicatat, saldo dompet pelanggan diturunkan. Bila aplikasi berhenti di tengah rangkaian itu — error jaringan, proses mati — data kita berhenti di kondisi setengah jadi tanpa penanda apa pun. Transaksi MongoDB menyelesaikan masalah ini dengan jaminan all-or-nothing: seluruh perubahan ter-commit bersama, atau tidak ada yang tersimpan sama sekali. Di PyMongo, transaksi dijalankan lewat session, dan chapter ini mempraktikkan dua polanya: with_transaction() yang praktis, serta start_transaction() + abort_transaction() untuk kendali manual.

Transaksi membutuhkan replica set atau sharded cluster — server standalone menolaknya. Server latihan kita adalah single-node replica set rs0, jadi semuanya bisa diuji langsung.

with_transaction: Commit dengan Coba-Ulang Otomatis #

Kasus klasisnya: transfer saldo 50.000 dari Budi ke Siti. Dua update_one harus terjadi sebagai satu kesatuan — dan with_transaction() mengurus mulai, commit, sampai coba-ulangnya:

python
import os

from pymongo import MongoClient

URI = os.environ.get(
    "MONGODB_URI", "mongodb://127.0.0.1:27017/?directConnection=true"
)

client = MongoClient(URI, serverSelectionTimeoutMS=5000)
client.drop_database("latihan_txn_py")
db = client["latihan_txn_py"]

db.rekening.insert_many([
    {"nama": "Budi", "saldo": 150000},
    {"nama": "Siti", "saldo": 50000},
])

def transfer(session, dari, ke, jumlah):
    """Dipanggil di dalam transaksi; session wajib diteruskan."""
    asal = db.rekening.find_one({"nama": dari}, session=session)
    if asal["saldo"] < jumlah:
        raise ValueError(f"saldo {dari} tidak cukup")
    db.rekening.update_one(
        {"nama": dari}, {"$inc": {"saldo": -jumlah}}, session=session)
    db.rekening.update_one(
        {"nama": ke}, {"$inc": {"saldo": jumlah}}, session=session)

with client.start_session() as session:
    session.with_transaction(lambda s: transfer(s, "Budi", "Siti", 50000))

print("setelah transaksi:")
for rekening in db.rekening.find({}, {"_id": 0}):
    print("  ", rekening)

client.drop_database("latihan_txn_py")
client.close()

Output yang Dihasilkan:

text
setelah transaksi:
   {'nama': 'Budi', 'saldo': 100000}
   {'nama': 'Siti', 'saldo': 100000}

Mari kita bedah baris-baris pentingnya:

  1. with client.start_session() as session: — semua transaksi hidup di dalam session; blok with memastikan session ditutup rapi setelah selesai.
  2. session.with_transaction(callback) — method andalan PyMongo. Ia membungkus tiga langkah sekaligus: start_transaction(), menjalankan callback, lalu commit_transaction(). Callback menerima session sebagai argumen pertama — di sini dijembatani lambda s: transfer(s, ...).
  3. session=session di setiap operasi — inilah syarat mutlak: operasi yang tidak menerima session berjalan di luar transaksi, diam-diam, tanpa peringatan.
  4. Guard di awal callback — find_one di dalam transaksi melihat data yang belum di-commit; bila saldo kurang, callback melempar ValueError dan with_transaction membiarkan transaksi batal — saldo kedua pihak utuh.
  5. Coba-ulang otomatis — bila jaringan tersentak di tengah jalan, server memberi label TransientTransactionError atau UnknownTransactionCommitResult pada error tertentu; with_transaction() mendeteksi label itu dan menjalankan ulang seluruh callback secara otomatis. Kita tidak menulis loop retry sendiri.

abort_transaction: Membatalkan secara Manual #

Pola kedua memberi kendali penuh: kita mulai transaksi sendiri, dan abort_transaction() mengembalikan semua document ke keadaan semula. Simulasinya: update terlanjur berjalan, lalu validasi menyadari hasilnya tidak masuk akal:

python
import os

from pymongo import MongoClient

URI = os.environ.get(
    "MONGODB_URI", "mongodb://127.0.0.1:27017/?directConnection=true"
)

client = MongoClient(URI, serverSelectionTimeoutMS=5000)
client.drop_database("latihan_txn_py")
db = client["latihan_txn_py"]

db.rekening.insert_many([
    {"nama": "Budi", "saldo": 150000},
    {"nama": "Siti", "saldo": 50000},
])

with client.start_session() as session:
    session.start_transaction()
    db.rekening.update_one(
        {"nama": "Budi"}, {"$inc": {"saldo": -200000}}, session=session)

    # Pembacaan di dalam transaksi melihat perubahan yang belum di-commit
    saldo = db.rekening.find_one({"nama": "Budi"}, session=session)["saldo"]
    print("di dalam transaksi, saldo Budi:", saldo)

    if saldo < 0:
        print("saldo minus terdeteksi — batalkan semuanya")
        session.abort_transaction()

print("setelah abort, saldo Budi:",
      db.rekening.find_one({"nama": "Budi"})["saldo"])
print("setelah abort, saldo Siti:",
      db.rekening.find_one({"nama": "Siti"})["saldo"])

client.drop_database("latihan_txn_py")
client.close()

Output yang Dihasilkan:

text
di dalam transaksi, saldo Budi: -50000
saldo minus terdeteksi — batalkan semuanya
setelah abort, saldo Budi: 150000
setelah abort, saldo Siti: 50000

Dua perilaku penting terbaca dari output itu. Pembacaan dengan session=session di dalam transaksi melihat saldo -50000 — perubahan yang belum di-commit milik session itu sendiri; pembaca lain di luar transaksi masih melihat 150000. Setelah abort_transaction(), seluruh perubahan dilepas dan saldo kembali ke nilai semula — seperti tidak pernah ada operasi yang terjadi.

Perlu dicatat pula: ValueError yang kita lempar pada guard di file pertama pada dasarnya memicu mekanisme yang sama — callback gagal, transaksi tidak pernah di-commit, dan session ditutup bersih oleh with.

Catatan Praktis #

HalPanduan
Kapan transaksihanya untuk invarian lintas document (saldo + pesanan + stok); operasi tunggal sudah atomik tanpa session
Durasijaga sesingkat mungkin — server membatasi umur transaksi (bawaan 60 detik) dan transaksi panjang menahan cache versi document
Pola coba-ulanggunakan with_transaction(); ia menangani label error transien otomatis
Abort manualpakai start_transaction() + abort_transaction() saat keputusan batal ada di tangan kode kita
Sharded clustertransaksi tetap berfungsi; hanya batas praktisnya lebih ketat

Rangkuman #

  • Transaksi menjalankan banyak operasi secara all-or-nothing; syaratnya replica set atau sharded cluster — rs0 lokal kita memenuhinya.
  • with client.start_session() as session: membuka wadah transaksi; session.with_transaction(callback) menangani start, commit, dan coba-ulang error transien otomatis.
  • Setiap operasi di dalam transaksi wajib menerima session=session — operasi tanpa session berjalan di luar transaksi tanpa peringatan.
  • Pembacaan dengan session melihat perubahan belum-commit; abort_transaction() membatalkan semuanya sekaligus.
  • Pakai transaksi hanya untuk invarian lintas document, jaga singkat, dan jangan menelan exception di callback.

Transaksi mengunci sisi penulisan. Satu bab terakhir menutup Section ini: mengenali dan menanggapi error PyMongo dengan pola yang tepat.