hibernate/hibernate-orm · error · UnsupportedOperationException

Follow-on locking for subqueries is not supported

Error message

Follow-on locking for subqueries is not supported

What it means

When pessimistic lock options apply to a query part, the translator picks a strategy via determineLockingStrategy; FOLLOW_ON means Hibernate issues the SELECT unlocked and then locks rows with follow-up SELECT ... FOR UPDATE statements. Follow-on locking only makes sense for a root query — inside a subquery there is nothing to follow up on — so a FOLLOW_ON strategy on a non-root QuerySpec throws UnsupportedOperationException.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java:1867

	protected void visitForUpdateClause(QuerySpec querySpec) {
		if ( querySpec != lockingTarget ) {
			// this check is intended to help with Oracle, though
			// really any dialect translator could leverage this
			return;
		}
		if ( lockOptions != null && lockOptions.getLockMode().isPessimistic() ) {
			final LockStrategy lockStrategy = determineLockingStrategy( querySpec, lockOptions.getFollowOnStrategy() );
			switch ( lockStrategy ) {
				case CLAUSE: {
					lockingClauseStrategy.render( getSqlAppender() );
					break;
				}
				case FOLLOW_ON: {
					if ( querySpec.isRoot() ) {
						lockOptions = null;
					}
					else {
						throw new UnsupportedOperationException( "Follow-on locking for subqueries is not supported" );
					}
					break;
				}
				case NONE: {
					// nothing to do
					break;
				}
			}
		}
	}

	protected LockMode getEffectiveLockMode() {
		if ( getLockOptions() == null ) {
			return LockMode.NONE;
		}
		else {
			final QueryPart currentQueryPart = getQueryPartStack().getCurrent();
			if ( currentQueryPart == null || !currentQueryPart.isRoot() ) {

View on GitHub (pinned to fad1729dce)

Solutions

  1. Do not force follow-on locking: remove LockOptions.setFollowOnLocking(true) / the follow-on query hint for this query.
  2. Restructure the query so the locked SELECT is the root statement (move the union/subquery out or lock a simpler query).
  3. Drop the pessimistic lock on the subquery-containing query and lock entities separately with EntityManager.find(..., LockModeType.PESSIMISTIC_WRITE) or a follow-up locking query you control.
  4. Upgrade hibernate-core — follow-on strategy detection for nested query parts has been refined across 6.x releases.

Example fix

// before — forcing follow-on locking on a query with subqueries
List<Order> l = session.createQuery("select o from Order o where o.total > (select avg(o2.total) from Order o2)", Order.class)
    .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE).setFollowOnLocking(true))
    .list();

// after — lock in a second, root-level statement you control
List<Long> ids = session.createQuery("select o.id from Order o where o.total > (select avg(o2.total) from Order o2)", Long.class).list();
List<Order> l = session.byId(Order.class).with(LockOptions.UPGRADE).loadMulti(ids);
Defensive patterns

Strategy: fallback

Validate before calling

org.hibernate.dialect.Dialect d = sessionFactory.getJdbcServices().getDialect();
boolean followOnDefault = d.getWriteRowLockStrategy() == org.hibernate.LockMode.PESSIMISTIC_WRITE && !d.supportsOuterJoinForUpdate();
if (followOnDefault && queryContainsSubqueries(hql)) {
    lockOptions.setFollowOnLocking(false); // avoid forcing follow-on on subqueries
}

Try / catch

try { query.setLockOptions(lockOptions).list(); }
catch (UnsupportedOperationException e) {
    if (e.getMessage().equals("Follow-on locking for subqueries is not supported")) {
        // re-run unlocked, then lock rows in a dedicated root-level statement
        List<Long> ids = queryUnlockedIds();
        results = lockByIds(ids);
    } else { throw e; }
}

Prevention

When it happens

Trigger: Pessimistic locking (LockMode.PESSIMISTIC_WRITE/FORCE_INCREMENT, or LockOptions with follow-on forced via LockOptions.setFollowOnLocking / a dialect defaulting to follow-on) applied to a query where the locked part is a subquery: pagination-wrapped query parts, set-operation arms, or secondary query specs.

Common situations: Setting hibernate.query.followOnLocking to true globally (e.g., for dialects like SQL Server/Sybase where it is the default) and then locking a query containing subqueries/unions; forcing follow-on locking via LockOptions#setFollowOnLocking(Boolean.TRUE); upgrades where a dialect's default write-row-lock strategy changed.

Related errors


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