Transactions and Write Management
DB plugin provides a robust transaction system for managing write operations. Transactions ensure data consistency, enable batch operations, and provide validation before any data reaches the database.
Transaction Lifecycle
Basic Transaction Pattern
beginWrites / commitWrites / endWrites
Database db = Database.Static.getDefault();
db.beginWrites();
try {
// Make changes
article1.save();
article2.save();
author.save();
// Commit changes
db.commitWrites();
} finally {
// Always clean up
db.endWrites();
}
What Happens
- beginWrites() - Starts a transaction, increments depth counter
- save() - Buffers the write, doesn't touch the database yet
- commitWrites() - Validates all buffered writes, then writes them atomically
- endWrites() - Decrements depth counter, cleans up resources
Why Use Transactions
// Without transaction - NOT atomic
article.save(); // Might succeed
author.save(); // Might fail
// Now you have inconsistent data!
// With transaction - atomic
db.beginWrites();
try {
article.save();
author.save();
db.commitWrites(); // Both succeed or both fail
} finally {
db.endWrites();
}
Nested Transactions
DB plugin supports nested transactions using a depth counter:
Database db = Database.Static.getDefault();
db.beginWrites(); // Depth 1
try {
article.save();
db.beginWrites(); // Depth 2 (nested)
try {
author.save();
db.commitWrites(); // Commits only at depth 1
} finally {
db.endWrites(); // Back to depth 1
}
db.commitWrites(); // Actually commits everything
} finally {
db.endWrites(); // Depth 0
}
How Nesting Works
- beginWrites() increments a counter
- endWrites() decrements it
- commitWrites() only writes when counter reaches 0
- Allows method composition without worrying about transaction state
public void saveArticleWithAuthor(Article article, Author author) {
// This method doesn't know if it's called within a transaction
db.beginWrites();
try {
article.save();
author.save();
db.commitWrites();
} finally {
db.endWrites();
}
}
// Called standalone - commits immediately
saveArticleWithAuthor(article, author);
// Called within transaction - commits with outer transaction
db.beginWrites();
try {
saveArticleWithAuthor(article1, author1);
saveArticleWithAuthor(article2, author2);
db.commitWrites(); // All 4 objects committed together
} finally {
db.endWrites();
}
Commit Strategies
commitWrites - Immediate Consistency
Commits changes immediately with strong consistency guarantees:
db.beginWrites();
try {
article.save();
db.commitWrites();
// Data is immediately visible to other transactions
} finally {
db.endWrites();
}
Use when:
- You need immediate read-after-write consistency
- Other processes depend on this data immediately
- Working with critical data (financial, inventory, etc.)
commitWritesEventually - Eventual Consistency
Commits changes for eventual consistency, allowing for optimization:
db.beginWrites();
try {
article.save();
db.commitWritesEventually();
// Data will be visible soon, but maybe not immediately
} finally {
db.endWrites();
}
Use when:
- Eventual consistency is acceptable
- Performance is critical
- Working with caching or replication systems
- Bulk imports or batch operations
The database implementation may:
- Batch writes together
- Delay replication
- Optimize index updates
- Trade immediate consistency for throughput
Isolated Writes
Create an isolated write session that doesn't interact with the main transaction:
// Main transaction
db.beginWrites();
try {
article.save();
// Isolated session
db.beginIsolatedWrites();
try {
// This saves independently
auditLog.save();
db.commitWrites();
} finally {
db.endWrites();
}
// Main transaction continues
db.commitWrites();
} finally {
db.endWrites();
}
Use cases:
- Audit logging that should succeed even if main transaction fails
- Separate concerns (e.g., metrics, analytics)
- Writing to different databases
Write Buffering and Validation
The Validation Phase
All validation happens before any database writes:
db.beginWrites();
try {
// These are buffered
article1.save();
article2.save();
article3.save();
// This triggers validation of ALL three
db.commitWrites();
// If any validation fails, NONE are written
} catch (ValidationException e) {
// Handle validation errors for all objects
Map<Object, Map<ObjectField, List<Throwable>>> errors = e.getAllErrors();
} finally {
db.endWrites();
}
Validation Order
- Annotation validation -
@Required,@Maximum, etc. - beforeSave() hooks - Custom pre-save logic
- onValidate() hooks - Custom validation logic
- afterValidate() hooks - Post-validation logic
- Database write - Only if all validation passes
- afterSave() hooks - Post-save logic
Example: Custom Validation
public class Article extends Record {
@Required
private String title;
@Required
private String content;
@Override
protected void beforeSave() {
// Normalize data before validation
if (title != null) {
title = title.trim();
}
}
@Override
protected void onValidate() {
// Custom validation logic
if (content != null && content.length() < 100) {
getState().addError(
getState().getField("content"),
new IllegalArgumentException("Content must be at least 100 characters")
);
}
if (title != null && title.toLowerCase().contains("spam")) {
getState().addError(
getState().getField("title"),
new IllegalArgumentException("Title contains forbidden words")
);
}
}
@Override
protected void afterSave() {
// Post-save logic (cache invalidation, notifications, etc.)
System.out.println("Article saved: " + getLabel());
}
}
Error Handling
Handling ValidationException
db.beginWrites();
try {
article.save();
db.commitWrites();
} catch (ValidationException e) {
// Get all validation errors
Map<Object, Map<ObjectField, List<Throwable>>> allErrors = e.getAllErrors();
for (Map.Entry<Object, Map<ObjectField, List<Throwable>>> entry : allErrors.entrySet()) {
Object object = entry.getKey();
Map<ObjectField, List<Throwable>> fieldErrors = entry.getValue();
System.out.println("Errors in " + object.getClass().getSimpleName() + ":");
for (Map.Entry<ObjectField, List<Throwable>> fieldEntry : fieldErrors.entrySet()) {
String fieldName = fieldEntry.getKey().getInternalName();
for (Throwable error : fieldEntry.getValue()) {
System.out.println(" " + fieldName + ": " + error.getMessage());
}
}
}
} catch (DatabaseException e) {
// Handle database errors (connection, constraint violations, etc.)
logger.error("Database error", e);
throw e;
} finally {
db.endWrites();
}
Handling Database Errors
db.beginWrites();
try {
article.save();
db.commitWrites();
} catch (DatabaseException e) {
// Check for specific error types
if (e.getCause() instanceof SQLException) {
SQLException sqlEx = (SQLException) e.getCause();
// Unique constraint violation
if (sqlEx.getErrorCode() == 1062) { // MySQL duplicate entry
throw new DuplicateException("Article already exists", e);
}
// Deadlock
if (sqlEx.getErrorCode() == 1213) { // MySQL deadlock
// Retry logic
return retryOperation();
}
}
throw e;
} finally {
db.endWrites();
}
Automatic Retry
DB plugin automatically retries certain recoverable errors:
// This is handled automatically by AbstractDatabase
db.beginWrites();
try {
article.save();
db.commitWrites(); // Will retry on transient errors
} finally {
db.endWrites();
}
Automatically retried errors:
- Connection timeouts
- Deadlocks
- Transient network failures
- Lock wait timeouts
Uses exponential backoff with configurable max attempts.
Batch Operations
Efficient Batch Writes
public void importArticles(List<ArticleData> dataList) {
Database db = Database.Static.getDefault();
db.beginWrites();
try {
for (ArticleData data : dataList) {
Article article = new Article();
article.setTitle(data.getTitle());
article.setContent(data.getContent());
article.setAuthor(data.getAuthor());
article.save(); // Buffered, not written yet
}
// All articles validated and written in one batch
db.commitWrites();
System.out.println("Imported " + dataList.size() + " articles");
} catch (ValidationException e) {
System.err.println("Import failed due to validation errors");
// None of the articles are saved
throw e;
} finally {
db.endWrites();
}
}
Batch with Progress Tracking
public void importWithProgress(List<ArticleData> dataList) {
Database db = Database.Static.getDefault();
int batchSize = 100;
int processed = 0;
for (int i = 0; i < dataList.size(); i += batchSize) {
int end = Math.min(i + batchSize, dataList.size());
List<ArticleData> batch = dataList.subList(i, end);
db.beginWrites();
try {
for (ArticleData data : batch) {
Article article = new Article();
// ... populate article
article.save();
}
db.commitWrites();
processed += batch.size();
System.out.println("Processed " + processed + " / " + dataList.size());
} catch (Exception e) {
System.err.println("Batch failed at index " + i);
throw e;
} finally {
db.endWrites();
}
}
}
Parallel Batch Processing
public void parallelImport(List<ArticleData> dataList) throws InterruptedException {
int numThreads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
int batchSize = 100;
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < dataList.size(); i += batchSize) {
int start = i;
int end = Math.min(i + batchSize, dataList.size());
Future<?> future = executor.submit(() -> {
List<ArticleData> batch = dataList.subList(start, end);
Database db = Database.Static.getDefault();
db.beginWrites();
try {
for (ArticleData data : batch) {
Article article = new Article();
// ... populate article
article.save();
}
db.commitWrites();
} finally {
db.endWrites();
}
});
futures.add(future);
}
// Wait for all batches to complete
for (Future<?> future : futures) {
try {
future.get();
} catch (ExecutionException e) {
System.err.println("Batch failed: " + e.getCause().getMessage());
}
}
executor.shutdown();
}
Distributed Locks
For unique constraint enforcement across distributed systems:
public class Article extends Record {
@Indexed
@Unique
private String slug;
@Override
protected void beforeSave() {
// DB plugin automatically acquires a distributed lock
// for unique fields during save
}
}
Transaction Patterns
Unit of Work Pattern
public class ArticleService {
public void publishArticle(UUID articleId) {
Database db = Database.Static.getDefault();
db.beginWrites();
try {
Article article = Query.from(Article.class)
.where("_id = ?", articleId)
.first();
if (article == null) {
throw new IllegalArgumentException("Article not found");
}
// Make multiple related changes
article.setPublishDate(db.now());
article.setStatus("published");
article.save();
// Update author stats
Author author = article.getAuthor();
author.incrementPublishedCount();
author.save();
// Create activity log
ActivityLog log = new ActivityLog();
log.setType("article_published");
log.setArticle(article);
log.setTimestamp(db.now());
log.save();
// All changes committed together
db.commitWrites();
} finally {
db.endWrites();
}
}
}
Saga Pattern (Compensating Transactions)
public class OrderService {
public void placeOrder(Order order) {
Database db = Database.Static.getDefault();
boolean inventoryReserved = false;
boolean paymentProcessed = false;
try {
db.beginWrites();
// Step 1: Reserve inventory
reserveInventory(order);
inventoryReserved = true;
// Step 2: Process payment
processPayment(order);
paymentProcessed = true;
// Step 3: Create order
order.setStatus("confirmed");
order.save();
db.commitWrites();
} catch (Exception e) {
// Compensating actions
if (paymentProcessed) {
refundPayment(order);
}
if (inventoryReserved) {
releaseInventory(order);
}
throw e;
} finally {
db.endWrites();
}
}
}
Optimistic Locking
public class Article extends Record {
@Indexed
private int version;
public void updateWithOptimisticLock(String newTitle) {
Database db = Database.Static.getDefault();
db.beginWrites();
try {
// Load current version
Article current = Query.from(Article.class)
.where("_id = ?", this.getId())
.first();
if (current.getVersion() != this.version) {
throw new ConcurrentModificationException(
"Article was modified by another transaction"
);
}
// Update with new version
this.setTitle(newTitle);
this.setVersion(this.version + 1);
this.save();
db.commitWrites();
} finally {
db.endWrites();
}
}
}
Performance Optimization
Batch vs. Individual Commits
// Slow - many small transactions
for (Article article : articles) {
db.beginWrites();
try {
article.save();
db.commitWrites();
} finally {
db.endWrites();
}
}
// Fast - one large transaction
db.beginWrites();
try {
for (Article article : articles) {
article.save();
}
db.commitWrites();
} finally {
db.endWrites();
}
Eventual Consistency for Bulk Operations
// Use eventual consistency for better performance
db.beginWrites();
try {
for (Article article : articles) {
article.save();
}
db.commitWritesEventually(); // Faster than commitWrites()
} finally {
db.endWrites();
}
Skip Validation for Trusted Data
// Import from trusted source - skip validation
db.beginWrites();
try {
for (ArticleData data : trustedData) {
Article article = convertToArticle(data);
db.saveUnsafely(article.getState()); // No validation
}
db.commitWrites();
} finally {
db.endWrites();
}
Best Practices
1. Always Use try-finally
// Good
db.beginWrites();
try {
article.save();
db.commitWrites();
} finally {
db.endWrites();
}
// Bad - endWrites() might not be called
db.beginWrites();
article.save();
db.commitWrites();
db.endWrites();
2. Keep Transactions Short
// Good - short transaction
db.beginWrites();
try {
article.save();
db.commitWrites();
} finally {
db.endWrites();
}
// Bad - long transaction holding locks
db.beginWrites();
try {
article.save();
sendEmail(article); // External I/O
updateSearchIndex(article); // Slow operation
db.commitWrites();
} finally {
db.endWrites();
}
3. Use Appropriate Commit Strategy
// Critical data - immediate consistency
db.beginWrites();
try {
payment.save();
db.commitWrites(); // Must be immediately visible
} finally {
db.endWrites();
}
// Non-critical data - eventual consistency
db.beginWrites();
try {
viewCount.save();
db.commitWritesEventually(); // Performance > consistency
} finally {
db.endWrites();
}
4. Don't Catch and Ignore Exceptions
// Bad - hides errors
db.beginWrites();
try {
article.save();
db.commitWrites();
} catch (Exception e) {
// Ignored!
} finally {
db.endWrites();
}
// Good - handle or propagate
db.beginWrites();
try {
article.save();
db.commitWrites();
} catch (ValidationException e) {
logger.error("Validation failed: {}", e.getMessage());
throw e;
} finally {
db.endWrites();
}