hibernate/hibernate-orm · error · IllegalArgumentException
Criteria has multiple query roots
Error message
Criteria has multiple query roots
What it means
Thrown as IllegalArgumentException by SqmUtil.validateCriteriaQueryStructure when a CriteriaQuery part has an empty select clause but more than one root in its from clause. With nothing explicitly selected, Hibernate implicitly selects the single root; two or more roots make the implicit selection ambiguous (which root is the result?), so validation fails and demands an explicit select/multiselect.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java:1035
return (SqmPredicate) restriction.toPredicate( root, sqmStatement.nodeBuilder() );
}
public static void validateCriteriaQuery(SqmQueryPart<?> queryPart) {
validateCriteriaQueryStructure( queryPart );
SqmCriteriaRootValidator.validate( queryPart );
}
private static void validateCriteriaQueryStructure(SqmQueryPart<?> queryPart) {
if ( queryPart instanceof SqmQuerySpec<?> sqmQuerySpec ) {
final var selectClause = sqmQuerySpec.getSelectClause();
if ( selectClause.getSelections().isEmpty() ) {
// make sure there is at least one root
final var sqmRoots = sqmQuerySpec.getFromClause().getRoots();
if ( sqmRoots == null || sqmRoots.isEmpty() ) {
throw new IllegalArgumentException( "Criteria did not define any query roots" );
}
if ( sqmRoots.size() != 1 ) {
throw new IllegalArgumentException( "Criteria has multiple query roots" );
}
}
}
else if ( queryPart instanceof SqmQueryGroup<?> queryGroup ) {
for ( var part : queryGroup.getQueryParts() ) {
validateCriteriaQueryStructure( part );
}
}
else {
assert false;
}
}
public static void validateCriteriaTree(SqmDeleteStatement<?> statement) {
SqmCriteriaRootValidator.validate( statement );
}
public static void validateCriteriaTree(SqmUpdateStatement<?> statement) {View on GitHub (pinned to fad1729dce)
Solutions
- Set an explicit selection: cq.select(personRoot) or cq.multiselect(personRoot, addressRoot)
- Drop the extra root and use a join instead: personRoot.join("address")
- Use cq.select(cb.construct(Dto.class, ...)) for DTO projections when multiple roots are needed
- Verify every dynamic path that adds roots also adds a matching select
Example fix
// before CriteriaQuery<Person> cq = cb.createQuery(Person.class); Root<Person> p = cq.from(Person.class); cq.from(Address.class); // second root, nothing selected // after CriteriaQuery<Person> cq = cb.createQuery(Person.class); Root<Person> p = cq.from(Person.class); cq.select(p);
Defensive patterns
Strategy: validation
Validate before calling
static <X> CriteriaQuery<X> requireSingleImplicitRoot(CriteriaQuery<X> cq) {
if (cq.getSelection() == null && cq.getRoots().size() > 1) {
throw new IllegalStateException(
"Multiple roots require an explicit select/multiselect");
}
return cq;
} Try / catch
try {
return em.createQuery(cq).getResultList();
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("multiple query roots")) {
cq.select(cq.getRoots().get(0)); // or multiselect(...) for both roots
return em.createQuery(cq).getResultList();
}
throw e;
} Prevention
- Set select()/multiselect() in the same block that adds a second root
- Prefer joins over additional roots for related entities
- Unit-test every dynamic branch that adds roots
When it happens
Trigger: cq.from(Person.class) followed by cq.from(Address.class) with no cq.select(...) or cq.multiselect(...); joining two entities as separate roots instead of a join and forgetting to project; dynamic builders that add roots per active filter but only set select() in some branches.
Common situations: Translating an HQL 'from A a, B b' style query into criteria without adding the select; refactoring a join into a second root; filter-driven criteria where the select statement is appended at the end and a branch returns early.
Related errors
- Criteria did not define any query roots
- Could not resolve entity class '{}'
- MappedSuperclassType cannot be used to create an SqmPath - t
- CTE joins can not be treated
- Lateral joins can only be left or inner. Illegal join type:
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/98d4e022cc1d4fe3.
Report an issue: GitHub.