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

  1. Set an explicit selection: cq.select(personRoot) or cq.multiselect(personRoot, addressRoot)
  2. Drop the extra root and use a join instead: personRoot.join("address")
  3. Use cq.select(cb.construct(Dto.class, ...)) for DTO projections when multiple roots are needed
  4. 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

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


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/98d4e022cc1d4fe3. Report an issue: GitHub.