Jakarta Data Integration
Declarative Repositories vs Imperative Morphium API — Side-by-Side
What Morphium Offers
Jakarta Data 1.0 support: define a @Repository interface with query methods, and
the Quarkus extension generates the implementation at build time. Method-name derivation,
@Find/@By annotations, JDQL queries and full MorphiumRepository
escape hatch for when you need the raw Morphium API.
The Challenge
Most MongoDB ODMs do not support Jakarta Data. Developers who want declarative repositories with MongoDB are usually stuck writing manual implementations or switching to a JPA-like abstraction that doesn't fit document databases.
Jakarta Data Features
Morphium API vs Jakarta Data — Side-by-Side
CRUD Operations
| Operation | Morphium (imperative) | Jakarta Data (declarative) |
|---|---|---|
| Find all | morphium.createQueryFor(Product.class).asList() |
repository.findAll().toList() |
| Find by ID | morphium.findById(Product.class, id) |
repository.findById(id) |
| Save (upsert) | morphium.store(product) |
repository.save(product) |
| Insert (fail if exists) | morphium.insert(product) |
repository.insert(product) |
| Delete | morphium.delete(product) |
repository.delete(product) |
| Count | morphium.createQueryFor(Product.class).countAll() |
repository.countByPriceGreaterThanEqual(0) |
Query Derivation
@Repository public interface EmployeeRepository extends BasicRepository<Employee, MorphiumId> { // Equality: WHERE department = ? List<Employee> findByDepartment(String dept); // Greater Than: WHERE salary > ? List<Employee> findBySalaryGreaterThan(double min); // Between: WHERE salary >= ? AND salary <= ? List<Employee> findBySalaryBetween(double min, double max); // Boolean: WHERE active = true List<Employee> findByActiveTrue(); // Count: COUNT WHERE department = ? long countByDepartment(String dept); // Exists: EXISTS WHERE email = ? boolean existsByEmail(String email); }
Each method name is parsed at build time into a Morphium query. The equivalent imperative code would be:
// findByDepartment("Engineering") morphium.createQueryFor(Employee.class) .f(Fields.department).eq("Engineering") .asList(); // findBySalaryGreaterThan(90000) morphium.createQueryFor(Employee.class) .f(Fields.salary).gt(90000) .asList(); // countByDepartment("Engineering") morphium.createQueryFor(Employee.class) .f(Fields.department).eq("Engineering") .countAll();
Pagination & Sorting
| Morphium | Jakarta Data |
|---|---|
query.f(Fields.department).eq(dept)
.sort(Map.of(Fields.salary, -1))
.skip((page - 1) * size)
.limit(size)
.asList(); |
repository.findByDepartment(dept,
PageRequest.ofPage(page, size, true),
Order.by(Sort.desc("salary"))); |
Jakarta Data returns a Page<T> with totalElements(),
totalPages(), hasNext() — all in one call. With Morphium,
you need a separate countAll() call.
@Find / @By / @OrderBy
// Embedded field (dot notation) + sort + limit @Find @OrderBy(value = "salary", descending = true) List<Employee> topEarnersInDepartment( @By("department") String dept, @By("active") boolean active, Limit limit); // Equivalent Morphium API: morphium.createQueryFor(Employee.class) .f(Fields.department).eq(dept) .f(Fields.active).eq(true) .sort(Map.of(Fields.salary, -1)) .limit(2) .asList();
@Query / JDQL
// Complex query with multiple conditions + sort @Query("WHERE department = :dept" + " AND salary >= :minSalary" + " AND active = true" + " ORDER BY salary DESC") List<Employee> topActiveEarners( @Param("dept") String dept, @Param("minSalary") double minSalary); // LIKE pattern matching (replaces Morphium regex) @Query("WHERE name LIKE :pattern ORDER BY price ASC") List<Product> searchByNameLike( @Param("pattern") String pattern);
@StaticMetamodel
// Generated at Quarkus build time (not in source code!) @StaticMetamodel(Employee.class) public class Employee_ { public static volatile Attribute<Employee> firstName; public static volatile SortableAttribute<Employee> salary; public static volatile TextAttribute<Employee> email; // ... all fields of the entity } // Usage: type-safe sort (no magic strings!) Order.by(Employee_.salary.desc());
Morphium ORM Features — Transparent Through Repositories
Since generated repositories delegate to morphium.store(), morphium.findById() etc.
internally, all Morphium ORM features work transparently through Jakarta Data repositories:
@Version(Optimistic Locking) —repository.save(entity)checks and increments the version automatically. AVersionMismatchExceptionis thrown on concurrent modification, just like withmorphium.store().@CreationTime/@LastChange— Timestamps are set automatically on everysave()orinsert()call through the repository.@PreStore/@PostStore/@PostLoad— All lifecycle callbacks fire normally when entities are loaded or stored through repositories.@Cache/@WriteBuffer— Morphium's read cache and write buffer are active for all repository queries.@Reference— Lazy and eager references are resolved automatically when entities are loaded viafindById(),findAll()or query methods.@Index— Indexes are created on startup, independent of how you access the data.
MorphiumRepository — The Escape Hatch
MorphiumRepository<T, K> extends CrudRepository with three
Morphium-specific methods for features beyond Jakarta Data 1.0:
distinct(fieldName)— returns unique values for a field across all documentsmorphium()— direct access to the Morphium instance for aggregation, atomic updates, etc.query()— creates a typed Morphium Query for complex conditions
This means you can use repository.morphium().inc(entity, "field", 1) instead of
injecting Morphium separately. All standard Jakarta Data features still work exactly the same.
Requires Direct Morphium API
The following features require MorphiumRepository.morphium() or @Inject Morphium:
- Atomic updates (
$inc,$set,$push,$pull) — field-level updates without loading the entity - Aggregation pipelines — $group, $lookup, $unwind, $bucket
- Change streams / Messaging — real-time event processing
When to Use Which Approach?
| Use Case | Recommendation |
|---|---|
| Standard CRUD | Jakarta Data — less boilerplate, declarative, type-safe |
| Simple queries (equality, range, boolean) | Jakarta Data — query derivation from method names |
| Complex queries (multi-condition, sort, LIKE) | Jakarta Data @Query/JDQL — SQL-like, readable |
| Pagination | Jakarta Data — Page<T> with total count in one call |
| Atomic field updates ($inc, $set) | Morphium API — field-level updates without loading the entity |
| Aggregation pipelines | Morphium API — $group, $lookup, $unwind etc. |
| @Version, @CreationTime, @PreStore, @Cache | Both! — work transparently through repository calls |
| Distinct queries | MorphiumRepository — repository.distinct("field") |
| Mixed: CRUD + advanced features | MorphiumRepository — Jakarta Data + morphium() / query() escape hatch |
How It Works Under the Hood
1. Jandex scans for @Repository interfaces at build time 2. Quarkus build step extracts entity type, ID type from generics 3. Custom method names are parsed: findByXxxAndYyyGreaterThan 4. Gizmo generates implementation class: - extends AbstractMorphiumRepository<T, K> - implements YourRepository interface - delegates to Morphium API at runtime 5. CDI registers the generated bean as @ApplicationScoped 6. @StaticMetamodel classes generated for all @Entity types Result: Zero runtime reflection, GraalVM native compatible!
Related Documentation
- Developer Guide — @Entity, @Embedded, Query API
- API Reference — Jakarta Data @Repository, @Find, @Query
- Field Names — @FieldNameConstants, StaticMetamodel