Skip to main content

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:

  1. Validates all fields (annotations + custom validation)
  2. Fires beforeSave() hooks
  3. Writes to database
  4. Updates indexes
  5. 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:

  1. Fires beforeDelete() hooks
  2. Removes from database
  3. Updates indexes
  4. 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());
}
}
}