Skip to main content
Version: 1.1.0

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:

1
State state = article.getState();
2
3
// Access fields by name
4
String title = (String) state.get("title");
5
Date publishDate = (Date) state.get("publishDate");
6
7
// Path-based access for nested fields
8
String authorName = (String) state.getByPath("author/name");
9
10
// Set values
11
state.put("title", "New Title");
12
state.putByPath("author/name", "John Doe");
13
14
// Check if field exists
15
boolean hasTitle = state.containsKey("title");
16
17
// Get all field values
18
Map<String, Object> values = state.getSimpleValues();

State Lifecycle

1
// New object - not yet saved
2
State state = State.getInstance(Article.class);
3
state.setStatus(StateStatus.NEW);
4
5
// Saved to database
6
state.setStatus(StateStatus.SAVED);
7
8
// Marked for deletion
9
state.setStatus(StateStatus.DELETED);
10
11
// Read-only (cannot be modified)
12
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

1
@Modification.Classes({Article.class, BlogPost.class})
2
public class SEOData extends Modification<Recordable> {
3
4
@Indexed
5
private String metaTitle;
6
7
@Indexed
8
private String metaDescription;
9
10
private String canonicalUrl;
11
12
private List<String> keywords;
13
14
// Getters and setters
15
public String getMetaTitle() {
16
return metaTitle;
17
}
18
19
public void setMetaTitle(String metaTitle) {
20
this.metaTitle = metaTitle;
21
}
22
23
// ... other getters/setters
24
25
// Custom logic
26
public String getEffectiveMetaTitle() {
27
if (metaTitle != null) {
28
return metaTitle;
29
}
30
31
// Fallback to article title
32
Article article = as(Article.class);
33
return article != null ? article.getTitle() : null;
34
}
35
}

Using Modifications

1
// Create an article
2
Article article = new Article();
3
article.setTitle("Getting Started");
4
article.setContent("...");
5
6
// Access the SEO modification
7
SEOData seo = article.as(SEOData.class);
8
seo.setMetaTitle("Getting Started Guide");
9
seo.setMetaDescription("Learn how to get started...");
10
seo.setKeywords(Arrays.asList("tutorial", "guide", "introduction"));
11
12
// Save - both Article and SEOData are saved
13
article.save();
14
15
// Later, load the article
16
Article loaded = Query.from(Article.class).where("_id = ?", id).first();
17
18
// Access SEO data
19
SEOData loadedSeo = loaded.as(SEOData.class);
20
String metaTitle = loadedSeo.getMetaTitle();

Querying with Modifications

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

Modification Lifecycle Hooks

1
public class AuditModification extends Modification<Recordable> {
2
3
private Date createdDate;
4
private User createdBy;
5
private Date modifiedDate;
6
private User modifiedBy;
7
8
@Override
9
protected void beforeSave() {
10
User currentUser = getCurrentUser();
11
12
if (getState().isNew()) {
13
createdDate = new Date();
14
createdBy = currentUser;
15
}
16
17
modifiedDate = new Date();
18
modifiedBy = currentUser;
19
}
20
21
// Getters and setters...
22
}

Cross-Cutting Concerns with Modifications

1
// Publishable - adds publishing workflow
2
@Modification.Classes({Article.class, Video.class, Image.class})
3
public class Publishable extends Modification<Recordable> {
4
5
@Indexed
6
private Date publishDate;
7
8
@Indexed
9
private Date expiryDate;
10
11
@Indexed
12
private String status; // "draft", "published", "expired"
13
14
public boolean isPublished() {
15
Date now = new Date();
16
return "published".equals(status)
17
&& (publishDate == null || publishDate.before(now))
18
&& (expiryDate == null || expiryDate.after(now));
19
}
20
21
public void publish() {
22
this.status = "published";
23
this.publishDate = new Date();
24
this.save();
25
}
26
}
27
28
// Usage
29
Article article = new Article();
30
article.setTitle("News Article");
31
article.as(Publishable.class).publish();
32
33
// Query published content
34
List<Article> published = Query.from(Article.class)
35
.where("publishable/status = ? and publishable/publishDate <= ?",
36
"published", new Date())
37
.selectAll();

Reference Resolution and Lazy Loading

How References Work

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

1
// Stored in database as:
2
// { "_ref": "uuid", "_type": "typeId" }
3
4
// When accessed, resolved to full object
5
Article article = ...;
6
Author author = article.getAuthor(); // Lazy loaded here

Reference Resolution Diagram

Controlling Resolution

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

Resolving Invisible Records

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

Atomic Operations

Atomic operations allow modifying values without race conditions:

Increment and Decrement

1
public class Article extends Record {
2
3
@Indexed
4
private int viewCount;
5
6
public void incrementViewCount() {
7
State state = getState();
8
state.incrementAtomically(1);
9
state.save();
10
}
11
12
public void decrementViewCount() {
13
State state = getState();
14
state.decrementAtomically(1);
15
state.save();
16
}
17
}

Add to Collection

1
public class Article extends Record {
2
3
private Set<String> tags;
4
5
public void addTag(String tag) {
6
State state = getState();
7
state.addAtomically("tags", tag);
8
state.save();
9
}
10
}

Remove from Collection

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

Replace Value

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

Why Use Atomic Operations?

1
// Non-atomic - race condition!
2
article.setViewCount(article.getViewCount() + 1);
3
article.save();
4
// Another thread could increment between read and write
5
6
// Atomic - safe for concurrent access
7
state.incrementAtomically("viewCount", 1);
8
state.save();
9
// Database handles the increment atomically

Validation

Annotation-Based Validation

1
public class Article extends Record {
2
3
@Required
4
@Maximum(200)
5
private String title;
6
7
@Required
8
@Minimum(100)
9
private String content;
10
11
@Regex("[a-z0-9-]+")
12
private String slug;
13
14
@Step(5) // Must be multiple of 5
15
private int rating;
16
17
@Minimum(0)
18
@Maximum(100)
19
private int completion;
20
}

Custom Validation Hooks

1
public class Article extends Record {
2
3
@Override
4
protected void onValidate() {
5
State state = getState();
6
7
// Validate business rules
8
if (publishDate != null && publishDate.before(createdDate)) {
9
state.addError(
10
state.getField("publishDate"),
11
new IllegalArgumentException(
12
"Publish date cannot be before created date"
13
)
14
);
15
}
16
17
// Cross-field validation
18
if (featured && featuredImage == null) {
19
state.addError(
20
state.getField("featuredImage"),
21
new IllegalArgumentException(
22
"Featured articles must have a featured image"
23
)
24
);
25
}
26
}
27
}

Field-Level Error Tracking

1
try {
2
article.save();
3
} catch (ValidationException e) {
4
Map<ObjectField, List<Throwable>> errors = e.getErrors();
5
6
for (Map.Entry<ObjectField, List<Throwable>> entry : errors.entrySet()) {
7
ObjectField field = entry.getKey();
8
List<Throwable> fieldErrors = entry.getValue();
9
10
System.out.println("Errors in " + field.getInternalName() + ":");
11
for (Throwable error : fieldErrors) {
12
System.out.println(" - " + error.getMessage());
13
}
14
}
15
}

Lifecycle Hooks

Complete Hook Lifecycle

Hook Examples

1
public class Article extends Record {
2
3
@Override
4
protected void afterCreate() {
5
// Called once when object is first created
6
this.createdDate = new Date();
7
this.status = "draft";
8
}
9
10
@Override
11
protected void beforeSave() {
12
// Called before validation
13
// Good for normalizing data
14
if (title != null) {
15
title = title.trim();
16
}
17
18
// Auto-generate slug
19
if (slug == null && title != null) {
20
slug = generateSlug(title);
21
}
22
}
23
24
@Override
25
protected void onValidate() {
26
// Custom validation logic
27
if (content != null && content.length() < 100) {
28
getState().addError(
29
getState().getField("content"),
30
new IllegalArgumentException("Content too short")
31
);
32
}
33
}
34
35
@Override
36
protected void afterValidate() {
37
// Post-validation logic
38
// Good for setting derived fields
39
wordCount = countWords(content);
40
}
41
42
@Override
43
protected void afterSave() {
44
// Called after successful save
45
// Good for side effects (cache invalidation, notifications)
46
notificationService.notifyFollowers(this);
47
searchIndex.update(this);
48
}
49
50
@Override
51
protected void beforeDelete() {
52
// Called before delete
53
System.out.println("Deleting article: " + getLabel());
54
}
55
56
@Override
57
protected void afterDelete() {
58
// Called after successful delete
59
searchIndex.remove(this.getId());
60
cacheService.invalidate(this.getId());
61
}
62
63
private String generateSlug(String title) {
64
return title.toLowerCase()
65
.replaceAll("[^a-z0-9]+", "-")
66
.replaceAll("^-|-$", "");
67
}
68
69
private int countWords(String text) {
70
return text == null ? 0 : text.split("\\s+").length;
71
}
72
}

Cascading Operations

1
public class Author extends Record {
2
3
@Override
4
protected void beforeDelete() {
5
// Delete all articles by this author
6
List<Article> articles = Query.from(Article.class)
7
.where("author = ?", this)
8
.selectAll();
9
10
for (Article article : articles) {
11
article.delete();
12
}
13
}
14
}

Indexing Strategies

Field Indexing

1
public class Article extends Record {
2
3
// Simple index - enables querying and sorting
4
@Indexed
5
private String title;
6
7
// Unique index - enforces uniqueness
8
@Indexed(unique = true)
9
private String slug;
10
11
// Multiple fields - creates composite index
12
@Indexed
13
private List<Author> authors;
14
15
@Indexed
16
private Date publishDate;
17
}

Composite Indexes

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

Full-Text Indexing

1
public class Article extends Record {
2
3
// Full-text searchable
4
@Indexed
5
private String title;
6
}
7
8
// Full-text search
9
Query.from(Article.class)
10
.where("title matches ?", "search query")
11
.selectAll();

Embedded Object Indexing

1
public class Article extends Record {
2
3
// Index nested field
4
@Indexed
5
private Author author;
6
}
7
8
// Query by nested field
9
Query.from(Article.class)
10
.where("author/name = ?", "John Doe")
11
.selectAll();

Visibility Indexing

1
public class Article extends Record {
2
3
@Indexed
4
@VisibilityAware
5
private boolean visible;
6
7
public void hide() {
8
visible = false;
9
save();
10
}
11
}
12
13
// Default - only visible articles
14
Query.from(Article.class).selectAll();
15
16
// Find invisible articles
17
Query.from(Article.class)
18
.where("visible = false")
19
.selectAll();

Type Groups

Group related types for polymorphic queries:

1
@Recordable.Groups("content")
2
public class Article extends Record {
3
// ...
4
}
5
6
@Recordable.Groups("content")
7
public class Video extends Record {
8
// ...
9
}
10
11
@Recordable.Groups("content")
12
public class Image extends Record {
13
// ...
14
}
15
16
// Query all content types
17
Query<Record> allContent = Query.fromGroup("content")
18
.where("publishDate > ?", cutoffDate)
19
.selectAll();
20
21
for (Record content : allContent) {
22
if (content instanceof Article) {
23
Article article = (Article) content;
24
// Handle article
25
} else if (content instanceof Video) {
26
Video video = (Video) content;
27
// Handle video
28
}
29
}

Embedded Objects

Embedding vs. References

1
// Regular reference - stored separately
2
public class Article extends Record {
3
@Indexed
4
private Author author; // Reference to separate Author record
5
}
6
7
// Embedded - stored inline
8
public class Article extends Record {
9
@Embedded
10
private Address address; // Stored within Article
11
}
12
13
public class Address {
14
private String street;
15
private String city;
16
private String state;
17
private String zip;
18
}

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

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

Working with JSON

Converting to/from JSON

1
// State to JSON
2
State state = article.getState();
3
Map<String, Object> simpleValues = state.getSimpleValues();
4
String json = ObjectUtils.toJson(simpleValues);
5
6
// JSON to State
7
String json = "{ \"title\": \"Test\", \"content\": \"...\" }";
8
Map<String, Object> values = ObjectUtils.fromJson(json);
9
State state = State.getInstance(Article.class);
10
state.setValues(values);

Performance Optimization

Batch Loading

1
// Bad - N+1 queries
2
for (UUID id : articleIds) {
3
Article article = Query.from(Article.class)
4
.where("_id = ?", id)
5
.first();
6
// Process article
7
}
8
9
// Good - single query
10
List<Article> articles = Query.from(Article.class)
11
.where("_id = ?", articleIds)
12
.selectAll();

Caching Strategies

1
// Use query cache
2
List<Article> articles = Query.from(Article.class)
3
.where("featured = ?", true)
4
.selectAll();
5
// Results are cached automatically
6
7
// Bypass cache for fresh data
8
List<Article> fresh = Query.from(Article.class)
9
.noCache()
10
.where("featured = ?", true)
11
.selectAll();