{"record":{"id":"3eff8261b31301c4","repo":"hibernate/hibernate-orm","slug":"attribute-attribute-is-not-joinable","errorCode":null,"errorMessage":"Attribute '{attribute}' is not joinable","messagePattern":"Attribute '(.+?)' is not joinable","errorType":"exception","errorClass":"SemanticException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/query/sqm/tree/spi/domain/AbstractSqmFrom.java","lineNumber":983,"sourceCode":"\t\t}\n\t}\n\n\tprivate <A> SqmSingularJoin<T, A> buildSingularJoin(\n\t\t\tSqmSingularPersistentAttribute<? super T, A> attribute,\n\t\t\tSqmJoinType joinType,\n\t\t\tboolean fetched) {\n\t\tif ( attribute.getPathType() instanceof ManagedDomainType ) {\n\t\t\treturn new SqmSingularJoin<>(\n\t\t\t\t\tthis,\n\t\t\t\t\tattribute,\n\t\t\t\t\tgenerateAlias(),\n\t\t\t\t\tjoinType,\n\t\t\t\t\tfetched,\n\t\t\t\t\tnodeBuilder()\n\t\t\t);\n\t\t}\n\n\t\tthrow new SemanticException( \"Attribute '\" + attribute + \"' is not joinable\" );\n\t}\n\n\tprivate <E> SqmBagJoin<T, E> buildBagJoin(\n\t\t\tBagPersistentAttribute<? super T, E> attribute,\n\t\t\tSqmJoinType joinType,\n\t\t\tboolean fetched) {\n\t\treturn new SqmBagJoin<>(\n\t\t\t\tthis,\n\t\t\t\t(SqmBagPersistentAttribute<? super T, E>) attribute,\n\t\t\t\tgenerateAlias(),\n\t\t\t\tjoinType,\n\t\t\t\tfetched,\n\t\t\t\tnodeBuilder()\n\t\t);\n\t}\n\n\tprivate <E> SqmListJoin<T, E> buildListJoin(\n\t\t\tListPersistentAttribute<? super T, E> attribute,","sourceCodeStart":965,"sourceCodeEnd":1001,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/spi/domain/AbstractSqmFrom.java#L965-L1001","documentation":"Thrown by AbstractSqmFrom#buildSingularJoin (hibernate-core .../tree/spi/domain/AbstractSqmFrom.java:983) when a query join is built over a singular attribute whose type is not a ManagedDomainType — i.e. anything that is not an entity, embeddable, or mapped superclass (a basic String/Integer/enum attribute, a basic array, or an @Any mapping, whose AnyMappingDomainType is only a SimpleDomainType). SQL joins need a managed type on the right-hand side so Hibernate can build a navigable path across the association; basic values have nothing to join to. It is a SemanticException raised while the SQM tree is built, so the query fails before any SQL is generated or executed.","triggerScenarios":"Calling Root.join(String) / join(String, JoinType) / join(SingularAttribute) on a basic-typed attribute, e.g. root.join(Order_.status) where status is a String — all these AbstractSqmFrom.join overloads funnel into buildSingularJoin (AbstractSqmFrom.java:968). The same code path is reached from an explicit HQL join such as 'select o from Order o join o.status s'. Also triggered by joining an @Any-mapped attribute, or by a dynamic query builder that joins any attribute name it receives.","commonSituations":"Dynamic query DSLs that turn every client-supplied field name into a join; metamodel constant typos (Order_.customerName instead of Order_.customer); copy-pasting an HQL join onto a column instead of an association; expecting @Any/@ManyToAny attributes to behave like joinable polymorphic associations; queries migrated from older Hibernate versions that produced different diagnostics.","solutions":["Replace the join with a predicate: basic attributes are filtered, not joined — use cb.equal(root.get(Order_.status), value) in criteria or a plain comparison in HQL.","Join only attributes whose type is a ManagedDomainType: @ManyToOne/@OneToOne associations (ENTITY) and @Embedded components (EMBEDDABLE); verify with attribute.getType().getPersistenceType() before calling join.","If the association was intended, fix the attribute reference (Order_.customer, not Order_.customerName) or the HQL path (join o.customer, not join o.customerName).","For @Any attributes, never join — filter on the discriminator/meta columns and load the target entity separately by id."],"exampleFix":"// before\nSqmRoot<Order> order = query.from(Order.class);\norder.join(\"status\"); // status is String -> SemanticException: Attribute 'status' is not joinable\n\n// after\nSqmRoot<Order> order = query.from(Order.class);\nquery.where(cb.equal(order.get(\"status\"), \"OPEN\")); // basic attributes are compared, not joined","handlingStrategy":"validation","validationCode":"import jakarta.persistence.metamodel.*;\n\nSingularAttribute<? super Order, ?> attr = Order_.status; // example attribute\nType<?> t = attr.getType();\nboolean joinable = t.getPersistenceType() == PersistenceType.ENTITY\n                || t.getPersistenceType() == PersistenceType.EMBEDDABLE;\nif (!joinable) {\n    throw new IllegalArgumentException(\n        \"Attribute '\" + attr.getName() + \"' is basic-valued; filter with a predicate instead of joining\");\n}\nroot.join(attr, JoinType.INNER);","typeGuard":"static boolean isJoinableAttribute(SingularAttribute<?, ?> attr) {\n    PersistenceType p = attr.getType().getPersistenceType();\n    return p == PersistenceType.ENTITY || p == PersistenceType.EMBEDDABLE;\n}","tryCatchPattern":"try {\n    root.join(attr, JoinType.INNER);\n} catch (org.hibernate.query.SemanticException e) {\n    // query-construction error: report the offending attribute, do not retry\n    throw new BadRequestException(\"Cannot join attribute: \" + e.getMessage(), e);\n}","preventionTips":["Only join associations (@ManyToOne/@OneToOne) and embeddables; never column/basic attributes.","In dynamic query DSLs, whitelist joinable attribute names against the JPA metamodel before building joins.","Write joins against metamodel constants (Order_.customer) so the compiler catches wrong attributes.","Remember @Any and @ManyToAny attributes are never joinable — plan discriminator-based queries instead."],"tags":["hibernate","sqm","jpa-criteria","hql","join","semantic-error"],"backgroundTag":"attribute-not-joinable","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}