What Morphium Offers

Morphium's fluent Query<T> API covers every MongoDB query operator — from basic comparisons and regex to text search, array operators and projections — with type-safe field references. Pagination, sorting and cursor-based iteration are built in.

The Challenge

Most ODMs are either too raw (error-prone string-based queries) or too abstract (limiting what you can express). Morphium's Query API gives you the full power of MongoDB's query language with compile-time field safety.

Morphium Features Used

eq / ne Equality and not-equal operators. f("field").eq(value) matches documents where the field equals the value ($eq). ne(value) matches where the field does NOT equal the value ($ne). MongoDBAtlasCosmosDB gt / lt Strictly greater-than ($gt) and strictly less-than ($lt). Used for open-ended range queries. f("salary").gt(50000) finds salaries above 50k (exclusive). MongoDBAtlasCosmosDB gte / lte Greater-than-or-equal ($gte) and less-than-or-equal ($lte). Used for inclusive range queries. Chain both on the same field for bounded ranges: f("salary").gte(min).f("salary").lte(max). MongoDBAtlasCosmosDB in / nin Membership operators. in(list) matches documents where the field value is in the list ($in). nin(list) matches where the field value is NOT in the list ($nin). For array fields, $in matches if ANY element matches. MongoDBAtlasCosmosDB matches (regex) Regular expression matching ($regex). f("lastName").matches("^M") finds last names starting with M. Prefix-anchored patterns can use indexes; unanchored patterns trigger collection scans. MongoDBAtlasCosmosDB exists Field existence check ($exists: true). f("email").exists() finds documents where the email field is present. Morphium does not store null fields by default, so exists() also filters out nulls. MongoDBAtlasCosmosDB sort / skip / limit Server-side pagination and ordering. sort(Map.of("field", 1)) sorts ascending, -1 descending. skip(n) skips the first n documents. limit(m) returns at most m documents. MongoDBAtlasCosmosDB countAll / distinct Server-side aggregation. countAll() returns the number of matching documents without transferring data. distinct("field") returns all unique values for a field. Both run entirely on the MongoDB server. MongoDBAtlasCosmosDB @Aliases Declares alternative field names for schema evolution. @Aliases({"mail", "e_mail"}) on email means Morphium will read values from "mail" or "e_mail" fields in legacy documents. Only affects reads; writes always use the current field name. Import: de.caluga.morphium.annotations.Aliases MongoDBAtlasCosmosDB @Transient Marks a field as NOT persisted to MongoDB. The field exists only in Java and is never written to or read from the database. Useful for computed/derived values populated by @PostLoad. Import: de.caluga.morphium.annotations.Transient MongoDBAtlasCosmosDB @IgnoreNullFromDB If the field is null or missing in the MongoDB document, Morphium keeps the Java field's current value instead of overwriting it with null. Prevents NullPointerExceptions for List/Collection fields. Import: de.caluga.morphium.annotations.IgnoreNullFromDB MongoDBAtlasCosmosDB @FieldNameConstants Lombok annotation. Generates Employee.Fields.firstName, .salary etc. for type-safe query construction. Usage: query.f(Employee.Fields.salary).gte(min). Import: lombok.experimental.FieldNameConstants MongoDBAtlasCosmosDB @Lifecycle Class-level annotation that enables lifecycle callback processing. Without it, Morphium will NOT scan for or invoke @PostLoad, @PreStore, etc. on this class. A performance optimization. Import: de.caluga.morphium.annotations.lifecycle.Lifecycle MongoDBAtlasCosmosDB @PostLoad Method-level lifecycle callback invoked after a document is loaded from MongoDB and deserialized. Ideal for computing derived fields. Requires @Lifecycle on the class. Import: de.caluga.morphium.annotations.lifecycle.PostLoad MongoDBAtlasCosmosDB

Prerequisites & Key Concepts

  • @Aliases for field name migration — if a field was previously stored under a different name (e.g., "mail" or "e_mail"), Morphium will still map those legacy field names to the current Java field when reading. Writes always use the current name, so documents naturally migrate over time.
  • @Transient fields are computed in @PostLoad — the fullName field is never stored in MongoDB. Instead, the @PostLoad callback recomputes it from firstName and lastName every time the entity is loaded. The class must be annotated with @Lifecycle for this to work.
  • @IgnoreNullFromDB keeps Java defaults when DB field is null — the skills list uses this annotation so that if a document lacks a skills field, the Java field keeps its default value instead of being set to null. This prevents NullPointerExceptions.
  • Compound Index @Index({"lastName, firstName"}) — declared at class level, creates a compound index that accelerates queries filtering or sorting by lastName and/or firstName.

Entity Source Code

Employee.java Java
import de.caluga.morphium.annotations.Aliases;
import de.caluga.morphium.annotations.Entity;
import de.caluga.morphium.annotations.Id;
import de.caluga.morphium.annotations.IgnoreNullFromDB;
import de.caluga.morphium.annotations.Index;
import de.caluga.morphium.annotations.Transient;
import de.caluga.morphium.annotations.lifecycle.Lifecycle;
import de.caluga.morphium.annotations.lifecycle.PostLoad;
import de.caluga.morphium.driver.MorphiumId;
import lombok.experimental.FieldNameConstants;

@Entity(collectionName = "employees")
@Index({"lastName, firstName"}) // compound index1
@Lifecycle // enables @PostLoad, @PreStore, etc.2
@Data @NoArgsConstructor @AllArgsConstructor @Builder
@FieldNameConstants3
public class Employee {

    @Id
    private MorphiumId id;

    private String firstName;
    private String lastName;

    @Aliases({"mail", "e_mail"}) // reads legacy field names4
    private String email;

    @Index // single-field index for department queries
    private String department;

    private String position;
    private double salary;
    private LocalDateTime hireDate;

    @Transient // NOT stored in MongoDB5
    private transient String fullName;

    @IgnoreNullFromDB // keeps Java default if DB field is null6
    private List<String> skills;

    private boolean active;

    @PostLoad // called after deserialization from MongoDB7
    public void onPostLoad() {
        this.fullName = firstName + " " + lastName;
    }
}
1 Class-level @Index with multiple fields creates a compound index — accelerates queries filtering or sorting by lastName and firstName together.
2 @Lifecycle must be present for Morphium to scan and invoke lifecycle callbacks like @PostLoad on this class.
3 @FieldNameConstants generates Employee.Fields.salary etc. for type-safe field references in queries.
4 @Aliases maps legacy field names from older documents to this Java field on read — writes always use the current name email.
5 @Transient excludes this field from MongoDB persistence; it exists only in Java memory and is populated by the @PostLoad callback.
6 @IgnoreNullFromDB prevents Morphium from overwriting the Java field with null when the field is absent in the MongoDB document.
7 @PostLoad is called after deserialization — ideal for computing derived fields like fullName that are not stored in the database.

Query Operations Reference

Morphium API — All Query Operators Java
import de.caluga.morphium.Morphium;
import de.caluga.morphium.query.Query;

// Equality: f().eq(value) → MongoDB $eq
morphium.createQueryFor(Employee.class)
    .f(Employee.Fields.department).eq("Engineering").asList();1

// Range: gte/lte → MongoDB $gte/$lte (inclusive bounds)
morphium.createQueryFor(Employee.class)
    .f(Employee.Fields.salary).gte(60000)2
    .f(Employee.Fields.salary).lte(100000)
    .sort(Map.of(Employee.Fields.salary, 1)).asList();3

// Regex: matches() → MongoDB $regex
morphium.createQueryFor(Employee.class)
    .f(Employee.Fields.lastName).matches("^M").asList();4

// Membership: in(list) → MongoDB $in
morphium.createQueryFor(Employee.class)
    .f(Employee.Fields.skills).in(List.of("Java", "Python")).asList();5

// Exclusion: nin(list) → MongoDB $nin
morphium.createQueryFor(Employee.class)
    .f(Employee.Fields.department).nin(List.of("Sales", "HR")).asList();

// Existence: exists() → MongoDB $exists: true
morphium.createQueryFor(Employee.class)
    .f(Employee.Fields.email).exists().asList();6

// Pagination: sort + skip + limit
morphium.createQueryFor(Employee.class)
    .sort(Map.of(Employee.Fields.lastName, 1))
    .skip(page * size).limit(size).asList();7

// Count: server-side count without data transfer
morphium.createQueryFor(Employee.class)
    .f(Employee.Fields.department).eq("Engineering").countAll();8

// Distinct: unique values for a field, runs on server
morphium.createQueryFor(Employee.class)
    .distinct(Employee.Fields.department);
1 .eq() translates to MongoDB's $eq — chain multiple .f().eq() calls to AND conditions together.
2 .gte() and .lte() create inclusive range bounds; chain them on the same field for a bounded salary range.
3 .sort() accepts a Map of field name to direction (1 = ascending, -1 = descending) for server-side ordering.
4 .matches() maps to MongoDB's $regex — prefix-anchored patterns like ^M can leverage string indexes.
5 .in() maps to $in — for array fields it matches documents where ANY element of the array is in the provided list.
6 .exists() maps to $exists: true — finds documents where the field is present (Morphium omits null fields by default, so this also filters them out).
7 .skip(n).limit(m) implements cursor-based pagination entirely on the MongoDB server without transferring unwanted documents.
8 .countAll() runs a server-side count — no documents are transferred to the application, making it very efficient for large collections.

Related Documentation