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

@Repository Marks an interface as a Jakarta Data repository. The quarkus-morphium extension generates the implementation at build time. Extends BasicRepository or CrudRepository for CRUD methods. Import: jakarta.data.repository.Repository MongoDBAtlasCosmosDB CrudRepository Provides save(), findById(), delete(), findAll(), insert(), insertAll() etc. Extends BasicRepository with insert operations. Import: jakarta.data.repository.CrudRepository MongoDBAtlasCosmosDB MorphiumRepository Extends CrudRepository with Morphium-specific operations: distinct(), morphium(), query(). The escape hatch for features beyond Jakarta Data 1.0 without injecting Morphium separately. Import: de.caluga.morphium.quarkus.data.MorphiumRepository MongoDBAtlasCosmosDB Query Derivation Method names like findByDepartment(), countByStatus(), existsByEmail() are parsed at build time into Morphium queries. Supports Equals, GreaterThan, LessThan, Between, Like, True, False, In, Not, and more. MongoDBAtlasCosmosDB Pagination Pass PageRequest and Order parameters to get paginated results back as Page<T> with totalElements, totalPages, hasNext etc. CursoredPage<T> for keyset pagination. Import: jakarta.data.page.PageRequest, jakarta.data.page.Page MongoDBAtlasCosmosDB @Find / @By / @OrderBy Fine-grained query control. @By maps parameters to specific fields (supports dot notation for embedded fields). @OrderBy specifies static sort order. @Find marks the method as a query. Import: jakarta.data.repository.Find, By, OrderBy MongoDBAtlasCosmosDB @Query (JDQL) Jakarta Data Query Language — SQL-like syntax for complex queries. WHERE, AND, OR, LIKE, ORDER BY, BETWEEN, GROUP BY, HAVING with aggregation functions. Parameters with :name syntax. Import: jakarta.data.repository.Query MongoDBAtlasCosmosDB GROUP BY + Aggregation JDQL supports SELECT with GROUP BY and aggregate functions: COUNT(this), SUM(field), AVG(field), MIN(field), MAX(field). Results map to Java Records. Translates to MongoDB $group pipeline stage. MongoDBAtlasCosmosDB HAVING (AND/OR) Filter aggregated results with HAVING clause. Supports AND/OR combinators: HAVING COUNT(this) > :min OR SUM(salary) >= :threshold. Translates to MongoDB $match after $group. MongoDBAtlasCosmosDB Stream<T> Cursor-backed lazy Stream for memory-efficient large result set processing. Auto-closeable. Works with query derivation, @Find and @Query methods. MongoDBAtlasCosmosDB CompletionStage<T> Non-blocking async repository methods. Return CompletionStage<T> for any query. Executed on Morphium's async thread pool. Suffix method name with Async. MongoDBAtlasCosmosDB @StaticMetamodel Generated at build time. Provides type-safe Sort attributes like Employee_.salary.desc() for use with Order.by(). Import: jakarta.data.metamodel.StaticMetamodel MongoDBAtlasCosmosDB

Morphium API vs Jakarta Data — Side-by-Side

CRUD Operations

OperationMorphium (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 Interface Java
@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:

Equivalent Morphium API Java
// 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

MorphiumJakarta 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

Fine-Grained Queries Java
// 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

JDQL (Jakarta Data Query Language) Java
// 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:

Aggregation via JDQL Java
// 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:

HAVING with AND/OR Java
// 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:

Cursor-Backed Stream Java
@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:

Async Repository Method Java
// 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 Metamodel Java
// 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. A VersionMismatchException is thrown on concurrent modification, just like with morphium.store().
  • @CreationTime / @LastChange — Timestamps are set automatically on every save() or insert() 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 via findById(), 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 documents
  • morphium() — 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 CaseRecommendation
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 MorphiumRepositoryrepository.distinct("field")
Mixed: CRUD + advanced features MorphiumRepository — Jakarta Data + morphium() / query() escape hatch

How It Works Under the Hood

Build-Time Code Generation (Gizmo) text
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