HowTo references and relationships - sboesebeck/morphium GitHub Wiki
This guide covers Morphium's @Reference annotation for modeling relationships between entities, including cascade operations and circular reference handling.
| Feature | @Reference |
@Embedded |
|---|---|---|
| Storage | Only the target's ID in the parent document | Full object embedded in parent document |
| Own collection | Yes — referenced entity lives in its own collection | No — embedded within the parent |
| Independent lifecycle | Yes — can exist without the parent | No — tied to the parent document |
| Query performance | N+1 queries (one per reference) | Single read for parent + children |
| Update independence | Can be updated without touching the parent | Must update the parent document |
Use @Reference when:
- The referenced entity is shared across multiple parents
- You need independent queries on the referenced entity
- The referenced data is large or frequently updated independently
Use @Embedded when:
- The child data belongs exclusively to the parent
- You want atomic reads/writes of parent + children
- The embedded data is small and rarely queried independently
@Entity
public class BlogPost {
@Id
private MorphiumId id;
private String title;
@Reference
private Author author;
@Reference
private List<Comment> comments;
@Reference
private Map<String, Tag> tags;
}In MongoDB, only the author's ID (and type info) is stored in the BlogPost document. The Author object lives in its own collection.
When automaticStore is enabled (the default), Morphium automatically persists referenced objects that don't yet have an ID:
Author author = new Author();
author.setName("Jane");
// author has no ID yet
BlogPost post = new BlogPost();
post.setAuthor(author);
morphium.store(post);
// author is automatically stored first, then post references author's new IDSet automaticStore = false when you want to control persistence manually:
@Reference(automaticStore = false)
private Author author;
// Must store author first:
morphium.store(author);
post.setAuthor(author);
morphium.store(post);With lazyLoading = true, referenced objects are not loaded from the database until first accessed:
@Reference(lazyLoading = true)
private Author author;
// When post is loaded, author is a CGLib proxy (no DB query yet)
BlogPost post = morphium.findById(BlogPost.class, id);
// First access triggers the actual DB query:
String name = post.getAuthor().getName(); // DB query happens hereBidirectional references: If entity A references B and B references A, Morphium automatically detects the cycle during deserialization and breaks it by substituting a lazy proxy for the back-reference. This works transparently even without lazyLoading = true — no StackOverflowError will occur. For best performance you can still set lazyLoading = true on one side to avoid the extra DB round-trip.
When cascadeDelete = true, deleting the parent entity also deletes the referenced entities.
Important: The entity class must be annotated with @CascadeAware for cascade operations to take effect. Without this marker, cascadeDelete and orphanRemoval checks are skipped entirely for performance (analogous to @Lifecycle for lifecycle callbacks).
@Entity
@CascadeAware
public class Order {
@Id
private MorphiumId id;
@Reference(cascadeDelete = true)
private List<OrderItem> items;
@Reference // default: cascadeDelete = false
private Customer customer;
}
// Deleting the order also deletes all items, but keeps the customer
morphium.delete(order);Key behaviors:
-
Requires
@CascadeAwareon the entity class — without it, cascade operations are silently skipped - Only applies to entity-based
remove(Object)/delete(Object)calls, not query-based deletes - Cascade delete is recursive: if OrderItem also has
@Reference(cascadeDelete = true), those references are deleted too - Cycle-safe: Circular cascade references (A→B→A) are detected and won't cause infinite loops
- Referenced objects are deleted before the parent to maintain referential consistency
When orphanRemoval = true, updating a parent entity will automatically delete referenced objects that are no longer referenced:
@Entity
@CascadeAware
public class Team {
@Id
private MorphiumId id;
@Reference(orphanRemoval = true)
private List<Player> roster;
}
// Initial state: team references players A, B, C
team.getRoster().remove(playerB);
morphium.store(team);
// playerB is automatically deleted from the database (orphan removed)Key behaviors:
- Only triggers on updates (entities with an existing ID), not inserts
- Setting a reference to
nullalso triggers orphan removal for the previously referenced object - The old version is loaded from DB before the update to determine which references were removed
- Works with single references, lists, and maps
| Scenario | cascadeDelete |
orphanRemoval |
|---|---|---|
| Parent deleted | Referenced entities deleted | No effect |
| Reference removed from parent | No effect | Unreferenced entity deleted |
| Both enabled | Both behaviors active | Both behaviors active |
Morphium includes cycle detection to prevent StackOverflowError when serializing entities with circular @Reference chains:
@Entity
public class Node {
@Id
private MorphiumId id;
@Reference
private Node next;
}
// A → B → A (circular)
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
// Morphium handles this gracefully:
// - Writer assigns IDs before serialization
// - If a cycle is detected during serialize(), objects with IDs return a minimal {_id: ...} document
// - Objects without IDs throw IllegalStateException with a clear error message
morphium.store(a);Best practices for bidirectional references:
- Cycle detection is automatic — bidirectional references work out of the box
- For performance, consider
lazyLoading = trueon one side to skip a DB round-trip - Use
automaticStore = falseon one side and store that entity first - These approaches can be combined:
@Entity
public class Parent {
@Reference
private Child child; // eager, auto-store
}
@Entity
public class Child {
@Reference(lazyLoading = true, automaticStore = false)
private Parent parent; // lazy, manual store
}Loading an entity with N @Reference fields triggers N additional queries. Mitigation strategies:
- Lazy loading — defer queries until actually needed
-
Caching — use
@Cacheon frequently referenced entity types -
Embedding — switch to
@Embeddedif the data is always loaded together - Projection — use query projections to load only the fields you need
| Feature | JPA | Morphium |
|---|---|---|
| Eager/Lazy loading | @ManyToOne(fetch = LAZY) |
@Reference(lazyLoading = true) |
| Cascade persist | cascade = PERSIST |
automaticStore = true (default) |
| Cascade delete | cascade = REMOVE |
cascadeDelete = true |
| Orphan removal | orphanRemoval = true |
orphanRemoval = true |
| Bidirectional |
mappedBy + inverse side |
Manual setup on both sides |
| FK constraints | Enforced by DB | Not enforced (MongoDB has no FK) |