Mendapatkan ID di Antara Query dalam Satu Transaksi SQLAlchemy
Daftar Isi
- Studi Kasus: Sistem Accounting Double Entry
- Kenapa Transaksi Database Itu Penting?
- Challenge: Mendapatkan ID di Antara Query
- Implementasi dengan SQLAlchemy
- Best Practices
- Kesimpulan
Pernahkah kamu mengalami situasi di mana kamu harus mengupdate lebih dari satu table, tapi semua operasi tersebut harus dianggap sebagai satu kesatuan? Proses nya wajib sukses semua, atau gagal (rollback) semua. Tidak boleh ada kondisi di mana di satu table sukses diupdate, tapi di table yang lain gagal diupdate.
Studi Kasus: Sistem Accounting Double Entry
Mari kita ambil contoh nyata dari sistem accounting yang menggunakan double entry. Bayangkan kamu sedang membangun aplikasi keuangan di mana setiap transaksi harus tercatat dalam dua sisi: debit dan kredit. Aturan dasar accounting adalah: total debit harus sama dengan total kredit.
Misalnya, ketika perusahaan membeli peralatan senilai Rp 10.000.000:
- Debit: Akun Peralatan (+Rp 10.000.000)
- Kredit: Akun Kas (-Rp 10.000.000)
Bayangkan jika insert pertama (debit) berhasil, tapi insert kedua (kredit) gagal karena error database atau masalah jaringan. Laporan keuangan akan menjadi tidak balance dan data menjadi tidak konsisten!
Kenapa Transaksi Database Itu Penting?
Transaksi database memberikan jaminan ACID:
- Atomicity: Semua operasi berhasil atau semua gagal
- Consistency: Data tetap konsisten sesuai aturan bisnis
- Isolation: Transaksi tidak saling mengganggu
- Durability: Perubahan yang sudah commit bersifat permanen
Tanpa transaksi, aplikasi kamu rentan terhadap data corruption dan inkonsistensi.
Challenge: Mendapatkan ID di Antara Query
Nah, sekarang ada tantangan menarik: bagaimana kalau kita perlu mendapatkan ID dari insert pertama untuk digunakan di insert berikutnya, semua dalam satu transaksi?
Contoh kasus:
- Insert data
transaction_header(dapat ID transaksi) - Insert data
transaction_detailsmenggunakan ID dari langkah 1 - Insert data
journal_entriesmenggunakan ID yang sama - Jika salah satu gagal, semua harus di-rollback
Implementasi dengan SQLAlchemy
Mari kita lihat bagaimana menyelesaikan masalah ini dengan SQLAlchemy!
Setup Model
Pertama, kita definisikan model database kita:
from sqlalchemy import Column, Integer, String, Numeric, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
Base = declarative_base()
class TransactionHeader(Base):
__tablename__ = 'transaction_headers'
id = Column(Integer, primary_key=True, autoincrement=True)
transaction_date = Column(DateTime, default=datetime.utcnow)
description = Column(String(200))
total_amount = Column(Numeric(15, 2))
status = Column(String(20), default='pending')
details = relationship("TransactionDetail", back_populates="header")
journal_entries = relationship("JournalEntry", back_populates="transaction")
class TransactionDetail(Base):
__tablename__ = 'transaction_details'
id = Column(Integer, primary_key=True, autoincrement=True)
transaction_id = Column(Integer, ForeignKey('transaction_headers.id'))
item_name = Column(String(100))
quantity = Column(Integer)
unit_price = Column(Numeric(15, 2))
subtotal = Column(Numeric(15, 2))
header = relationship("TransactionHeader", back_populates="details")
class JournalEntry(Base):
__tablename__ = 'journal_entries'
id = Column(Integer, primary_key=True, autoincrement=True)
transaction_id = Column(Integer, ForeignKey('transaction_headers.id'))
account_code = Column(String(20))
account_name = Column(String(100))
debit = Column(Numeric(15, 2), default=0)
credit = Column(Numeric(15, 2), default=0)
transaction = relationship("TransactionHeader", back_populates="journal_entries")
Menggunakan Session Transaction
Ini adalah cara yang paling umum dan recommended:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine('postgresql://user:password@localhost/mydb')
Session = sessionmaker(bind=engine)
def create_complete_transaction(data):
session = Session()
try:
# 1. Insert header dan dapatkan ID-nya
header = TransactionHeader(
description=data['description'],
total_amount=data['total_amount']
)
session.add(header)
session.flush() # Ini penting! Flush akan generate ID tanpa commit
# Sekarang header.id sudah tersedia!
transaction_id = header.id
print(f"Transaction ID: {transaction_id}")
# 2. Insert detail items menggunakan transaction_id
for item in data['items']:
detail = TransactionDetail(
transaction_id=transaction_id,
item_name=item['name'],
quantity=item['quantity'],
unit_price=item['price'],
subtotal=item['quantity'] * item['price']
)
session.add(detail)
# 3. Insert journal entries (double entry)
# Debit: Inventory/Asset
journal_debit = JournalEntry(
transaction_id=transaction_id,
account_code='1100',
account_name='Inventory',
debit=data['total_amount'],
credit=0
)
session.add(journal_debit)
# Credit: Cash
journal_credit = JournalEntry(
transaction_id=transaction_id,
account_code='1000',
account_name='Cash',
debit=0,
credit=data['total_amount']
)
session.add(journal_credit)
# Validasi double entry
total_debit = data['total_amount']
total_credit = data['total_amount']
if total_debit != total_credit:
raise ValueError("Debit and Credit must be equal!")
# Commit semua perubahan
session.commit()
print("Transaction completed successfully!")
return transaction_id
except Exception as e:
# Jika ada error, rollback semua perubahan
session.rollback()
print(f"Transaction failed: {str(e)}")
raise
finally:
session.close()
# Contoh penggunaan
transaction_data = {
'description': 'Purchase Office Equipment',
'total_amount': 10000000,
'items': [
{'name': 'Laptop', 'quantity': 2, 'price': 4000000},
{'name': 'Monitor', 'quantity': 4, 'price': 500000}
]
}
try:
txn_id = create_complete_transaction(transaction_data)
print(f"Created transaction with ID: {txn_id}")
except Exception as e:
print(f"Failed to create transaction: {e}")
Kunci Penting: flush() vs commit()
Perbedaan antara flush() dan commit() sangat penting:
- flush(): Mengirim SQL ke database dan generate ID, tapi belum commit. Masih dalam transaksi yang sama dan bisa di-rollback.
- commit(): Menyimpan semua perubahan secara permanen ke database.
Dengan flush(), kamu bisa mendapatkan header.id yang sudah di-generate oleh database, tapi transaksi masih terbuka. Jika ada error di langkah selanjutnya, semua bisa di-rollback.
Menggunakan Context Manager
Cara yang lebih clean dengan context manager:
from contextlib import contextmanager
@contextmanager
def transaction_scope():
session = Session()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
# Penggunaan
def create_transaction_v2(data):
with transaction_scope() as session:
header = TransactionHeader(
description=data['description'],
total_amount=data['total_amount']
)
session.add(header)
session.flush()
# Gunakan header.id untuk operasi selanjutnya
for item in data['items']:
detail = TransactionDetail(
transaction_id=header.id,
item_name=item['name'],
quantity=item['quantity'],
unit_price=item['price'],
subtotal=item['quantity'] * item['price']
)
session.add(detail)
# Auto commit jika tidak ada error
# Auto rollback jika ada exception
return header.id
Best Practices
-
Selalu gunakan transaksi untuk operasi multi-table: Jangan pernah insert ke multiple tables tanpa transaksi.
-
Gunakan flush() untuk mendapatkan ID: Jangan commit di tengah-tengah proses jika masih ada operasi lain.
-
Validasi sebelum commit: Pastikan semua business rules terpenuhi sebelum commit.
-
Handle exception dengan proper: Selalu tangkap exception dan lakukan rollback.
-
Gunakan context manager: Lebih clean dan otomatis handle commit/rollback.
-
Test rollback scenario: Pastikan rollback bekerja dengan baik saat terjadi error.
Kesimpulan
Mengelola transaksi database dengan benar adalah skill fundamental yang wajib dikuasai developer. Dengan SQLAlchemy, kamu bisa dengan mudah:
- Mengelola transaksi multi-table
- Mendapatkan ID dari insert pertama untuk digunakan di query berikutnya
- Memastikan data consistency dengan ACID properties
Ingat: gunakan flush() untuk mendapatkan ID dalam transaksi yang sama, dan pastikan semua operasi berhasil sebelum melakukan commit().
Happy coding! 🚀
Artikel Terkait:
- Database Design Fundamentals — Panduan Lengkap Desain Database
- Best Practice API Development — Membangun API yang Secure & Scalable
- Node.js Best Practices — Kuasai Pengembangan Backend
- Microservices Architecture — Panduan Membangun Sistem Terdistribusi
- Testing Strategies for Web Apps — Panduan Testing yang Komprehensif