This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch fix/composite-id-criteria-14516 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 3e9a6219a7e98f5b8d1df3fe7f47d231524be624 Author: James Fredley <[email protected]> AuthorDate: Wed Feb 25 22:56:33 2026 -0500 Fix composite ID criteria projection regression (#14516) Handle composite ID component properties in AbstractHibernateCriteriaBuilder that are not registered as JPA Metamodel attributes. When entityType.getAttribute() fails for a composite key component, fall back to checking if the property type is a managed entity so the criteria builder can navigate associations forming part of a composite key. Assisted-by: Claude Code <[email protected]> --- .../query/AbstractHibernateCriteriaBuilder.java | 40 +++++++++++++++++++- .../tests/compositeid/CompositeIdCriteria.groovy | 43 ++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java index d23650e909..33d0050127 100644 --- a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java +++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateCriteriaBuilder.java @@ -1806,7 +1806,45 @@ public abstract class AbstractHibernateCriteriaBuilder extends GroovyObjectSuppo if (pd != null && pd.getReadMethod() != null) { final Metamodel metamodel = sessionFactory.getMetamodel(); final EntityType<?> entityType = metamodel.entity(targetClass); - final Attribute<?, ?> attribute = entityType.getAttribute(name); + + Attribute<?, ?> attribute = null; + try { + attribute = entityType.getAttribute(name); + } catch (IllegalArgumentException e) { + // Composite ID components may not be registered as JPA Metamodel + // attributes. Fall back to checking if the property type is a managed + // entity so the criteria builder can navigate associations that form + // part of a composite key (e.g. UserRole with composite: ['user', 'role']). + Class<?> propertyType = pd.getPropertyType(); + try { + metamodel.entity(propertyType); + // Property type is a managed entity - treat as association + Class oldTargetClass = targetClass; + targetClass = propertyType; + if (targetClass.equals(oldTargetClass) && !hasMoreThanOneArg) { + joinType = org.hibernate.sql.JoinType.LEFT_OUTER_JOIN.getJoinTypeValue(); + } + associationStack.add(name); + final String associationPath = getAssociationPath(); + createAliasIfNeccessary(name, associationPath, joinType); + logicalExpressionStack.add(new LogicalExpression(AND)); + invokeClosureNode(callable); + aliasStack.remove(aliasStack.size() - 1); + if (!aliasInstanceStack.isEmpty()) { + aliasInstanceStack.remove(aliasInstanceStack.size() - 1); + } + LogicalExpression logicalExpression = logicalExpressionStack.remove(logicalExpressionStack.size() - 1); + if (!logicalExpression.args.isEmpty()) { + addToCriteria(logicalExpression.toCriterion()); + } + associationStack.remove(associationStack.size() - 1); + targetClass = oldTargetClass; + return name; + } catch (IllegalArgumentException ignored) { + // Not a managed entity type - re-throw original exception + throw e; + } + } if (attribute.isAssociation()) { Class oldTargetClass = targetClass; diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy index 73bb0f8fb3..08beb75e37 100644 --- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy +++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/tests/compositeid/CompositeIdCriteria.groovy @@ -79,6 +79,49 @@ class CompositeIdCriteria extends Specification { } } == [compositeIdToMany] } + @Issue("https://github.com/apache/grails-core/issues/14516") + def "test that composite id components can be used in criteria projections"() { + Author _author = new Author(name:"Author").save() + Book _book = new Book(title:"Book").save() + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + + when: "querying with projections navigating composite ID component associations" + def results = CompositeIdToMany.createCriteria().list { + projections { + book { + property('id') + } + } + author { + eq('id', _author.id) + } + } + + then: "the projection returns the expected ID" + results.size() == 1 + results[0] == _book.id + } + + @Issue("https://github.com/apache/grails-core/issues/14516") + def "test that composite id components can be used in criteria restrictions"() { + Author _author = new Author(name:"Author2").save() + Book _book = new Book(title:"Book2").save() + CompositeIdToMany compositeIdToMany = new CompositeIdToMany(author:_author, book:_book).save(failOnError:true, flush:true) + + when: "querying with restrictions on composite ID component associations" + def results = CompositeIdToMany.createCriteria().list { + author { + eq('name', 'Author2') + } + book { + eq('title', 'Book2') + } + } + + then: "the entity is found" + results.size() == 1 + results[0] == compositeIdToMany + } } @Entity
