Transaction Demos
These demos show how to group operations into atomic transactions.
Basic Transaction
Group multiple operations that succeed or fail together.
from sqliter import SqliterDB
from sqliter.model import BaseDBModel
class Account(BaseDBModel):
name: str
balance: float
db = SqliterDB(memory=True)
db.create_table(Account)
alice: Account = db.insert(Account(name="Alice", balance=100.0))
bob: Account = db.insert(Account(name="Bob", balance=50.0))
print(f"Before: Alice=${alice.balance}, Bob=${bob.balance}")
# Transfer money using context manager
with db:
alice.balance = alice.balance - 20.0
bob.balance = bob.balance + 20.0
db.update(alice)
db.update(bob)
alice_updated = alice
bob_updated = bob
print(
f"After: Alice=${alice_updated.balance}, Bob=${bob_updated.balance}"
)
print("Transaction auto-committed on success")
db.close()
What Happens
- Both updates succeed or both fail
- If an error occurs, changes are rolled back
- Database remains in a consistent state
Transaction Rollback
Automatically rollback on errors.
from sqliter import SqliterDB
from sqliter.model import BaseDBModel
class Item(BaseDBModel):
name: str
quantity: int
db = SqliterDB(memory=True)
db.create_table(Item)
item: Item = db.insert(Item(name="Widget", quantity=10))
print(f"Initial quantity: {item.quantity}")
# Use context manager for automatic rollback on error
try:
with db:
item.quantity = 5
db.update(item)
print("Inside transaction: updated to 5")
# If error occurs, changes are rolled back
error_msg = "Intentional error for rollback"
raise RuntimeError(error_msg) # noqa: TRY301
except RuntimeError:
print("Error occurred - transaction rolled back")
print("Original value preserved (quantity=10)")
db.close()
Rollback Behavior
- All changes within the transaction should be undone
- Database state should be as if nothing happened
- Exception continues to propagate unless caught
Manual Transaction Control
Explicitly commit using the context manager or connect/commit methods.
from sqliter import SqliterDB
from sqliter.model import BaseDBModel
class Log(BaseDBModel):
message: str
db = SqliterDB(memory=True)
db.create_table(Log)
# Manual transaction control
db.connect()
log1 = db.insert(Log(message="First entry"))
print(f"Inserted: {log1.message}")
print("Not committed yet")
db.commit()
print("Committed")
db.insert(Log(message="Second entry"))
db.commit()
all_logs = db.select(Log).fetch_all()
print(f"Total logs: {len(all_logs)}")
db.close()
When to Use
- Complex logic: Need conditional commit/rollback
- Error handling: Different rollback strategies
- Nested operations: Multiple validation steps
Transaction Isolation
Transactions are isolated from other operations.
from sqliter import SqliterDB
from sqliter.model import BaseDBModel
class Counter(BaseDBModel):
value: int
db = SqliterDB(memory=True)
db.create_table(Counter)
counter = db.insert(Counter(value=0))
with db:
# Increment counter
counter.value += 1
db.update(counter)
# Value is 1 inside transaction
print(f"Inside: {counter.value}")
# Value is still 1 after commit
reloaded = db.get(Counter, counter.pk)
if reloaded is not None:
print(f"After commit: {reloaded.value}")
db.close()
Nested Transactions
SQLiter supports nested context manager usage.
from sqliter import SqliterDB
from sqliter.model import BaseDBModel
class Order(BaseDBModel):
total: float
class Payment(BaseDBModel):
amount: float
db = SqliterDB(memory=True)
db.create_table(Order)
db.create_table(Payment)
# Outer transaction
with db:
order = db.insert(Order(total=100.0))
# Inner context (part of same transaction)
with db:
payment = db.insert(Payment(amount=100.0))
# Both are committed together
Note
SQLite's nested contexts are part of the same transaction - the outermost context exit finalizes everything.
Performance Considerations
Bulk Operations with Transactions
from sqliter.model import BaseDBModel
class User(BaseDBModel):
name: str
# ❌ SLOW: Each insert is its own transaction
for i in range(1000):
db.insert(User(name=f"User {i}"))
# ✅ FAST: All inserts in one transaction
with db:
for i in range(1000):
db.insert(User(name=f"User {i}"))
Performance Impact
- Without transaction: 1000 separate commits = slow
- With transaction: 1 commit for all records = 10-100x faster
When to Use Transactions
Always Use For
- Related operations: Multiple tables that must stay in sync
- Financial data: Money transfers must be atomic
- Complex updates: Changes that affect multiple records
Optional For
- Single operations: One insert/update/delete
- Independent records: No relationship between operations
- Non-critical data: Temporary data, logs
Never For
- Long-running operations: Transactions lock the database
- Interactive operations: Waiting for user input
- Large bulk imports: Consider periodic commits
Common Patterns
Transfer Pattern
def transfer(db: SqliterDB, from_account: Account, to_account: Account, amount: float) -> None:
"""Transfer funds between accounts."""
with db:
from_account.balance -= amount
to_account.balance += amount
db.update(from_account)
db.update(to_account)
Create or Update Pattern
from sqliter.model import BaseDBModel
class User(BaseDBModel):
name: str
email: str
def create_or_update(db: SqliterDB, user: User) -> None:
"""Insert or update a user."""
with db:
existing = db.select(User).filter(
email__eq=user.email
).fetch_one()
if existing:
existing.name = user.name
db.update(existing)
else:
db.insert(user)
Best Practices
DO
- Use transactions for multistep operations
- Keep transactions short (don't hold locks)
- Use context managers for automatic cleanup
- Handle exceptions appropriately
DON'T
- Forget that transactions lock the database
- Put user input inside transactions
- Use transactions for single operations (unnecessary overhead)
- Mix manual and automatic transaction control
Related Documentation
- CRUD Operations - Create, update, and delete records
- Error Handling - Handle transaction errors
- ORM Features - Work with related data in transactions