Skip to main content
Version: 2.3.x

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
4
// Saved to database
5
state.setStatus(StateStatus.SAVED);
6
7
// Marked for deletion
8
state.setStatus(StateStatus.DELETED);
9
10
// Read-only (cannot be modified)
11
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
public String getMetaDescription() {
24
return metaDescription;
25
}
26
27
public void setMetaDescription(String metaDescription) {
28
this.metaDescription = metaDescription;
29
}
30
31
public String getCanonicalUrl() {
32
return canonicalUrl;
33
}
34
35
public void setCanonicalUrl(String canonicalUrl) {
36
this.canonicalUrl = canonicalUrl;
37
}
38
39
public List<String> getKeywords() {
40
if (keywords == null) {
41
keywords = new ArrayList<>();
42
}
43
return keywords;
44
}
45
46
public void setKeywords(List<String> keywords) {
47
this.keywords = keywords;
48
}
49
50
// Custom logic
51
public String getEffectiveMetaTitle() {
52
if (metaTitle != null) {
53
return metaTitle;
54
}
55
56
// Fallback to article title
57
Article article = as(Article.class);
58
return article != null ? article.getTitle() : null;
59
}
60
}

Using Modifications​

1
// Create an article
2
Article article = new Article();
3
article.setTitle("Getting Started");
4
article.setContent("Article content...");
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 = ?", article.getId()).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("sEOData/keywords = ?", "tutorial")
4
.selectAll();

Modification Lifecycle Hooks​

1
@Modification.Classes({ Article.class, BlogPost.class, Video.class, Image.class })
2
public class AuditModification extends Modification<Recordable> {
3
4
private Date createdDate;
5
private User createdBy;
6
private Date modifiedDate;
7
private User modifiedBy;
8
9
@Override
10
protected void beforeSave() {
11
User currentUser = getCurrentUser();
12
13
if (getState().isNew()) {
14
createdDate = new Date();
15
createdBy = currentUser;
16
}
17
18
modifiedDate = new Date();
19
modifiedBy = currentUser;
20
}
21
22
private User getCurrentUser() {
23
// Mock implementation
24
return new User();
25
}
26
27
public Date getCreatedDate() {
28
return createdDate;
29
}
30
31
public void setCreatedDate(Date createdDate) {
32
this.createdDate = createdDate;
33
}
34
35
public User getCreatedBy() {
36
return createdBy;
37
}
38
39
public void setCreatedBy(User createdBy) {
40
this.createdBy = createdBy;
41
}
42
43
public Date getModifiedDate() {
44
return modifiedDate;
45
}
46
47
public void setModifiedDate(Date modifiedDate) {
48
this.modifiedDate = modifiedDate;
49
}
50
51
public User getModifiedBy() {
52
return modifiedBy;
53
}
54
55
public void setModifiedBy(User modifiedBy) {
56
this.modifiedBy = modifiedBy;
57
}
58
}

Cross-Cutting Concerns with Modifications​

1
@Modification.Classes({ Article.class, Video.class, Image.class })
2
public class PublishableModification extends Modification<Recordable> {
3
4
@Indexed
5
private Date publishDate;
6
7
@Indexed
8
private Date expiryDate;
9
10
@Indexed
11
private String status; // "draft", "published", "expired"
12
13
public boolean isPublished() {
14
Date now = new Date();
15
return "published".equals(status)
16
&& (publishDate == null || publishDate.before(now))
17
&& (expiryDate == null || expiryDate.after(now));
18
}
19
20
public void publish() {
21
this.status = "published";
22
this.publishDate = new Date();
23
this.save();
24
}
25
26
public Date getPublishDate() {
27
return publishDate;
28
}
29
30
public void setPublishDate(Date publishDate) {
31
this.publishDate = publishDate;
32
}
33
34
public Date getExpiryDate() {
35
return expiryDate;
36
}
37
38
public void setExpiryDate(Date expiryDate) {
39
this.expiryDate = expiryDate;
40
}
41
42
public String getStatus() {
43
return status;
44
}
45
46
public void setStatus(String status) {
47
this.status = status;
48
}
49
}

Usage:

1
// Usage
2
Article article = new Article();
3
article.setTitle("News Article");
4
article.as(PublishableModification.class).publish();
5
6
// Query published content
7
List<Article> published = Query.from(Article.class)
8
.where(
9
"publishableModification/status = ? and publishableModification/publishDate <= ?",
10
"published", new Date())
11
.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 void incrementViewCount() {
2
State state = getState();
3
state.incrementAtomically("viewCount", 1);
4
state.save();
5
}
6
7
public void decrementViewCount() {
8
State state = getState();
9
state.decrementAtomically("viewCount", 1);
10
state.save();
11
}

Add to Collection​

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

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 ValidatedArticle 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 beforeCommit() {
44
// Called just before database write
45
// Good for final data manipulation before persistence
46
this.lastModifiedDate = new Date();
47
}
48
49
@Override
50
protected void afterSave() {
51
// Called after successful save
52
// Good for side effects (cache invalidation, notifications)
53
notificationService.notifyFollowers(this);
54
searchIndex.update(this);
55
}
56
57
@Override
58
protected void beforeDelete() {
59
// Called before delete
60
System.out.println("Deleting article: " + getLabel());
61
}
62
63
@Override
64
protected void afterDelete() {
65
// Called after successful delete
66
searchIndex.remove(this.getId());
67
cacheService.invalidate(this.getId());
68
}
69
70
private String generateSlug(String title) {
71
return title.toLowerCase()
72
.replaceAll("[^a-z0-9]+", "-")
73
.replaceAll("^-|-$", "");
74
}
75
76
private int countWords(String text) {
77
return text == null ? 0 : text.split("\\s+").length;
78
}
79
}

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 IndexedArticle 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
// Index on collection field
12
@Indexed
13
private List<Author> authors;
14
15
// Index for date range queries
16
@Indexed
17
private Date publishDate;
18
}

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
// Query all content types
2
List<Object> allContent = Query.fromGroup("content")
3
.where("publishDate > ?", cutoffDate)
4
.selectAll();
5
6
for (Object content : allContent) {
7
if (content instanceof Article) {
8
Article article = (Article) content;
9
// Handle article
10
} else if (content instanceof Video) {
11
Video video = (Video) content;
12
// Handle video
13
} else if (content instanceof Image) {
14
Image image = (Image) content;
15
// Handle image
16
}
17
}

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
}
1
public class Address {
2
3
private String street;
4
private String city;
5
private String state;
6
private String zip;
7
8
public String getStreet() {
9
return street;
10
}
11
12
public void setStreet(String street) {
13
this.street = street;
14
}
15
16
public String getCity() {
17
return city;
18
}
19
20
public void setCity(String city) {
21
this.city = city;
22
}
23
24
public String getState() {
25
return state;
26
}
27
28
public void setState(String state) {
29
this.state = state;
30
}
31
32
public String getZip() {
33
return zip;
34
}
35
36
public void setZip(String zip) {
37
this.zip = zip;
38
}
39
}

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();