Blog System
Versioning, References, Embedded Documents, Lifecycle Callbacks
What Morphium Offers
Morphium manages document lifecycle automatically. @Version prevents concurrent overwrites
with optimistic locking. @Reference links documents across collections (eager or lazy).
@CreationTime, @LastChange and @LastAccess maintain timestamps
without application code. Lifecycle callbacks (@PreStore, @PostLoad) let entities
react to persistence events.
The Challenge
Versioning, cross-collection references and automatic timestamps typically require custom interceptors or event listeners with significant boilerplate. Morphium handles all of this declaratively through annotations on the entity class.
Morphium Features Used
Prerequisites & Key Concepts
- Eager vs Lazy
@Reference— By default,@Referenceis eager: Morphium resolves the referenced entity immediately when the parent is loaded (extra query). With@Reference(lazyLoading = true), Morphium returns a proxy that fetches the target only when a getter is first called. Lazy references reduce initial load time but may cause unexpected queries later. @Referencecascade options —@Reference(cascadeDelete = true)automatically deletes referenced entities when the parent is deleted.@Reference(orphanRemoval = true)deletes entities that are removed from a reference field during an update. WithautomaticStore = true(the default), referenced entities without an ID are auto-stored when the parent is stored — you do not need to store them manually first.@Versioncounter — When you store an entity with a@Versionfield, Morphium adds a filter{ version: currentValue }to the update. If another thread already incremented the version, the update matches zero documents and Morphium throwsVersionMismatchException. This is optimistic locking — no database-level locks are held.@Lifecyclerequirement — The class-level@Lifecycleannotation is a prerequisite for any lifecycle method (@PreStore,@PostLoad, etc.) to fire. Without it, the callbacks are silently ignored.@Transient+@PostLoadpattern — Mark a field@Transientso Morphium skips it during serialization. Then annotate a method with@PostLoadto recompute the transient value after every read. This is useful for summary strings, computed flags, or denormalized counts that should not be stored.- Container types — Morphium natively handles
List<String>(stored as a JSON array) andMap<String, String>(stored as a sub-document). No special annotation is needed for these standard collection types.
Entity Relationship
BlogPost (@Entity)
_id: MorphiumId @Id
title: String @Index
content: String
author: Author @Reference
reviewer: Author @Reference(lazyLoading = true)
comments: List<Comment> @Embedded
tags: List<String>
metadata: Map<String, String>
published: boolean
version: Long @Version
createdAt: LocalDateTime @CreationTime
lastChanged: LocalDateTime @LastChange
lastAccessed: LocalDateTime @LastAccess
editNote: String @Transient
@Reference →
Author (@Entity)
_id: MorphiumId @Id
username: String @Index(unique)
displayName: String
email: String
bio: String
@Embedded →
@Entity
@Embedded
@Reference
Entity Source Code
BlogPost.java
Java
import de.caluga.morphium.annotations.*; import de.caluga.morphium.annotations.lifecycle.Lifecycle; import de.caluga.morphium.annotations.lifecycle.PreStore; import de.caluga.morphium.annotations.lifecycle.PostLoad; import de.caluga.morphium.driver.MorphiumId; import lombok.Data; import lombok.experimental.FieldNameConstants; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Entity(collectionName = "blog_posts")1 @Lifecycle2 @Data @FieldNameConstants public class BlogPost { @Id private MorphiumId id; @Index3 private String title; private String content; @Reference4 private Author author; // eager by default @Reference(lazyLoading = true)5 private Author reviewer; // loaded on first access private List<Comment> comments; // @Embedded sub-documents private List<String> tags; private Map<String, String> metadata; private boolean published; @Version6 private Long version; @CreationTime7 private LocalDateTime createdAt; @LastChange8 private LocalDateTime lastChanged; @LastAccess private LocalDateTime lastAccessed; @Transient9 private transient String editNote; // not persisted @PreStore10 public void onPreStore() { if (tags == null) tags = new ArrayList<>(); if (metadata == null) metadata = new HashMap<>(); } @PostLoad11 public void onPostLoad() { // Could perform validation or enrichment after loading } }
1 Maps this class to the
blog_posts MongoDB collection.2 Required for lifecycle callbacks (
@PreStore, @PostLoad, etc.) to fire. Imported from de.caluga.morphium.annotations.lifecycle.3 Creates a single-field ascending index on
title for fast lookups.4 Stores a DBRef to the
Author in a separate collection; resolved eagerly by default.5 Lazy reference — Morphium creates a proxy; the
Author is not loaded from the database until a method on reviewer is actually called.6 Optimistic locking counter — Morphium auto-increments on every store and throws
VersionMismatchException on conflict.7 Set once by Morphium when the entity is first persisted; never updated again.
8 Updated automatically by Morphium on every
store() or field-level set() call.9 Not stored in MongoDB. The
transient keyword adds Java serialization safety on top of Morphium's @Transient.10 Called before each write — ensures
tags and metadata are never null in the database.11 Called after each read — can be used for validation, enrichment, or rehydrating transient state.
Author.java
Java
import de.caluga.morphium.annotations.Entity; import de.caluga.morphium.annotations.Id; import de.caluga.morphium.annotations.Index; import de.caluga.morphium.driver.MorphiumId; import lombok.Data; @Entity(collectionName = "authors")1 @Data public class Author { @Id2 private MorphiumId id; @Index(options = {"unique:1"})3 private String username; private String displayName; private String email; private String bio; }
1 Declares this class as a top-level Morphium entity stored in the
authors collection.2 Marks the field as the MongoDB document identifier (
_id); MorphiumId wraps an ObjectId.3 Unique index — MongoDB enforces that no two authors can share the same
username, even under concurrent inserts.
Comment.java
Java
import de.caluga.morphium.annotations.Embedded; import lombok.Data; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Embedded1 @Data @NoArgsConstructor @AllArgsConstructor public class Comment { private String author; private String text; private LocalDateTime createdAt; }
1 Marks this class as an embedded sub-document — stored inline inside the parent document, not in its own collection.
Service Code
Store & Version Bump
Morphium API — Store & Version
Java
import de.caluga.morphium.Morphium; import de.caluga.morphium.query.Query; @Inject Morphium morphium;1 // Store a new blog post — @Version starts at 1 morphium.store(blogPost);2 // Update the post — @Version auto-increments (1 → 2) // If another thread stored version 2 first, this throws // VersionMismatchException (optimistic locking) blogPost.setTitle("Updated Title"); morphium.store(blogPost);3
1 CDI-injected
Morphium instance provided by the Quarkus extension.2 Persists the entity for the first time; Morphium sets the
@Version field to 1.3 Increments
@Version to 2; throws VersionMismatchException if another thread already wrote version 2.Add Embedded Comment
Add Embedded Comment
Java
// Add a comment as an embedded sub-document Comment comment = new Comment(); comment.setText("Great post!"); comment.setAuthor("Alice"); comment.setCreatedAt(LocalDateTime.now()); // Push directly into the embedded list morphium.push(blogPost, BlogPost.Fields.comments, comment, false);1 // Or add to list and re-store the whole entity blogPost.getComments().add(comment); morphium.store(blogPost); // @Version increments2
1 Appends a single element to the embedded array in MongoDB using an atomic
$push update. The fourth parameter (false) disables upsert — the document must already exist.2 Re-stores the entire entity including the updated comments list;
@Version is incremented atomically.
@Reference Eager Loading
Java
// When loading a BlogPost, the @Reference author // is resolved eagerly (extra query to "authors" collection) BlogPost post = morphium.createQueryFor(BlogPost.class)1 .f(BlogPost.Fields.title).eq("My First Post")2 .get(); // author is already populated — no lazy proxy String authorName = post.getAuthor().getDisplayName();3 // With automaticStore=true (default), Morphium auto-stores the Author Author author = new Author(); author.setUsername("janedoe"); author.setDisplayName("Jane Doe"); morphium.store(author);4 BlogPost newPost = new BlogPost(); newPost.setAuthor(author); // sets the @Reference5 morphium.store(newPost);
1 Creates a type-safe, fluent query builder for the
BlogPost collection.2
BlogPost.Fields.title is a Lombok-generated compile-time constant — no magic strings.3 Eager
@Reference means the Author is already fully populated when this line runs — no further query is triggered.4 Explicit store for clarity. With
automaticStore = true (the default), Morphium auto-stores referenced entities that lack an ID when the parent is stored.5 Morphium serializes this assignment as a DBRef (or just the
ObjectId) inside the BlogPost document.Related Documentation
- Developer Guide — @Version, @Reference, @Embedded, Lifecycle
- Optimistic Locking — @Version, VersionMismatchException
- API Reference — store(), createQueryFor(), @Reference eager/lazy