{"record":{"id":"769ece2e259a17a2","repo":"hibernate/hibernate-orm","slug":"locking-with-group-by-is-not-supported","errorCode":null,"errorMessage":"Locking with GROUP BY is not supported","messagePattern":"Locking with GROUP BY is not supported","errorType":"exception","errorClass":"IllegalQueryOperationException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java","lineNumber":1932,"sourceCode":"\t\treturn AggregateFunctionChecker.hasAggregateFunctions( querySpec );\n\t}\n\n\tprotected LockStrategy determineLockingStrategy(\n\t\t\tQuerySpec querySpec,\n\t\t\tLocking.FollowOn followOnStrategy) {\n\t\tif ( followOnStrategy == Locking.FollowOn.FORCE ) {\n\t\t\treturn LockStrategy.FOLLOW_ON;\n\t\t}\n\n\t\tif ( !querySpec.isRoot() ) {\n\t\t\tfollowOnStrategy = Locking.FollowOn.ALLOW;\n\t\t}\n\n\t\tLockStrategy strategy = LockStrategy.CLAUSE;\n\n\t\tif ( !querySpec.getGroupByClauseExpressions().isEmpty() ) {\n\t\t\tif ( followOnStrategy == Locking.FollowOn.DISALLOW ) {\n\t\t\t\tthrow new IllegalQueryOperationException( \"Locking with GROUP BY is not supported\" );\n\t\t\t}\n\t\t\telse if ( followOnStrategy == Locking.FollowOn.IGNORE ) {\n\t\t\t\treturn LockStrategy.NONE;\n\t\t\t}\n\t\t\tstrategy = LockStrategy.FOLLOW_ON;\n\t\t}\n\n\t\tif ( querySpec.getHavingClauseRestrictions() != null ) {\n\t\t\tif ( followOnStrategy == Locking.FollowOn.DISALLOW ) {\n\t\t\t\tthrow new IllegalQueryOperationException( \"Locking with HAVING is not supported\" );\n\t\t\t}\n\t\t\telse if ( followOnStrategy == Locking.FollowOn.IGNORE ) {\n\t\t\t\treturn LockStrategy.NONE;\n\t\t\t}\n\t\t\tstrategy = LockStrategy.FOLLOW_ON;\n\t\t}\n\n\t\tif ( querySpec.getSelectClause().isDistinct() ) {","sourceCodeStart":1914,"sourceCodeEnd":1950,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java#L1914-L1950","documentation":"determineLockingStrategy decides between a native FOR UPDATE clause, follow-on locking, or no locking. Databases cannot combine SELECT ... FOR UPDATE with GROUP BY sensibly, so a non-empty group-by clause forces follow-on locking; if the follow-on strategy is DISALLOW (the JPA pessimistic-lock default, where locking must happen in the same query), it throws IllegalQueryOperationException instead.","triggerScenarios":"A query with GROUP BY executed with a pessimistic lock mode whose follow-on strategy is DISALLOW — e.g., em.createQuery(...).setLockMode(LockModeType.PESSIMISTIC_WRITE) on 'select e.dept, count(e) from Employee e group by e.dept'.","commonSituations":"Applying JPA pessimistic lock modes to reporting/aggregation queries; adding @Lock(PESSIMISTIC_WRITE) to repository methods that aggregate; blanket lock-mode aspects (e.g., Spring @Lock annotations) hitting grouped queries.","solutions":["Allow follow-on locking for this query so Hibernate locks rows afterwards: query.setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE).setFollowOnLocking(true)) or the equivalent hint.","Drop the lock from the aggregate query — grouping queries rarely benefit from row locks on aggregates.","Lock the underlying rows in a separate root-level query (select ids ... for update) and then run the aggregate."],"exampleFix":"// before — JPA pessimistic lock (follow-on DISALLOW) on a grouped query\nList<Tuple> r = em.createQuery(\"select e.dept, count(e) from Employee e group by e.dept\", Tuple.class)\n    .setLockMode(LockModeType.PESSIMISTIC_WRITE).getResultList();\n\n// after — explicitly allow follow-on locking via Hibernate LockOptions\nList<Tuple> r = em.createQuery(\"select e.dept, count(e) from Employee e group by e.dept\", Tuple.class)\n    .unwrap(org.hibernate.query.Query.class)\n    .setLockOptions(new org.hibernate.LockOptions(org.hibernate.LockMode.PESSIMISTIC_WRITE)\n        .setFollowOnLocking(true))\n    .getResultList();","handlingStrategy":"fallback","validationCode":"boolean grouped = hql.toLowerCase().contains(\" group by\");\nif (grouped && lockMode != null && lockMode.isPessimistic()) {\n    // grouping forces follow-on locking; JPA lock modes disallow it\n    lockOptions.setFollowOnLocking(true);\n}","typeGuard":null,"tryCatchPattern":"try { em.createQuery(hql).setLockMode(LockModeType.PESSIMISTIC_WRITE).getResultList(); }\ncatch (org.hibernate.query.IllegalQueryOperationException e) {\n    if (e.getMessage().equals(\"Locking with GROUP BY is not supported\")) {\n        em.createQuery(hql).unwrap(org.hibernate.query.Query.class)\n          .setLockOptions(new org.hibernate.LockOptions(org.hibernate.LockMode.PESSIMISTIC_WRITE).setFollowOnLocking(true))\n          .getResultList();\n    } else { throw e; }\n}","preventionTips":["Do not apply pessimistic lock modes to aggregate/grouped queries by default (no blanket @Lock aspects).","Prefer locking ids in a dedicated simple query, then aggregating unlocked.","Know that JPA pessimistic lock modes imply follow-on locking is disallowed."],"tags":["hibernate","orm","pessimistic-locking","group-by","jpa"],"backgroundTag":"pessimistic-locking-unsupported","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}