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:
1State state = article.getState();23// Access fields by name4String title = (String) state.get("title");5Date publishDate = (Date) state.get("publishDate");67// Path-based access for nested fields8String authorName = (String) state.getByPath("author/name");910// Set values11state.put("title", "New Title");12state.putByPath("author/name", "John Doe");1314// Check if field exists15boolean hasTitle = state.containsKey("title");1617// Get all field values18Map<String, Object> values = state.getSimpleValues();
State Lifecycle
1// New object - not yet saved2State state = State.getInstance(Article.class);3state.setStatus(StateStatus.NEW);45// Saved to database6state.setStatus(StateStatus.SAVED);78// Marked for deletion9state.setStatus(StateStatus.DELETED);1011// Read-only (cannot be modified)12state.setStatus(StateStatus.REFERENCE_ONLY);
Why Separate State from Record?
- Composition - Multiple object types can share the same State
- Flexibility - Query without loading full objects
- Modifications - Aspect-oriented extensions (see below)
- Lazy Loading - Load data on-demand
- 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})2public class SEOData extends Modification<Recordable> {34@Indexed5private String metaTitle;67@Indexed8private String metaDescription;910private String canonicalUrl;1112private List<String> keywords;1314// Getters and setters15public String getMetaTitle() {16return metaTitle;17}1819public void setMetaTitle(String metaTitle) {20this.metaTitle = metaTitle;21}2223// ... other getters/setters2425// Custom logic26public String getEffectiveMetaTitle() {27if (metaTitle != null) {28return metaTitle;29}3031// Fallback to article title32Article article = as(Article.class);33return article != null ? article.getTitle() : null;34}35}
Using Modifications
1// Create an article2Article article = new Article();3article.setTitle("Getting Started");4article.setContent("...");56// Access the SEO modification7SEOData seo = article.as(SEOData.class);8seo.setMetaTitle("Getting Started Guide");9seo.setMetaDescription("Learn how to get started...");10seo.setKeywords(Arrays.asList("tutorial", "guide", "introduction"));1112// Save - both Article and SEOData are saved13article.save();1415// Later, load the article16Article loaded = Query.from(Article.class).where("_id = ?", id).first();1718// Access SEO data19SEOData loadedSeo = loaded.as(SEOData.class);20String metaTitle = loadedSeo.getMetaTitle();
Querying with Modifications
1// Query by modification field2List<Article> articles = Query.from(Article.class)3.where("keywords = ?", "tutorial")4.selectAll();
Modification Lifecycle Hooks
1public class AuditModification extends Modification<Recordable> {23private Date createdDate;4private User createdBy;5private Date modifiedDate;6private User modifiedBy;78@Override9protected void beforeSave() {10User currentUser = getCurrentUser();1112if (getState().isNew()) {13createdDate = new Date();14createdBy = currentUser;15}1617modifiedDate = new Date();18modifiedBy = currentUser;19}2021// Getters and setters...22}
Cross-Cutting Concerns with Modifications
1// Publishable - adds publishing workflow2@Modification.Classes({Article.class, Video.class, Image.class})3public class Publishable extends Modification<Recordable> {45@Indexed6private Date publishDate;78@Indexed9private Date expiryDate;1011@Indexed12private String status; // "draft", "published", "expired"1314public boolean isPublished() {15Date now = new Date();16return "published".equals(status)17&& (publishDate == null || publishDate.before(now))18&& (expiryDate == null || expiryDate.after(now));19}2021public void publish() {22this.status = "published";23this.publishDate = new Date();24this.save();25}26}2728// Usage29Article article = new Article();30article.setTitle("News Article");31article.as(Publishable.class).publish();3233// Query published content34List<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" }34// When accessed, resolved to full object5Article article = ...;6Author author = article.getAuthor(); // Lazy loaded here
Reference Resolution Diagram
Controlling Resolution
1// Load without resolving references (fast)2List<Article> articles = Query.from(Article.class)3.resolveToReferenceOnly()4.selectAll();
Resolving Invisible Records
1// Include invisible/deleted records when resolving references2Query.from(Article.class)3.resolveInvisible()4.selectAll();
Atomic Operations
Atomic operations allow modifying values without race conditions:
Increment and Decrement
1public class Article extends Record {23@Indexed4private int viewCount;56public void incrementViewCount() {7State state = getState();8state.incrementAtomically(1);9state.save();10}1112public void decrementViewCount() {13State state = getState();14state.decrementAtomically(1);15state.save();16}17}
Add to Collection
1public class Article extends Record {23private Set<String> tags;45public void addTag(String tag) {6State state = getState();7state.addAtomically("tags", tag);8state.save();9}10}
Remove from Collection
1public void removeTag(String tag) {2State state = getState();3state.removeAtomically("tags", tag);4state.save();5}
Replace Value
1public void updateStatusIfDraft(String newStatus) {2State state = getState();3// Only replaces if current value is same as one in database4state.replaceAtomically("status", newStatus);5state.save();6}
Why Use Atomic Operations?
1// Non-atomic - race condition!2article.setViewCount(article.getViewCount() + 1);3article.save();4// Another thread could increment between read and write56// Atomic - safe for concurrent access7state.incrementAtomically("viewCount", 1);8state.save();9// Database handles the increment atomically
Validation
Annotation-Based Validation
1public class Article extends Record {23@Required4@Maximum(200)5private String title;67@Required8@Minimum(100)9private String content;1011@Regex("[a-z0-9-]+")12private String slug;1314@Step(5) // Must be multiple of 515private int rating;1617@Minimum(0)18@Maximum(100)19private int completion;20}
Custom Validation Hooks
1public class Article extends Record {23@Override4protected void onValidate() {5State state = getState();67// Validate business rules8if (publishDate != null && publishDate.before(createdDate)) {9state.addError(10state.getField("publishDate"),11new IllegalArgumentException(12"Publish date cannot be before created date"13)14);15}1617// Cross-field validation18if (featured && featuredImage == null) {19state.addError(20state.getField("featuredImage"),21new IllegalArgumentException(22"Featured articles must have a featured image"23)24);25}26}27}
Field-Level Error Tracking
1try {2article.save();3} catch (ValidationException e) {4Map<ObjectField, List<Throwable>> errors = e.getErrors();56for (Map.Entry<ObjectField, List<Throwable>> entry : errors.entrySet()) {7ObjectField field = entry.getKey();8List<Throwable> fieldErrors = entry.getValue();910System.out.println("Errors in " + field.getInternalName() + ":");11for (Throwable error : fieldErrors) {12System.out.println(" - " + error.getMessage());13}14}15}
Lifecycle Hooks
Complete Hook Lifecycle
Hook Examples
1public class Article extends Record {23@Override4protected void afterCreate() {5// Called once when object is first created6this.createdDate = new Date();7this.status = "draft";8}910@Override11protected void beforeSave() {12// Called before validation13// Good for normalizing data14if (title != null) {15title = title.trim();16}1718// Auto-generate slug19if (slug == null && title != null) {20slug = generateSlug(title);21}22}2324@Override25protected void onValidate() {26// Custom validation logic27if (content != null && content.length() < 100) {28getState().addError(29getState().getField("content"),30new IllegalArgumentException("Content too short")31);32}33}3435@Override36protected void afterValidate() {37// Post-validation logic38// Good for setting derived fields39wordCount = countWords(content);40}4142@Override43protected void afterSave() {44// Called after successful save45// Good for side effects (cache invalidation, notifications)46notificationService.notifyFollowers(this);47searchIndex.update(this);48}4950@Override51protected void beforeDelete() {52// Called before delete53System.out.println("Deleting article: " + getLabel());54}5556@Override57protected void afterDelete() {58// Called after successful delete59searchIndex.remove(this.getId());60cacheService.invalidate(this.getId());61}6263private String generateSlug(String title) {64return title.toLowerCase()65.replaceAll("[^a-z0-9]+", "-")66.replaceAll("^-|-$", "");67}6869private int countWords(String text) {70return text == null ? 0 : text.split("\\s+").length;71}72}
Cascading Operations
1public class Author extends Record {23@Override4protected void beforeDelete() {5// Delete all articles by this author6List<Article> articles = Query.from(Article.class)7.where("author = ?", this)8.selectAll();910for (Article article : articles) {11article.delete();12}13}14}
Indexing Strategies
Field Indexing
1public class Article extends Record {23// Simple index - enables querying and sorting4@Indexed5private String title;67// Unique index - enforces uniqueness8@Indexed(unique = true)9private String slug;1011// Multiple fields - creates composite index12@Indexed13private List<Author> authors;1415@Indexed16private Date publishDate;17}
Composite Indexes
1// Query using composite index2// Efficient if both fields are indexed3Query.from(Article.class)4.where("author = ? and publishDate > ?", author, cutoffDate)5.selectAll();
Full-Text Indexing
1public class Article extends Record {23// Full-text searchable4@Indexed5private String title;6}78// Full-text search9Query.from(Article.class)10.where("title matches ?", "search query")11.selectAll();
Embedded Object Indexing
1public class Article extends Record {23// Index nested field4@Indexed5private Author author;6}78// Query by nested field9Query.from(Article.class)10.where("author/name = ?", "John Doe")11.selectAll();
Visibility Indexing
1public class Article extends Record {23@Indexed4@VisibilityAware5private boolean visible;67public void hide() {8visible = false;9save();10}11}1213// Default - only visible articles14Query.from(Article.class).selectAll();1516// Find invisible articles17Query.from(Article.class)18.where("visible = false")19.selectAll();
Type Groups
Group related types for polymorphic queries:
1@Recordable.Groups("content")2public class Article extends Record {3// ...4}56@Recordable.Groups("content")7public class Video extends Record {8// ...9}1011@Recordable.Groups("content")12public class Image extends Record {13// ...14}1516// Query all content types17Query<Record> allContent = Query.fromGroup("content")18.where("publishDate > ?", cutoffDate)19.selectAll();2021for (Record content : allContent) {22if (content instanceof Article) {23Article article = (Article) content;24// Handle article25} else if (content instanceof Video) {26Video video = (Video) content;27// Handle video28}29}
Embedded Objects
Embedding vs. References
1// Regular reference - stored separately2public class Article extends Record {3@Indexed4private Author author; // Reference to separate Author record5}67// Embedded - stored inline8public class Article extends Record {9@Embedded10private Address address; // Stored within Article11}1213public class Address {14private String street;15private String city;16private String state;17private 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 field2Query.from(Article.class)3.where("address/city = ?", "New York")4.selectAll();
Working with JSON
Converting to/from JSON
1// State to JSON2State state = article.getState();3Map<String, Object> simpleValues = state.getSimpleValues();4String json = ObjectUtils.toJson(simpleValues);56// JSON to State7String json = "{ \"title\": \"Test\", \"content\": \"...\" }";8Map<String, Object> values = ObjectUtils.fromJson(json);9State state = State.getInstance(Article.class);10state.setValues(values);
Performance Optimization
Batch Loading
1// Bad - N+1 queries2for (UUID id : articleIds) {3Article article = Query.from(Article.class)4.where("_id = ?", id)5.first();6// Process article7}89// Good - single query10List<Article> articles = Query.from(Article.class)11.where("_id = ?", articleIds)12.selectAll();
Caching Strategies
1// Use query cache2List<Article> articles = Query.from(Article.class)3.where("featured = ?", true)4.selectAll();5// Results are cached automatically67// Bypass cache for fresh data8List<Article> fresh = Query.from(Article.class)9.noCache()10.where("featured = ?", true)11.selectAll();