Skip to main content
Version: 1.1.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
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();