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

Reply via email to