Database Operations
The Database interface provides a comprehensive set of methods for interacting with your data store. This guide covers all read and write operations, index management, and database utilities.
Getting a Database Instance
Default Database
Database db = Database.Static.getDefault();
Named Database
Database searchDb = Database.Static.getInstance("search");
All Databases
List<Database> allDatabases = Database.Static.getAll();
By Type
List<SqlDatabase> sqlDatabases = Database.Static.getByClass(SqlDatabase.class);
Temporary Override
Temporarily override the default database for the current thread:
Database specialDb = Database.Static.getInstance("special");
Database.Static.overrideDefault(specialDb);
try {
// All operations in this block use specialDb
article.save();
Query.from(Article.class).selectAll();
} finally {
Database.Static.restoreDefault();
}
Read Operations
readAll
Reads all objects matching a query:
Query<Article> query = Query.from(Article.class)
.where("publishDate > ?", cutoffDate);
List<Article> articles = db.readAll(query);
readFirst
Reads the first object matching a query:
Article article = db.readFirst(
Query.from(Article.class)
.where("title = ?", "Getting Started")
);
if (article == null) {
// No matching article found
}
readCount
Returns the count of objects matching a query:
long totalArticles = db.readCount(
Query.from(Article.class)
);
long publishedCount = db.readCount(
Query.from(Article.class)
.where("publishDate != missing")
);
readPartial
Reads a subset of results with offset and limit (pagination):
// Page 1: items 0-9
List<Article> page1 = db.readPartial(
Query.from(Article.class).sortDescending("publishDate"),
0, // offset
10 // limit
);
// Page 2: items 10-19
List<Article> page2 = db.readPartial(
Query.from(Article.class).sortDescending("publishDate"),
10, // offset
10 // limit
);
readIterable
Returns an iterable for streaming large result sets without loading everything into memory:
Query<Article> query = Query.from(Article.class);
Iterable<Article> articles = db.readIterable(query, 100); // Fetch size
for (Article article : articles) {
// Process each article
System.out.println(article.getTitle());
// Memory is managed efficiently - old items can be garbage collected
}
This is useful for:
- Processing large datasets
- Export operations
- Batch processing
- Reducing memory footprint
readAllGrouped
Reads grouped results with aggregations:
// Group articles by author
List<Grouping<Article>> groupings = db.readAllGrouped(
Query.from(Article.class),
"author"
);
for (Grouping<Article> grouping : groupings) {
Author author = (Author) grouping.getKeys().get(0);
long count = grouping.getCount();
System.out.println(author.getName() + ": " + count + " articles");
}
// Group by multiple fields
List<Grouping<Article>> groupings = db.readAllGrouped(
Query.from(Article.class),
"author",
"publishDate.year" // Group by year
);
for (Grouping<Article> grouping : groupings) {
Author author = (Author) grouping.getKeys().get(0);
Integer year = (Integer) grouping.getKeys().get(1);
long count = grouping.getCount();
System.out.println(author.getName() + " in " + year + ": " + count);
}
readPartialGrouped
Paginated version of readAllGrouped:
List<Grouping<Article>> page = db.readPartialGrouped(
Query.from(Article.class),
0, // offset
10, // limit
"author"
);
readLastUpdate
Returns the most recent update time for objects matching a query:
Date lastUpdate = db.readLastUpdate(
Query.from(Article.class)
);
if (lastUpdate != null) {
System.out.println("Last article update: " + lastUpdate);
}
Useful for:
- Cache invalidation
- Change detection
- Synchronization checks
Write Operations
save
Saves an object with full validation:
Article article = new Article();
article.setTitle("New Article");
article.setAuthor(author);
article.setPublishDate(new Date());
try {
db.save(article.getState());
// or simply: article.save();
} catch (ValidationException e) {
// Handle validation errors
for (Map.Entry<ObjectField, List<Throwable>> entry : e.getErrors().entrySet()) {
System.out.println("Error in " + entry.getKey().getInternalName());
}
}
The save process:
- Validates all fields (annotations + custom validation)
- Fires
beforeSave()hooks - Writes to database
- Updates indexes
- Fires
afterSave()hooks
saveUnsafely
Saves without validation (use with caution):
// Skip validation for performance-critical paths
db.saveUnsafely(article.getState());
When to use:
- Importing trusted data
- System-level operations
- Performance-critical batch operations where you've pre-validated
When NOT to use:
- User input
- External data sources
- When data integrity is critical
delete
Deletes an object:
db.delete(article.getState());
// or: article.delete();
The delete process:
- Fires
beforeDelete()hooks - Removes from database
- Updates indexes
- Fires
afterDelete()hooks
deleteByQuery
Deletes all objects matching a query:
// Delete old articles
Date cutoff = DateUtils.addMonths(new Date(), -12);
db.deleteByQuery(
Query.from(Article.class)
.where("publishDate < ?", cutoff)
);
// Returns the number of deleted objects
long deletedCount = db.deleteByQuery(query);
System.out.println("Deleted " + deletedCount + " articles");
Warning: This bypasses lifecycle hooks for performance. If you need hooks to fire, load and delete objects individually:
List<Article> articles = Query.from(Article.class)
.where("publishDate < ?", cutoff)
.selectAll();
db.beginWrites();
try {
for (Article article : articles) {
article.delete(); // Fires hooks
}
db.commitWrites();
} finally {
db.endWrites();
}
Index Operations
index
Updates indexes for a specific object:
// Re-index after making changes that affect queries
article.setTitle("Updated Title");
db.index(article.getState());
Typically you don't need to call this directly as save() handles indexing. Use it when:
- Manually updating index data
- Recovering from index corruption
- Implementing custom indexing strategies
indexAll
Re-indexes all objects for a specific index:
ObjectType articleType = ObjectType.getInstance(Article.class);
ObjectIndex titleIndex = articleType.getIndex("title");
// Re-index all articles for the title index
db.indexAll(titleIndex);
Use cases:
- Adding a new index to existing data
- Recovering from index corruption
- Changing index definitions
recalculate
Recalculates specific indexes for an object:
ObjectType type = ObjectType.getInstance(Article.class);
ObjectIndex authorIndex = type.getIndex("author");
ObjectIndex dateIndex = type.getIndex("publishDate");
// Recalculate only these indexes
db.recalculate(article.getState(), authorIndex, dateIndex);
More efficient than full re-indexing when you know exactly which indexes need updates.
Batch Operations
Batch Reads
Read multiple objects by ID efficiently:
List<UUID> articleIds = Arrays.asList(id1, id2, id3, id4, id5);
List<Article> articles = Query.from(Article.class)
.where("_id = ?", articleIds)
.selectAll();
Batch Writes
Write multiple objects in a single transaction:
db.beginWrites();
try {
for (Article article : articles) {
db.save(article.getState());
}
db.commitWrites(); // All saved atomically
} finally {
db.endWrites();
}
Benefits:
- Atomic (all succeed or all fail)
- Better performance
- Validation happens before any writes
- Single database roundtrip
Database Utilities
ping
Check if the database is available:
// Ping replica/read connection
boolean isAvailable = db.ping(false);
// Ping primary/write connection
boolean primaryAvailable = db.ping(true);
if (!isAvailable) {
// Handle database unavailability
throw new ServiceUnavailableException("Database is down");
}
Use in:
- Health checks
- Monitoring
- Graceful degradation logic
now
Get the database server's current time:
Date dbTime = db.now();
Why use this instead of new Date()?
- Consistent timestamps across distributed systems
- Avoids clock skew between app servers
- Some databases have special time functions
// Example: Set timestamp from database
article.setPublishDate(db.now());
article.save();
getName / setName
String name = db.getName();
db.setName("newName");
Database names are used to:
- Identify databases in multi-database configurations
- Route queries to specific databases
- Configure database-specific settings
Environment (Deprecated)
Note: getEnvironment() and setEnvironment() are deprecated. Use DatabaseEnvironment.getCurrent() and DatabaseEnvironment.override() instead.
Notifiers (Update Listeners)
Register callbacks for object changes:
Update Notifiers
// Listen for article updates
db.addUpdateNotifier(new UpdateNotifier<Article>() {
@Override
public void onUpdate(UpdateNotification<Article> notification) {
List<Article> updated = notification.getObjects();
System.out.println("Articles updated: " + updated.size());
for (Article article : updated) {
// Handle update - invalidate cache, send notifications, etc.
cacheService.invalidate(article.getId());
}
}
});
Delete Notifiers
// Listen for article deletions
db.addDeleteNotifier(new DeleteNotifier<Article>() {
@Override
public void onDelete(DeleteNotification<Article> notification) {
List<Article> deleted = notification.getObjects();
for (Article article : deleted) {
// Handle deletion
cacheService.remove(article.getId());
searchIndex.remove(article.getId());
}
}
});
Removing Notifiers
UpdateNotifier<Article> notifier = new UpdateNotifier<Article>() { ... };
db.addUpdateNotifier(notifier);
// Later, remove it
db.removeUpdateNotifier(notifier);
Check Notifier Support
if (db.canCallNotifiers()) {
// This database supports notifiers
db.addUpdateNotifier(notifier);
} else {
// Use polling or other mechanisms
}
Note: Not all database implementations support notifiers. AbstractSqlDatabase typically does, but some specialized databases may not.
Practical Examples
Bulk Import
public void importArticles(List<ArticleData> data) {
Database db = Database.Static.getDefault();
db.beginWrites();
try {
for (ArticleData articleData : data) {
Article article = new Article();
article.setTitle(articleData.getTitle());
article.setContent(articleData.getContent());
// ... set other fields
db.save(article.getState());
}
db.commitWrites();
System.out.println("Imported " + data.size() + " articles");
} catch (Exception e) {
// Transaction will be rolled back
System.err.println("Import failed: " + e.getMessage());
throw e;
} finally {
db.endWrites();
}
}
Soft Delete Pattern
public class Article extends Record {
@Indexed(visibility = true)
private Date deletedDate;
public boolean isDeleted() {
return deletedDate != null;
}
public void softDelete() {
this.deletedDate = Database.Static.getDefault().now();
this.save();
}
public void restore() {
this.deletedDate = null;
this.save();
}
}
// Query hides articles with populated visibility indexes
List<Article> activeArticles = Query.from(Article.class)
.selectAll();
Streaming Export
public void exportToCSV(Writer writer) throws IOException {
Database db = Database.Static.getDefault();
CSVWriter csv = new CSVWriter(writer);
// Write header
csv.writeNext(new String[]{"ID", "Title", "Author", "Date"});
// Stream results
Query<Article> query = Query.from(Article.class)
.sortDescending("publishDate");
for (Article article : db.readIterable(query, 100)) {
csv.writeNext(new String[]{
article.getId().toString(),
article.getTitle(),
article.getAuthor().getName(),
article.getPublishDate().toString()
});
}
csv.close();
}
Cascading Delete
public class Author extends Record {
@Override
protected void beforeDelete() {
Database db = Database.Static.getDefault();
// Delete all articles by this author
db.deleteByQuery(
Query.from(Article.class)
.where("author = ?", this)
);
}
}
Cache Invalidation with Notifiers
public class CacheInvalidator {
private final Cache<UUID, Article> cache;
public CacheInvalidator(Database db, Cache<UUID, Article> cache) {
this.cache = cache;
// Invalidate cache on updates
db.addUpdateNotifier(new UpdateNotifier<Article>() {
@Override
public void onUpdate(UpdateNotification<Article> notification) {
for (Article article : notification.getObjects()) {
cache.remove(article.getId());
}
}
});
// Invalidate cache on deletes
db.addDeleteNotifier(new DeleteNotifier<Article>() {
@Override
public void onDelete(DeleteNotification<Article> notification) {
for (Article article : notification.getObjects()) {
cache.remove(article.getId());
}
}
});
}
public Article get(UUID id) {
return cache.get(id, key -> {
return Database.Static.getDefault().readFirst(
Query.from(Article.class).where("_id = ?", key)
);
});
}
}
Performance Considerations
Use Batch Operations
// Good - single transaction
db.beginWrites();
for (Article article : articles) {
db.save(article.getState());
}
db.commitWrites();
// Bad - multiple transactions
for (Article article : articles) {
db.save(article.getState());
}
Use readIterable for Large Datasets
// Good - memory efficient
for (Article article : db.readIterable(query, 100)) {
process(article);
}
// Bad - loads everything into memory
List<Article> all = db.readAll(query);
for (Article article : all) {
process(article);
}
Use readCount Instead of Loading All
// Good
long count = db.readCount(query);
// Bad
long count = db.readAll(query).size();
Use Specific Queries
// Good - only loads needed objects
List<Article> recent = db.readPartial(
Query.from(Article.class)
.where("publishDate > ?", cutoff)
.sortDescending("publishDate"),
0,
10
);
// Bad - loads everything then filters
List<Article> all = db.readAll(Query.from(Article.class));
List<Article> recent = all.stream()
.filter(a -> a.getPublishDate().after(cutoff))
.sorted(...)
.limit(10)
.collect(Collectors.toList());
Error Handling
ValidationException
try {
article.save();
} catch (ValidationException e) {
Map<ObjectField, List<Throwable>> errors = e.getErrors();
for (Map.Entry<ObjectField, List<Throwable>> entry : errors.entrySet()) {
String fieldName = entry.getKey().getInternalName();
for (Throwable error : entry.getValue()) {
logger.error("Validation error in {}: {}", fieldName, error.getMessage());
}
}
}