hibernate/hibernate-orm · error · IllegalQueryOperationException

Locking with joins is not supported

Error message

Locking with joins is not supported

What it means

For aggregate-typed columns (SQL STRUCT/aggregate and JSON mappings), AggregateColumnWriteExpression keeps parallel arrays - selectableMappings[] and valueExpressions[] - and getValueExpression(selectableMapping) resolves by instance identity (==, not equals). IllegalArgumentException means the SelectableMapping you asked with is not one of the mappings this aggregate write expression was built from, i.e. the caller used a mapping from a different part of the mapping model.

Source

Thrown at hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java:242

		else {
			super.visitQuerySpec( querySpec );
		}
	}

	protected boolean shouldEmulateFetchClause(QueryPart queryPart) {
		// Check if current query part is already row numbering to avoid infinite recursion
		return useOffsetFetchClause( queryPart ) && getQueryPartForRowNumbering() != queryPart
				&& getDialect().supportsWindowFunctions() && !isRowsOnlyFetchClauseType( queryPart );
	}

	@Override
	protected LockStrategy determineLockingStrategy(QuerySpec querySpec, Locking.FollowOn followOnStrategy) {
		final LockStrategy lockStrategy = super.determineLockingStrategy( querySpec, followOnStrategy );
		final LockingClauseStrategy lockingClauseStrategy = getLockingClauseStrategy();
		if ( lockingClauseStrategy != null && lockingClauseStrategy.containsJoins() ) {
			// Altibase does not allow FOR UPDATE when the query also contains joins.
			if ( followOnStrategy == Locking.FollowOn.DISALLOW ) {
				throw new IllegalQueryOperationException( "Locking with joins is not supported" );
			}
			else if ( followOnStrategy == Locking.FollowOn.IGNORE ) {
				return LockStrategy.NONE;
			}
			return LockStrategy.FOLLOW_ON;
		}
		return lockStrategy;
	}

	@Override
	protected void renderPartitionItem(Expression expression) {
		if ( expression instanceof Literal ) {
			appendSql( "'0' || '0'" );
		}
		else if ( expression instanceof Summarization ) {
			// This could theoretically be emulated by rendering all grouping variations of the query and
			// connect them via union all but that's probably pretty inefficient and would have to happen
			// on the query spec level

View on GitHub (pinned to fad1729dce)

Solutions

  1. Always obtain the SelectableMapping from the same aggregate mapping that produced the write expression: iterate mapping.forEachSelectable(...) and pass exactly those instances.
  2. Never cache SelectableMapping instances across SessionFactory lifecycles - re-fetch them from the current mapping.
  3. Align custom aggregate-column rendering code with the current Hibernate version's AggregateColumnWriteExpression API.
  4. Upgrade Hibernate; if the call comes from core rendering, report HHH with the aggregate mapping.

Example fix

// before: asking with a mapping from another source
Expression e = writeExpr.getValueExpression( foreignMapping.getSelectable() );

// after: resolve through the aggregate's own selectables
final Expression[] found = new Expression[1];
aggregateMapping.forEachSelectable( (i, sel) -> {
    if ( sel == candidateSelectable ) found[0] = writeExpr.getValueExpression( sel );
} );
Defensive patterns

Strategy: type-guard

Validate before calling

// Verify the candidate mapping belongs to this aggregate write expression before asking
boolean owned = false;
aggregateMapping.forEachSelectable( (i, sel) -> { if ( sel == candidateSelectable ) owned = true; } );
if ( !owned ) {
    throw new IllegalArgumentException("candidateSelectable is not part of this aggregate mapping");
}

Type guard

// Java: only resolve value expressions through the aggregate's own selectables
static Expression valueExpressionFor(AggregateColumnWriteExpression write,
                                     AggregateMapping owner,
                                     SelectableMapping candidate) {
    final boolean[] found = { false };
    owner.forEachSelectable( (i, sel) -> { if ( sel == candidate ) found[0] = true; } );
    if ( !found[0] ) {
        return null; // caller falls back / reports instead of risking IllegalArgumentException
    }
    return write.getValueExpression( candidate );
}

Try / catch

try {
    return writeExpr.getValueExpression( selectable );
} catch ( IllegalArgumentException e ) {
    if ( e.getMessage() != null && e.getMessage().startsWith("Couldn't find value expression") ) {
        // re-resolve the selectable from the current aggregate mapping and retry
        return writeExpr.getValueExpression( resolveFreshSelectable( aggregateMapping, selectable ) );
    }
    throw e;
}

Prevention

When it happens

Trigger: Custom JdbcType/SqlAstTranslator or aggregate-mapping code calling getValueExpression with a SelectableMapping obtained elsewhere (another aggregate part, a rebuilt mapping instance) instead of one delivered by this aggregate mapping's forEachSelectable; also possible after metadata enhancement creates duplicate mapping instances for the same column.

Common situations: Projects with custom aggregate/JSON JdbcTypes after a Hibernate upgrade changed how selectable mapping instances are created and cached; code that caches SelectableMappings across SessionFactory rebuilds; mixing mapping instances from two metamodels.

Related errors


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