Skip to main content

Advanced Topics

This guide covers advanced concepts in DB plugin including the State architecture, modifications pattern, atomic operations, indexing strategies, and performance optimization techniques.

State vs. Record Architecture

DB plugin separates data storage (State) from business logic (Record) to enable powerful composition and flexibility.

State - The Data Container

State is a flexible map-like container that holds an object's data:

State state = article.getState();

// Access fields by name
String title = (String) state.get("title");
Date publishDate = (Date) state.get("publishDate");

// Path-based access for nested fields
String authorName = (String) state.getByPath("author/name");

// Set values
state.put("title", "New Title");
state.putByPath("author/name", "John Doe");

// Check if field exists
boolean hasTitle = state.containsKey("title");

// Get all field values
Map<String, Object> values = state.getSimpleValues();

State Lifecycle

// New object - not yet saved
State state = State.getInstance(Article.class);
state.setStatus(StateStatus.NEW);

// Saved to database
state.setStatus(StateStatus.SAVED);

// Marked for deletion
state.setStatus(StateStatus.DELETED);

// Read-only (cannot be modified)
state.setStatus(StateStatus.REFERENCE_ONLY);

Why Separate State from Record?

  1. Composition - Multiple object types can share the same State
  2. Flexibility - Query without loading full objects
  3. Modifications - Aspect-oriented extensions (see below)
  4. Lazy Loading - Load data on-demand
  5. Caching - Cache State independently of business logic

Modifications Pattern

Modifications enable composition-based extensions without inheritance:

Creating a Modification

@Modification.Classes({Article.class, BlogPost.class})
public class SEOData extends Modification<Recordable> {

@Indexed
private String metaTitle;

@Indexed
private String metaDescription;

private String canonicalUrl;

private List<String> keywords;

// Getters and setters
public String getMetaTitle() {
return metaTitle;
}

public void setMetaTitle(String metaTitle) {
this.metaTitle = metaTitle;
}

// ... other getters/setters

// Custom logic
public String getEffectiveMetaTitle() {
if (metaTitle != null) {
return metaTitle;
}

// Fallback to article title
Article article = as(Article.class);
return article != null ? article.getTitle() : null;
}
}

Using Modifications

// Create an article
Article article = new Article();
article.setTitle("Getting Started");
article.setContent("...");

// Access the SEO modification
SEOData seo = article.as(SEOData.class);
seo.setMetaTitle("Getting Started Guide");
seo.setMetaDescription("Learn how to get started...");
seo.setKeywords(Arrays.asList("tutorial", "guide", "introduction"));

// Save - both Article and SEOData are saved
article.save();

// Later, load the article
Article loaded = Query.from(Article.class).where("_id = ?", id).first();

// Access SEO data
SEOData loadedSeo = loaded.as(SEOData.class);
String metaTitle = loadedSeo.getMetaTitle();

Querying with Modifications

// Query by modification field
List<Article> articles = Query.from(Article.class)
.where("keywords = ?", "tutorial")
.selectAll();

Modification Lifecycle Hooks

public class AuditModification extends Modification<Recordable> {

private Date createdDate;
private User createdBy;
private Date modifiedDate;
private User modifiedBy;

@Override
protected void beforeSave() {
User currentUser = getCurrentUser();

if (getState().isNew()) {
createdDate = new Date();
createdBy = currentUser;
}

modifiedDate = new Date();
modifiedBy = currentUser;
}

// Getters and setters...
}

Cross-Cutting Concerns with Modifications

// Publishable - adds publishing workflow
@Modification.Classes({Article.class, Video.class, Image.class})
public class Publishable extends Modification<Recordable> {

@Indexed
private Date publishDate;

@Indexed
private Date expiryDate;

@Indexed
private String status; // "draft", "published", "expired"

public boolean isPublished() {
Date now = new Date();
return "published".equals(status)
&& (publishDate == null || publishDate.before(now))
&& (expiryDate == null || expiryDate.after(now));
}

public void publish() {
this.status = "published";
this.publishDate = new Date();
this.save();
}
}

// Usage
Article article = new Article();
article.setTitle("News Article");
article.as(Publishable.class).publish();

// Query published content
List<Article> published = Query.from(Article.class)
.where("publishable/status = ? and publishable/publishDate <= ?",
"published", new Date())
.selectAll();

Reference Resolution and Lazy Loading

How References Work

References are stored as lightweight stubs containing only ID and type:

// Stored in database as:
// { "_ref": "uuid", "_type": "typeId" }

// When accessed, resolved to full object
Article article = ...;
Author author = article.getAuthor(); // Lazy loaded here

Reference Resolution Diagram

Controlling Resolution

// Load without resolving references (fast)
List<Article> articles = Query.from(Article.class)
.resolveToReferenceOnly()
.selectAll();

Resolving Invisible Records

// Include invisible/deleted records when resolving references
Query.from(Article.class)
.resolveInvisible()
.selectAll();

Atomic Operations

Atomic operations allow modifying values without race conditions:

Increment and Decrement

public class Article extends Record {

@Indexed
private int viewCount;

public void incrementViewCount() {
State state = getState();
state.incrementAtomically(1);
state.save();
}

public void decrementViewCount() {
State state = getState();
state.decrementAtomically(1);
state.save();
}
}

Add to Collection

public class Article extends Record {

private Set<String> tags;

public void addTag(String tag) {
State state = getState();
state.addAtomically("tags", tag);
state.save();
}
}

Remove from Collection

public void removeTag(String tag) {
State state = getState();
state.removeAtomically("tags", tag);
state.save();
}

Replace Value

public void updateStatusIfDraft(String newStatus) {
State state = getState();
// Only replaces if current value is same as one in database
state.replaceAtomically("status", newStatus);
state.save();
}

Why Use Atomic Operations?

// Non-atomic - race condition!
article.setViewCount(article.getViewCount() + 1);
article.save();
// Another thread could increment between read and write

// Atomic - safe for concurrent access
state.incrementAtomically("viewCount", 1);
state.save();
// Database handles the increment atomically

Validation

Annotation-Based Validation

public class Article extends Record {

@Required
@Maximum(200)
private String title;

@Required
@Minimum(100)
private String content;

@Regex("[a-z0-9-]+")
private String slug;

@Step(5) // Must be multiple of 5
private int rating;

@Minimum(0)
@Maximum(100)
private int completion;
}

Custom Validation Hooks

public class Article extends Record {

@Override
protected void onValidate() {
State state = getState();

// Validate business rules
if (publishDate != null && publishDate.before(createdDate)) {
state.addError(
state.getField("publishDate"),
new IllegalArgumentException(
"Publish date cannot be before created date"
)
);
}

// Cross-field validation
if (featured && featuredImage == null) {
state.addError(
state.getField("featuredImage"),
new IllegalArgumentException(
"Featured articles must have a featured image"
)
);
}
}
}

Field-Level Error Tracking

try {
article.save();
} catch (ValidationException e) {
Map<ObjectField, List<Throwable>> errors = e.getErrors();

for (Map.Entry<ObjectField, List<Throwable>> entry : errors.entrySet()) {
ObjectField field = entry.getKey();
List<Throwable> fieldErrors = entry.getValue();

System.out.println("Errors in " + field.getInternalName() + ":");
for (Throwable error : fieldErrors) {
System.out.println(" - " + error.getMessage());
}
}
}

Lifecycle Hooks

Complete Hook Lifecycle

Hook Examples

public class Article extends Record {

@Override
protected void afterCreate() {
// Called once when object is first created
this.createdDate = new Date();
this.status = "draft";
}

@Override
protected void beforeSave() {
// Called before validation
// Good for normalizing data
if (title != null) {
title = title.trim();
}

// Auto-generate slug
if (slug == null && title != null) {
slug = generateSlug(title);
}
}

@Override
protected void onValidate() {
// Custom validation logic
if (content != null && content.length() < 100) {
getState().addError(
getState().getField("content"),
new IllegalArgumentException("Content too short")
);
}
}

@Override
protected void afterValidate() {
// Post-validation logic
// Good for setting derived fields
wordCount = countWords(content);
}

@Override
protected void afterSave() {
// Called after successful save
// Good for side effects (cache invalidation, notifications)
notificationService.notifyFollowers(this);
searchIndex.update(this);
}

@Override
protected void beforeDelete() {
// Called before delete
System.out.println("Deleting article: " + getLabel());
}

@Override
protected void afterDelete() {
// Called after successful delete
searchIndex.remove(this.getId());
cacheService.invalidate(this.getId());
}

private String generateSlug(String title) {
return title.toLowerCase()
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-|-$", "");
}

private int countWords(String text) {
return text == null ? 0 : text.split("\\s+").length;
}
}

Cascading Operations

public class Author extends Record {

@Override
protected void beforeDelete() {
// Delete all articles by this author
List<Article> articles = Query.from(Article.class)
.where("author = ?", this)
.selectAll();

for (Article article : articles) {
article.delete();
}
}
}

Indexing Strategies

Field Indexing

public class Article extends Record {

// Simple index - enables querying and sorting
@Indexed
private String title;

// Unique index - enforces uniqueness
@Indexed(unique = true)
private String slug;

// Multiple fields - creates composite index
@Indexed
private List<Author> authors;

@Indexed
private Date publishDate;
}

Composite Indexes

// Query using composite index
// Efficient if both fields are indexed
Query.from(Article.class)
.where("author = ? and publishDate > ?", author, cutoffDate)
.selectAll();

Full-Text Indexing

public class Article extends Record {

// Full-text searchable
@Indexed
private String title;
}

// Full-text search
Query.from(Article.class)
.where("title matches ?", "search query")
.selectAll();

Embedded Object Indexing

public class Article extends Record {

// Index nested field
@Indexed
private Author author;
}

// Query by nested field
Query.from(Article.class)
.where("author/name = ?", "John Doe")
.selectAll();

Visibility Indexing

public class Article extends Record {

@Indexed
@VisibilityAware
private boolean visible;

public void hide() {
visible = false;
save();
}
}

// Default - only visible articles
Query.from(Article.class).selectAll();

// Find invisible articles
Query.from(Article.class)
.where("visible = false")
.selectAll();

Type Groups

Group related types for polymorphic queries:

@Recordable.Groups("content")
public class Article extends Record {
// ...
}

@Recordable.Groups("content")
public class Video extends Record {
// ...
}

@Recordable.Groups("content")
public class Image extends Record {
// ...
}

// Query all content types
Query<Record> allContent = Query.fromGroup("content")
.where("publishDate > ?", cutoffDate)
.selectAll();

for (Record content : allContent) {
if (content instanceof Article) {
Article article = (Article) content;
// Handle article
} else if (content instanceof Video) {
Video video = (Video) content;
// Handle video
}
}

Embedded Objects

Embedding vs. References

// Regular reference - stored separately
public class Article extends Record {
@Indexed
private Author author; // Reference to separate Author record
}

// Embedded - stored inline
public class Article extends Record {
@Embedded
private Address address; // Stored within Article
}

public class Address {
private String street;
private String city;
private String state;
private String zip;
}

When to Embed

Embed when:

  • Object is only used by this parent
  • Object is small and rarely changes
  • You always need the data together
  • Object has no independent identity

Use references when:

  • Object is shared between multiple parents
  • Object is large or frequently updated
  • Object has independent lifecycle
  • You need to query the object independently

Querying Embedded Objects

// Query by embedded field
Query.from(Article.class)
.where("address/city = ?", "New York")
.selectAll();

Working with JSON

Converting to/from JSON

// State to JSON
State state = article.getState();
Map<String, Object> simpleValues = state.getSimpleValues();
String json = ObjectUtils.toJson(simpleValues);

// JSON to State
String json = "{ \"title\": \"Test\", \"content\": \"...\" }";
Map<String, Object> values = ObjectUtils.fromJson(json);
State state = State.getInstance(Article.class);
state.setValues(values);

Performance Optimization

Batch Loading

// Bad - N+1 queries
for (UUID id : articleIds) {
Article article = Query.from(Article.class)
.where("_id = ?", id)
.first();
// Process article
}

// Good - single query
List<Article> articles = Query.from(Article.class)
.where("_id = ?", articleIds)
.selectAll();

Caching Strategies

// Use query cache
List<Article> articles = Query.from(Article.class)
.where("featured = ?", true)
.selectAll();
// Results are cached automatically

// Bypass cache for fresh data
List<Article> fresh = Query.from(Article.class)
.noCache()
.where("featured = ?", true)
.selectAll();