hibernate/hibernate-orm · error · IllegalArgumentException

Expected table group with table joins to have an entity type

Error message

Expected table group with table joins to have an entity typed model part but got: 

What it means

To emit a locking clause for a table group with joins, the strategy must resolve an EntityPersister from the group's model part. Only three shapes are supported: an EntityPersister itself, a PluralAttributeMapping (via its element persister), or an EntityAssociationMapping (via the associated entity). Any other model part — e.g. a table group backed by a values clause, CTE, or function source — hits the else branch and throws IllegalArgumentException naming what was found. This is an internal capability gap of the locking translator.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java:288

					return tableMapping;
				}
			}
		}
		throw new IllegalArgumentException( "Couldn't find subclass index for joined table reference " + joinedTableReference );
	}

	private EntityPersister determineEntityPersister(ModelPartContainer modelPart) {
		if ( modelPart instanceof EntityPersister entityPersister ) {
			return entityPersister;
		}
		else if ( modelPart instanceof PluralAttributeMapping pluralAttributeMapping ) {
			return pluralAttributeMapping.getCollectionDescriptor().getElementPersister();
		}
		else if ( modelPart instanceof EntityAssociationMapping entityAssociationMapping ) {
			return entityAssociationMapping.getAssociatedEntityMappingType().getEntityPersister();
		}
		else {
			throw new IllegalArgumentException( "Expected table group with table joins to have an entity typed model part but got: " + modelPart );
		}
	}

	private String[] determineKeyColumnNames(TableGroup tableGroup) {
		if ( tableGroup instanceof LockingTableGroup lockingTableGroup ) {
			return extractColumnNames( lockingTableGroup.getKeyColumnMappings() );
		}
		else if ( tableGroup.getModelPart() instanceof EntityPersister entityPersister ) {
			return entityPersister.getIdentifierColumnNames();
		}
		else if ( tableGroup.getModelPart() instanceof PluralAttributeMapping pluralAttributeMapping ) {
			return extractColumnNames( pluralAttributeMapping.getKeyDescriptor() );
		}
		else if ( tableGroup.getModelPart() instanceof EntityAssociationMapping entityAssociationMapping ) {
			return extractColumnNames( entityAssociationMapping.getAssociatedEntityMappingType().getIdentifierMapping() );
		}
		else {
			throw new AssertionFailure( "Unable to determine columns for locking" );

View on GitHub (pinned to fad1729dce)

Solutions

  1. Remove the pessimistic lock from that part of the query — lock only entity roots
  2. Restructure the query so the locked table group corresponds to a real entity mapping
  3. For read-only derived tables, mark them read-only and keep locking on the entity side
  4. Upgrade Hibernate and check the release notes — unsupported-model-part cases in locking have been progressively handled

Example fix

// before
List<Order> rows = em.createQuery(
    "select o from Order o join OrderValues v on v.orderId = o.id where v.amount > :a",
    Order.class)
    .setLockMode(LockModeType.PESSIMISTIC_WRITE) // v has non-entity model part
    .getResultList();

// after
List<Order> rows = em.createQuery(
    "select o from Order o where o.total > :a", Order.class)
    .setLockMode(LockModeType.PESSIMISTIC_WRITE)
    .getResultList();
Defensive patterns

Strategy: validation

Validate before calling

// apply pessimistic locks only to entity roots
for (Map.Entry<String, LockModeType> e : requestedLocks.entrySet()) {
    SqmRoot<?> root = (SqmRoot<?>) query.getSqmQuery().getRoot(e.getKey());
    if (root == null) {
        throw new IllegalArgumentException("lock alias " + e.getKey() + " is not an entity root");
    }
}

Try / catch

try {
    return query.getResultList();
}
catch (IllegalArgumentException e) {
    if (e.getMessage() != null && e.getMessage().contains("entity typed model part")) {
        // unsupported construct under locking: drop the lock or restructure the query
        return unlockedQuery().getResultList();
    }
    throw e;
}

Prevention

When it happens

Trigger: A query with pessimistic locking (especially FOR UPDATE OF on dialects that render table-specific clauses) whose table group is derived from a non-entity source: join to a VALUES/CTE construct, a @Subselect/mapped subselect entity usage, or a generateSeries/values-valued path.

Common situations: Exotic reporting or batch queries that mix CTE/values joins with LockModeType.PESSIMISTIC_WRITE; frameworks generating synthetic table groups; mapped views (@Subselect) combined with lock hints.

Related errors


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