{"record":{"id":"e51e990bd2f2c98e","repo":"hibernate/hibernate-orm","slug":"follow-on-locking-for-subqueries-is-not-supported","errorCode":null,"errorMessage":"Follow-on locking for subqueries is not supported","messagePattern":"Follow-on locking for subqueries is not supported","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java","lineNumber":1867,"sourceCode":"\tprotected void visitForUpdateClause(QuerySpec querySpec) {\n\t\tif ( querySpec != lockingTarget ) {\n\t\t\t// this check is intended to help with Oracle, though\n\t\t\t// really any dialect translator could leverage this\n\t\t\treturn;\n\t\t}\n\t\tif ( lockOptions != null && lockOptions.getLockMode().isPessimistic() ) {\n\t\t\tfinal LockStrategy lockStrategy = determineLockingStrategy( querySpec, lockOptions.getFollowOnStrategy() );\n\t\t\tswitch ( lockStrategy ) {\n\t\t\t\tcase CLAUSE: {\n\t\t\t\t\tlockingClauseStrategy.render( getSqlAppender() );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase FOLLOW_ON: {\n\t\t\t\t\tif ( querySpec.isRoot() ) {\n\t\t\t\t\t\tlockOptions = null;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthrow new UnsupportedOperationException( \"Follow-on locking for subqueries is not supported\" );\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase NONE: {\n\t\t\t\t\t// nothing to do\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected LockMode getEffectiveLockMode() {\n\t\tif ( getLockOptions() == null ) {\n\t\t\treturn LockMode.NONE;\n\t\t}\n\t\telse {\n\t\t\tfinal QueryPart currentQueryPart = getQueryPartStack().getCurrent();\n\t\t\tif ( currentQueryPart == null || !currentQueryPart.isRoot() ) {","sourceCodeStart":1849,"sourceCodeEnd":1885,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java#L1849-L1885","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Do not force follow-on locking: remove LockOptions.setFollowOnLocking(true) / the follow-on query hint for this query.","Restructure the query so the locked SELECT is the root statement (move the union/subquery out or lock a simpler query).","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.","Upgrade hibernate-core — follow-on strategy detection for nested query parts has been refined across 6.x releases."],"exampleFix":"// before — forcing follow-on locking on a query with subqueries\nList<Order> l = session.createQuery(\"select o from Order o where o.total > (select avg(o2.total) from Order o2)\", Order.class)\n    .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE).setFollowOnLocking(true))\n    .list();\n\n// after — lock in a second, root-level statement you control\nList<Long> ids = session.createQuery(\"select o.id from Order o where o.total > (select avg(o2.total) from Order o2)\", Long.class).list();\nList<Order> l = session.byId(Order.class).with(LockOptions.UPGRADE).loadMulti(ids);","handlingStrategy":"fallback","validationCode":"org.hibernate.dialect.Dialect d = sessionFactory.getJdbcServices().getDialect();\nboolean followOnDefault = d.getWriteRowLockStrategy() == org.hibernate.LockMode.PESSIMISTIC_WRITE && !d.supportsOuterJoinForUpdate();\nif (followOnDefault && queryContainsSubqueries(hql)) {\n    lockOptions.setFollowOnLocking(false); // avoid forcing follow-on on subqueries\n}","typeGuard":null,"tryCatchPattern":"try { query.setLockOptions(lockOptions).list(); }\ncatch (UnsupportedOperationException e) {\n    if (e.getMessage().equals(\"Follow-on locking for subqueries is not supported\")) {\n        // re-run unlocked, then lock rows in a dedicated root-level statement\n        List<Long> ids = queryUnlockedIds();\n        results = lockByIds(ids);\n    } else { throw e; }\n}","preventionTips":["Never set followOnLocking=true globally; scope it per query.","Keep locked queries simple and root-level; lock by ids for complex reads.","Review global lock settings (hibernate.query.followOnLocking) after dialect upgrades."],"tags":["hibernate","orm","pessimistic-locking","follow-on-locking","subquery"],"backgroundTag":"pessimistic-locking-unsupported","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}