{"record":{"id":"98af9f0ff26dede0","repo":"hibernate/hibernate-orm","slug":"expected-table-group-with-table-joins-to-have-an-e","errorCode":null,"errorMessage":"Expected table group with table joins to have an entity typed model part but got: ","messagePattern":"Expected table group with table joins to have an entity typed model part but got: ","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java","lineNumber":288,"sourceCode":"\t\t\t\t\treturn tableMapping;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthrow new IllegalArgumentException( \"Couldn't find subclass index for joined table reference \" + joinedTableReference );\n\t}\n\n\tprivate EntityPersister determineEntityPersister(ModelPartContainer modelPart) {\n\t\tif ( modelPart instanceof EntityPersister entityPersister ) {\n\t\t\treturn entityPersister;\n\t\t}\n\t\telse if ( modelPart instanceof PluralAttributeMapping pluralAttributeMapping ) {\n\t\t\treturn pluralAttributeMapping.getCollectionDescriptor().getElementPersister();\n\t\t}\n\t\telse if ( modelPart instanceof EntityAssociationMapping entityAssociationMapping ) {\n\t\t\treturn entityAssociationMapping.getAssociatedEntityMappingType().getEntityPersister();\n\t\t}\n\t\telse {\n\t\t\tthrow new IllegalArgumentException( \"Expected table group with table joins to have an entity typed model part but got: \" + modelPart );\n\t\t}\n\t}\n\n\tprivate String[] determineKeyColumnNames(TableGroup tableGroup) {\n\t\tif ( tableGroup instanceof LockingTableGroup lockingTableGroup ) {\n\t\t\treturn extractColumnNames( lockingTableGroup.getKeyColumnMappings() );\n\t\t}\n\t\telse if ( tableGroup.getModelPart() instanceof EntityPersister entityPersister ) {\n\t\t\treturn entityPersister.getIdentifierColumnNames();\n\t\t}\n\t\telse if ( tableGroup.getModelPart() instanceof PluralAttributeMapping pluralAttributeMapping ) {\n\t\t\treturn extractColumnNames( pluralAttributeMapping.getKeyDescriptor() );\n\t\t}\n\t\telse if ( tableGroup.getModelPart() instanceof EntityAssociationMapping entityAssociationMapping ) {\n\t\t\treturn extractColumnNames( entityAssociationMapping.getAssociatedEntityMappingType().getIdentifierMapping() );\n\t\t}\n\t\telse {\n\t\t\tthrow new AssertionFailure( \"Unable to determine columns for locking\" );","sourceCodeStart":270,"sourceCodeEnd":306,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardLockingClauseStrategy.java#L270-L306","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove the pessimistic lock from that part of the query — lock only entity roots","Restructure the query so the locked table group corresponds to a real entity mapping","For read-only derived tables, mark them read-only and keep locking on the entity side","Upgrade Hibernate and check the release notes — unsupported-model-part cases in locking have been progressively handled"],"exampleFix":"// before\nList<Order> rows = em.createQuery(\n    \"select o from Order o join OrderValues v on v.orderId = o.id where v.amount > :a\",\n    Order.class)\n    .setLockMode(LockModeType.PESSIMISTIC_WRITE) // v has non-entity model part\n    .getResultList();\n\n// after\nList<Order> rows = em.createQuery(\n    \"select o from Order o where o.total > :a\", Order.class)\n    .setLockMode(LockModeType.PESSIMISTIC_WRITE)\n    .getResultList();","handlingStrategy":"validation","validationCode":"// apply pessimistic locks only to entity roots\nfor (Map.Entry<String, LockModeType> e : requestedLocks.entrySet()) {\n    SqmRoot<?> root = (SqmRoot<?>) query.getSqmQuery().getRoot(e.getKey());\n    if (root == null) {\n        throw new IllegalArgumentException(\"lock alias \" + e.getKey() + \" is not an entity root\");\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    return query.getResultList();\n}\ncatch (IllegalArgumentException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"entity typed model part\")) {\n        // unsupported construct under locking: drop the lock or restructure the query\n        return unlockedQuery().getResultList();\n    }\n    throw e;\n}","preventionTips":["Do not combine CTE/values/subselect table groups with pessimistic locking","Lock only entity roots; mark derived/read-only joins as such","Re-run exotic reporting queries without locks after Hibernate upgrades"],"tags":["locking","sql-ast","query-api","sql-generation"],"backgroundTag":"unsupported-query-construct","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}