{"record":{"id":"a34363f7b59c278f","repo":"hibernate/hibernate-orm","slug":"collection-is-not-an-association-role","errorCode":null,"errorMessage":"Collection is not an association: ${role}","messagePattern":"Collection is not an association: (.+?)","errorType":"exception","errorClass":"MappingException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/type/CollectionType.java","lineNumber":453,"sourceCode":"\tpublic Joinable getAssociatedJoinable(SessionFactoryImplementor factory)\n\t\t\tthrows MappingException {\n\t\treturn (Joinable) getPersister( factory );\n\t}\n\n\t@Override\n\tpublic boolean isModified(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic String getAssociatedEntityName(SessionFactoryImplementor factory)\n\t\t\tthrows MappingException {\n\t\tfinal var persister = getPersister( factory );\n\t\tif ( persister.getElementType().isEntityType() ) {\n\t\t\treturn persister.getElementPersister().getEntityName();\n\t\t}\n\t\telse {\n\t\t\tthrow new MappingException( \"Collection is not an association: \" + persister.getRole() );\n\t\t}\n\t}\n\n\t/**\n\t * Replace the elements of a collection with the elements of another collection.\n\t *\n\t * @param original The 'source' of the replacement elements (where we copy from)\n\t * @param target The target of the replacement elements (where we copy to)\n\t * @param owner The owner of the collection being merged\n\t * @param copyCache The map of elements already replaced.\n\t * @param session The session from which the merge event originated.\n\t * @return The merged collection.\n\t */\n\t@SuppressWarnings({\"rawtypes\", \"unchecked\"})\n\tpublic Object replaceElements(\n\t\t\tObject original,\n\t\t\tObject target,\n\t\t\tObject owner,","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java#L435-L471","documentation":"CollectionType.getAssociatedEntityName (CollectionType.java:445-455) returns the element entity name only when the collection's element type is an entity; for collections of basic values or embeddables (@ElementCollection) it throws MappingException 'Collection is not an association: <role>'. The role string is 'EntityFQN.propertyName', which names the exact collection that was treated as a to-many entity association.","triggerScenarios":"Operations that treat a collection role as an entity association: legacy Criteria createAlias/createCriteria on an @ElementCollection property; HQL/implicit paths that require the associated entity of the collection (e.g. navigating 'o.someEntityField' through element-collection elements as if they were entities); tooling walking CollectionType.getAssociatedEntityName for every collection role; mistakenly mapping what should be an @OneToMany as @ElementCollection of @Embeddable and then joining on entity fields.","commonSituations":"Embeddable value objects later promoted to entities while the mapping stays @ElementCollection; generic join builders and specification APIs aliasing every plural attribute; Envers/report queries asking for the 'target entity' of each collection; renaming element classes so developers assume entity semantics.","solutions":["Read the role in the message: <OwnerEntity>.<property> - open that mapping and decide whether the elements are values or entities.","If elements should be entities, switch to @OneToMany + a target @Entity (and a join table/fk) so the collection really is an association.","If elements are values, adjust the query/code to treat them as embeddables: join and select value(o.elements) / element fields directly, without asking for an associated entity name.","In generic walkers, guard with persister.getElementType().isEntityType() before calling getAssociatedEntityName."],"exampleFix":"// before: element collection used like an entity association\n@ElementCollection\nSet<Tag> tags;\n\nsession.createQuery(\n    \"select o from Owner o join o.tags t where t.name = :n\", Owner.class); // ok for value paths\n// but legacy aliasing / getAssociatedEntityName('tags') throws\n\n// after (if Tag should be an entity association)\n@Entity public class Tag { @Id Long id; String name; @ManyToOne Owner owner; }\n\n@OneToMany(mappedBy = \"owner\")\nSet<Tag> tags; // now a real association; joins and aliases work","handlingStrategy":"validation","validationCode":"// Generic association walkers: check element type before asking for target entity\nCollectionPersister cp = ((SessionFactoryImplementor) sessionFactory)\n        .getMappingMetamodel().getCollectionDescriptor(role);\nif (!cp.getElementType().isEntityType()) {\n    // element collection: join values only, never ask for associated entity name\n    return Optional.empty();\n}","typeGuard":"static boolean isEntityValuedCollection(CollectionPersister cp) {\n    return cp.getElementType().isEntityType();\n}","tryCatchPattern":"try {\n    return type.getAssociatedEntityName(factory);\n} catch (MappingException e) {\n    if (e.getMessage().startsWith(\"Collection is not an association\")) {\n        // fall back to element-level (value) handling for @ElementCollection roles\n        return handleAsElementCollection(role);\n    }\n    throw e;\n}","preventionTips":["Decide per collection whether elements are values (@ElementCollection) or entities (@OneToMany) and query accordingly.","Guard generic join/alias builders with an elementType().isEntityType() check and log skipped element collections."],"tags":["hibernate","element-collection","association","mapping","join"],"backgroundTag":"invalid-collection-join","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}