Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

For modern Hibernate applications, query object properties with the Jakarta Persistence Criteria API: build a typed query from an entity root, refer to mapped Java attributes with paths, and use joins for entity associations. The old native Hibernate org.hibernate.Criteria API is a different, legacy API; Hibernate deprecated it in the 5.x line and removed it in Hibernate ORM 6.0. These examples use jakarta.persistence.criteria.*, not the older javax.persistence.criteria.* namespace. See the Hibernate 6 migration guide for the removal and migration context.

What “querying object properties” means

Criteria queries work with the persistent entity model, not ordinarily with database column names. If a mapped Java attribute is named status, use customer.get("status") or the static metamodel equivalent—not a database column name such as customer_status. Attribute names must match the persistent Java attributes as mapped by the entity’s access strategy (field or property access).

For example, assume Customer has basic attributes name and status, a single-valued address mapping, and an orders collection. The exact navigation depends on whether address is an embeddable value or an associated entity; that distinction matters later.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The basic Criteria query lifecycle

The main pieces are CriteriaBuilder (creates expressions, predicates and ordering), CriteriaQuery<T> (describes the result), Root<T> (the entity being queried), Path<T> (an attribute path), Predicate (a condition), and the typed query used to execute it. The Jakarta Criteria API documentation describes these building blocks.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
Root<Customer> customer = cq.from(Customer.class);

Predicate active = cb.equal(customer.get("status"), CustomerStatus.ACTIVE);
cq.select(customer)
  .where(active)
  .orderBy(cb.asc(customer.get("name")));

List<Customer> customers = entityManager.createQuery(cq).getResultList();

The sequence is: obtain the builder, create a typed query, add a root, build paths and predicates, choose the selection, optionally set ordering/grouping, and execute. A Criteria tree is programmatic query structure; it is not a promise of particular generated SQL or better performance than an equivalent HQL query.

Compare basic properties

Use builder operations suited to the property’s Java type:

cb.equal(customer.get("name"), "Alice")
cb.notEqual(customer.get("status"), CustomerStatus.INACTIVE)
cb.greaterThan(customer.get("creditLimit"), BigDecimal.valueOf(1000))
cb.lessThan(customer.get("createdAt"), cutoff)
cb.isNull(customer.get("deletedAt"))
cb.isNotNull(customer.get("email"))

Comparable values are appropriate for range comparisons, while like is for strings. For example:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
cb.like(customer.get("name"), "%smith%")
cb.equal(cb.lower(customer.get("email")), email.toLowerCase(Locale.ROOT))

In production search, account for wildcard characters: % and _ in a LIKE value have pattern meaning. The Criteria API offers like overloads with an escape character. Also consider that applying lower() to a column may affect ordinary index use; collation and functional-index behavior depend on the database.

SQL uses three-valued logic for NULL, so test nullness explicitly. Prefer cb.isNull(path) and cb.isNotNull(path) over cb.equal(path, null).

String paths or the static metamodel?

The short form is customer.get("status"). It is convenient for generic filters, but a misspelling is discovered at runtime, and Java may not infer the desired generic type. Where available, the generated static metamodel gives compile-time attribute checking and refactoring support:

customer.get(Customer_.status)

The Jakarta Criteria documentation generally recommends metamodel attributes rather than string-valued names when available. The trade-off is that metamodel classes must be generated and kept in the build. For a generic builder, strings can still be useful, but constrain them to known fields.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If string access needs a type Java cannot infer, provide an explicit type witness:

Path<Set<String>> nicknames = customer.<Set<String>>get("nicknames");

The Path API documentation notes this issue with string-based access. Prefer the static metamodel in ordinary application code where practical.

Navigate nested values and associations

For an embeddable or another appropriate single-valued path, chain get() calls:

Path<String> city = customer.get("billingAddress").get("city");
cq.where(cb.equal(city, "Boston"));

If the intermediate attribute is an entity association, use a join to express the relational navigation clearly. For example, if Customer.address refers to an Address entity:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Join<Customer, Address> address = customer.join("address");
cq.where(cb.equal(address.get("city"), "Boston"));

Inspect the entity mapping before choosing: an embedded value and an associated entity may look similar in Java but have different persistence semantics. The Criteria API exposes joins as paths, so you can continue with address.get("city"). See the Join API for join operations and join types.

Inner joins, left joins, and collection joins

A default association join is an inner join: customers without a matching association are excluded. Use a left join when those customers should remain in the result:

Join<Employee, Department> department =
    employee.join("department", JoinType.LEFT);

cq.where(cb.equal(department.get("name"), "Engineering"));

To find customers with open orders, join the collection:

Join<Customer, Order> order = customer.join("orders");
cq.select(customer)
  .distinct(true)
  .where(cb.equal(order.get("status"), OrderStatus.OPEN));

A collection join can yield multiple SQL rows for one root customer. distinct(true) requests distinct query results when duplicate roots are possible. If the question is only whether a matching collection element exists, an exists subquery can sometimes express the condition more naturally and avoid row multiplication.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Do not confuse join() with fetch(). A join is for navigation and query conditions; a fetch join is about loading an association along with selected entities. Collection fetch joins and pagination are a particularly risky combination: duplicates or in-memory pagination may result, depending on provider and query shape. For difficult paged entity-loading cases, a safer approach is often to page root IDs first, then fetch those entities in a second query.

Build optional filters safely

Criteria is useful when filters are added conditionally. Collect predicates, then apply them together:

List<Predicate> predicates = new ArrayList<>();

if (status != null) {
    predicates.add(cb.equal(customer.get("status"), status));
}
if (name != null && !name.isBlank()) {
    predicates.add(cb.like(
        cb.lower(customer.get("name")),
        "%" + name.toLowerCase(Locale.ROOT) + "%"
    ));
}
if (createdAfter != null) {
    predicates.add(cb.greaterThanOrEqualTo(
        customer.get("createdAt"), createdAfter
    ));
}

cq.select(customer)
  .where(predicates.toArray(Predicate[]::new));

Use cb.or(...) for alternatives, such as a name match or an email match; the default combination of the predicates passed to where is conjunction (AND). Values can be passed directly to builder methods as above, or represented with an explicit parameter:

ParameterExpression<String> nameParam = cb.parameter(String.class, "name");
cq.where(cb.equal(customer.get("name"), nameParam));

TypedQuery<Customer> typedQuery = entityManager.createQuery(cq);
typedQuery.setParameter("name", "Alice");

Do not concatenate values into HQL or SQL. Criteria keeps query structure separate from values; explicit parameters are useful when the same query structure is reused or parameter binding should be visible.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Never pass arbitrary request-supplied property names directly to get(). Whitelist permitted fields and define each field’s expected type and allowed operators. Otherwise a request can trigger runtime failures or expose attributes that should not be searchable. A map from an allowed external key to an expression factory is one possible implementation, but the whitelist must validate operator and value types as well as the property.

Decide explicitly what an empty IN collection means: no filter, no results, or invalid input. Do not let empty-list SQL behavior accidentally decide the endpoint’s semantics.

Collections and membership

For an entity collection, join to query properties of the related entity, as with customer.join("orders"). For an element collection, membership can be tested directly:

cq.where(cb.isMember(
    "vip",
    customer.<Set<String>>get("tags")
));

Path has APIs for singular attributes and collection, list, set, and map attributes. Choose the operation that matches the mapping; entity collections and collections of basic values are not interchangeable query shapes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Select an attribute, tuple, or entity

If callers need only one property, select that property and type the query accordingly:

CriteriaQuery<String> emailsQuery = cb.createQuery(String.class);
Root<Customer> customer = emailsQuery.from(Customer.class);
emailsQuery.select(customer.get("email"))
    .where(cb.equal(customer.get("status"), CustomerStatus.ACTIVE));

List<String> emails = entityManager.createQuery(emailsQuery).getResultList();

For several values, use a tuple:

CriteriaQuery<Tuple> tupleQuery = cb.createTupleQuery();
Root<Customer> customer = tupleQuery.from(Customer.class);
tupleQuery.multiselect(
    customer.get("id").alias("id"),
    customer.get("name").alias("name"),
    customer.get("email").alias("email")
);

for (Tuple row : entityManager.createQuery(tupleQuery).getResultList()) {
    Long id = row.get("id", Long.class);
    String name = row.get("name", String.class);
}

Use tuples for flexible multi-column results, a constructor/DTO projection for a stable response shape, or entity selection when managed entities and their persistence behavior are needed. Hibernate’s current user guide covers typed Criteria queries, selections, tuples, joins, paths, parameters, and grouping.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Sort, paginate, and count

Order by one or more properties with orderBy:

cq.orderBy(
    cb.asc(customer.get("lastName")),
    cb.asc(customer.get("firstName"))
);

For descending order, use cb.desc(path). Null placement can be database- and provider-sensitive; if it is a requirement, make the behavior explicit with an expression or a clearly identified Hibernate/database-specific feature rather than assuming portable defaults.

Pagination is applied to the executable query, not to the Criteria tree. Pair it with a deterministic order, including a unique tie-breaker:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
cq.orderBy(
    cb.asc(customer.get("createdAt")),
    cb.asc(customer.get("id"))
);

TypedQuery<Customer> query = entityManager.createQuery(cq);
query.setFirstResult(page * pageSize);
query.setMaxResults(pageSize);
List<Customer> results = query.getResultList();

Without stable ordering, rows can shift between pages because the database does not guarantee a particular order absent an ordering clause, and concurrent changes can also affect page contents.

Build a separate count query for totals, repeating the same filters:

CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
Root<Customer> customer = countQuery.from(Customer.class);
countQuery.select(cb.count(customer))
    .where(cb.equal(customer.get("status"), CustomerStatus.ACTIVE));
Long total = entityManager.createQuery(countQuery).getSingleResult();

If a collection join can duplicate the root, use cb.countDistinct(customer) when the desired total is distinct customers. Count queries should reflect the filters while avoiding joins that inflate the count unnecessarily.

Common errors and how to fix them

  • “Could not resolve attribute”: Check spelling, use the Java persistent attribute rather than a column name, and confirm the attribute is mapped under the entity’s field/property access strategy.
  • Type inference or compilation errors: Use the static metamodel, or provide a type witness such as customer.<LocalDate>get("createdAt").
  • Duplicate root entities: A collection join may multiply rows. Use distinct(true) where appropriate, consider an exists subquery, and make the count query distinct if it counts roots.
  • Missing entities unexpectedly: An inner join removes rows without a matching association. Select JoinType.LEFT if unassociated roots must remain; also place conditions carefully, since a restrictive condition on the joined side can effectively eliminate unmatched rows.
  • Wrong persistence imports: javax.persistence.criteria.CriteriaQuery and jakarta.persistence.criteria.CriteriaQuery are different APIs. Use imports matching the application’s persistence API and Hibernate generation. Modern Hibernate/Jakarta applications use jakarta.*.
  • Unexpected results after editing a query: Build the Criteria tree before creating/executing its query. Hibernate 6 changed handling around mutation of Criteria trees passed to the provider; do not rely on mutating a tree after query creation without verifying the exact version and configuration. See the migration guide.
  • Pagination behaves strangely with fetched collections: Avoid casually combining collection fetch joins with pagination; use a two-query ID-page-then-fetch approach when needed and check the behavior for the Hibernate version in use.

When Criteria is the right tool

Use Criteria when filters are optional, the query shape changes at runtime, or reusable predicate builders are valuable. For a fixed business query, HQL may be easier for a team to read. Repository specifications or a query DSL can help when an application needs a shared composition model. Use native SQL when database-specific features or exact SQL control are essential. Hibernate’s quick guide describes Criteria as programmatically constructed queries and notes HQL’s capabilities in Hibernate 6 and later. Criteria is not inherently faster; generated query semantics, mappings, indexes, database plans, and provider version determine performance.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Hibernate version notes

For Hibernate ORM 6 and later, use Jakarta imports and the standard execution path entityManager.createQuery(criteriaQuery) for ordinary selection queries. Hibernate 6 introduced a Semantic Query Model shared by HQL and Criteria translation; see the Hibernate 6 release information. Hibernate also has native Criteria-related extensions under org.hibernate.query.criteria, but those are Hibernate-specific, not portable Jakarta Persistence code. Avoid mixing legacy org.hibernate.Criteria examples or javax.* imports into a modern Jakarta application. Verify the persistence API and Java requirements against the Hibernate version selected for your project rather than copying an unverified dependency version.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.