Jakarta Data Integration
@Repository, JDQL, GROUP BY Aggregation, Stream, Async — 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);
GROUP BY + Aggregation (JDQL)
JDQL supports GROUP BY with aggregate functions — no manual pipeline construction needed.
Results map directly to Java Records:
// Record return type — fields match SELECT order record DeptStats(String department, long count, double sum) {} // Headcount + total salary per department @Query("SELECT department, COUNT(this), SUM(salary)" + " GROUP BY department ORDER BY department ASC") List<DeptStats> statsByDepartment(); // Equivalent Morphium aggregation pipeline: var agg = morphium.createAggregator(Employee.class, Map.class); agg.group("$department") .sum("count", 1) .sum("sum", "$salary") .end(); agg.sort(Map.of("_id", 1)); agg.aggregate();
Supported aggregate functions: COUNT(this), COUNT(field),
SUM(field), AVG(field), MIN(field), MAX(field).
Multi-field GROUP BY is supported: GROUP BY department, status.
HAVING Clause
Filter aggregated results with HAVING — supports AND/OR combinators:
// Only departments with > 1 employee @Query("SELECT department, COUNT(this), SUM(salary)" + " GROUP BY department" + " HAVING COUNT(this) > :min") List<DeptStats> deptStatsHavingCountAbove( @Param("min") long minCount); // OR combinator: large team OR high total salary @Query("SELECT department, COUNT(this), SUM(salary)" + " GROUP BY department" + " HAVING COUNT(this) > :minCount" + " OR SUM(salary) >= :minSalary") List<DeptStats> deptStatsHavingOr( @Param("minCount") long minCount, @Param("minSalary") double minSalary);
Translates to a $match stage after $group in the MongoDB aggregation pipeline.
The extension handles the entire pipeline construction — you just write JDQL.
Stream<T> — Memory-Efficient Processing
Return Stream<T> from any query method for cursor-backed lazy loading.
Documents are fetched incrementally — ideal for large result sets that don't fit in memory:
@Query("WHERE department = :dept ORDER BY salary DESC") Stream<Employee> streamByDepartment( @Param("dept") String department); // Usage — auto-closeable, processes one document at a time try (Stream<Employee> stream = repo.streamByDepartment("Engineering")) { stream.filter(e -> e.getSalary() > 80000) .map(Employee::getFirstName) .forEach(System.out::println); }
Works with query derivation, @Find, and @Query methods.
The stream delegates to Morphium's Query.stream() which uses a MongoDB cursor internally.
CompletionStage<T> — Async Queries
Return CompletionStage<T> for non-blocking repository queries.
The query executes on Morphium's async thread pool:
// Suffix method name with Async — return type is CompletionStage CompletionStage<List<Employee>> findByDepartmentAsync( String department); // Usage — compose with thenApply, thenAccept, etc. repo.findByDepartmentAsync("Engineering") .thenApply(employees -> employees.stream() .map(Employee::getFirstName) .toList()) .thenAccept(names -> System.out.println(names));
Async variants also work for findById, save, insert,
update, and delete operations on MorphiumRepository.
@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 - Multi-stage aggregation — $lookup, $unwind, $bucket, $facet (simple GROUP BY + HAVING works through JDQL)
- 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> / CursoredPage<T> with total count in one call |
| GROUP BY / aggregation (COUNT, SUM, AVG) | Jakarta Data @Query/JDQL — GROUP BY + HAVING with Record return types |
| Stream / async queries | Jakarta Data — Stream<T> (cursor-backed) and CompletionStage<T> |
| Complex aggregation ($lookup, $unwind, $bucket) | Morphium API — multi-stage pipelines beyond GROUP BY |
| Atomic field updates ($inc, $set) | Morphium API — field-level updates without loading the entity |
| @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