You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by da...@apache.org on 2026/02/26 00:26:22 UTC

(grails-core) branch fix-detachedcriteria-join-get-hibernate7 created (now 72cbb2f598)

This is an automated email from the ASF dual-hosted git repository.

davydotcom pushed a change to branch fix-detachedcriteria-join-get-hibernate7
in repository https://gitbox.apache.org/repos/asf/grails-core.git


      at 72cbb2f598 adding one more test spec

This branch includes the following new commits:

     new 6e2fb58aef Fix DetachedCriteria join handling for get() in Hibernate
     new b1a61ee9e9 Auto-create aliases for joined association projections
     new 72cbb2f598 adding one more test spec

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(grails-core) 02/03: Auto-create aliases for joined association projections

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davydotcom pushed a commit to branch fix-detachedcriteria-join-get-hibernate7
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit b1a61ee9e913d7143db10e23dd149cc0dd01b83e
Author: David Estes <da...@gmail.com>
AuthorDate: Wed Feb 25 17:57:27 2026 -0500

    Auto-create aliases for joined association projections
    
    DetachedCriteria join('club') with property('club.name') failed unless an explicit createAlias('club','club') was provided. The query path for detached projections adapted projections directly and did not normalize nested association property paths into Hibernate aliases.\n\nAdd projection-property normalization in AbstractHibernateQuery to auto-create aliases for association paths and map property-based projections to aliased paths before adapting to Hibernate projections.\n\nAdd reg [...]
---
 .../hibernate/query/AbstractHibernateQuery.java    | 88 ++++++++++++++++++++--
 .../gorm/specs/DetachedCriteriaJoinSpec.groovy     | 24 ++++++
 2 files changed, 104 insertions(+), 8 deletions(-)

diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java
index ef96b55d66..8bb56963c5 100644
--- a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java
+++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java
@@ -685,6 +685,78 @@ public abstract class AbstractHibernateQuery extends Query {
         c.addOrder(order.isIgnoreCase() ? hibernateOrder.ignoreCase() : hibernateOrder);
     }
 
+    private String calculateProjectionPropertyName(String propertyName) {
+        int firstDot = propertyName.indexOf('.');
+        if (firstDot < 0) {
+            return calculatePropertyName(propertyName);
+        }
+
+        PersistentEntity currentEntity = getEntity();
+        String currentAlias = null;
+        StringBuilder associationPath = new StringBuilder();
+        String[] tokens = propertyName.split("\\.");
+
+        for (int i = 0; i < tokens.length - 1; i++) {
+            String token = tokens[i];
+            PersistentProperty persistentProperty = currentEntity != null ? currentEntity.getPropertyByName(token) : null;
+            if (!(persistentProperty instanceof Association) || persistentProperty instanceof Embedded) {
+                return calculatePropertyName(propertyName);
+            }
+
+            if (associationPath.length() > 0) {
+                associationPath.append('.');
+            }
+            associationPath.append(token);
+
+            CriteriaAndAlias criteriaAndAlias = getOrCreateAlias(associationPath.toString(), generateAlias(token));
+            if (criteriaAndAlias == null) {
+                return calculatePropertyName(propertyName);
+            }
+            currentAlias = criteriaAndAlias.alias;
+            currentEntity = ((Association) persistentProperty).getAssociatedEntity();
+        }
+
+        if (currentAlias == null) {
+            return calculatePropertyName(propertyName);
+        }
+        return currentAlias + '.' + tokens[tokens.length - 1];
+    }
+
+    private Query.Projection normalizeProjectionPropertyPath(Query.Projection projection) {
+        if (!(projection instanceof Query.PropertyProjection)) {
+            return projection;
+        }
+
+        String propertyName = ((Query.PropertyProjection) projection).getPropertyName();
+        String normalizedPropertyName = calculateProjectionPropertyName(propertyName);
+        if (propertyName.equals(normalizedPropertyName)) {
+            return projection;
+        }
+
+        if (projection instanceof Query.DistinctPropertyProjection) {
+            return org.grails.datastore.mapping.query.Projections.distinct(normalizedPropertyName);
+        }
+        if (projection instanceof Query.CountDistinctProjection) {
+            return org.grails.datastore.mapping.query.Projections.countDistinct(normalizedPropertyName);
+        }
+        if (projection instanceof Query.GroupPropertyProjection) {
+            return org.grails.datastore.mapping.query.Projections.groupProperty(normalizedPropertyName);
+        }
+        if (projection instanceof Query.SumProjection) {
+            return org.grails.datastore.mapping.query.Projections.sum(normalizedPropertyName);
+        }
+        if (projection instanceof Query.MinProjection) {
+            return org.grails.datastore.mapping.query.Projections.min(normalizedPropertyName);
+        }
+        if (projection instanceof Query.MaxProjection) {
+            return org.grails.datastore.mapping.query.Projections.max(normalizedPropertyName);
+        }
+        if (projection instanceof Query.AvgProjection) {
+            return org.grails.datastore.mapping.query.Projections.avg(normalizedPropertyName);
+        }
+        return org.grails.datastore.mapping.query.Projections.property(normalizedPropertyName);
+    }
+
     @Override
     public Query join(String property) {
         this.hasJoins = true;
@@ -957,19 +1029,19 @@ public abstract class AbstractHibernateQuery extends Query {
 
         @Override
         public ProjectionList add(Projection p) {
-            projectionList.add(new HibernateProjectionAdapter(p).toHibernateProjection());
+            projectionList.add(new HibernateProjectionAdapter(normalizeProjectionPropertyPath(p)).toHibernateProjection());
             return this;
         }
 
         @Override
         public org.grails.datastore.mapping.query.api.ProjectionList countDistinct(String property) {
-            projectionList.add(Projections.countDistinct(calculatePropertyName(property)));
+            projectionList.add(Projections.countDistinct(calculateProjectionPropertyName(property)));
             return this;
         }
 
         @Override
         public org.grails.datastore.mapping.query.api.ProjectionList distinct(String property) {
-            projectionList.add(Projections.distinct(Projections.property(calculatePropertyName(property))));
+            projectionList.add(Projections.distinct(Projections.property(calculateProjectionPropertyName(property))));
             return this;
         }
 
@@ -995,31 +1067,31 @@ public abstract class AbstractHibernateQuery extends Query {
 
         @Override
         public ProjectionList property(String name) {
-            projectionList.add(Projections.property(calculatePropertyName(name)));
+            projectionList.add(Projections.property(calculateProjectionPropertyName(name)));
             return this;
         }
 
         @Override
         public ProjectionList sum(String name) {
-            projectionList.add(Projections.sum(calculatePropertyName(name)));
+            projectionList.add(Projections.sum(calculateProjectionPropertyName(name)));
             return this;
         }
 
         @Override
         public ProjectionList min(String name) {
-            projectionList.add(Projections.min(calculatePropertyName(name)));
+            projectionList.add(Projections.min(calculateProjectionPropertyName(name)));
             return this;
         }
 
         @Override
         public ProjectionList max(String name) {
-            projectionList.add(Projections.max(calculatePropertyName(name)));
+            projectionList.add(Projections.max(calculateProjectionPropertyName(name)));
             return this;
         }
 
         @Override
         public ProjectionList avg(String name) {
-            projectionList.add(Projections.avg(calculatePropertyName(name)));
+            projectionList.add(Projections.avg(calculateProjectionPropertyName(name)));
             return this;
         }
 
diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
index 1193e3eeee..1beabf3a83 100644
--- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
+++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
@@ -104,4 +104,28 @@ class DetachedCriteriaJoinSpec extends GrailsDataTckSpec<GrailsDataHibernate5Tck
         team != null
         Hibernate.isInitialized(team.club)
     }
+
+    def 'check list with join and projected association property works without explicit alias'() {
+        given:
+        def club = new Club(name: 'Milan').save(flush: true)
+        new Team(name: 'Rossoneri', club: club).save(flush: true)
+
+        when:
+        def result = Team.where { name == 'Rossoneri' }.join('club').property('club.name').list()
+
+        then:
+        result == ['Milan']
+    }
+
+    def 'check get with join and projected association property works without explicit alias'() {
+        given:
+        def club = new Club(name: 'Inter').save(flush: true)
+        new Team(name: 'Nerazzurri', club: club).save(flush: true)
+
+        when:
+        def result = Team.where { name == 'Nerazzurri' }.join('club').property('club.name').get()
+
+        then:
+        result == 'Inter'
+    }
 }


(grails-core) 03/03: adding one more test spec

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davydotcom pushed a commit to branch fix-detachedcriteria-join-get-hibernate7
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 72cbb2f598a4df569a0bd26e148396693458c4f4
Author: David Estes <da...@gmail.com>
AuthorDate: Wed Feb 25 18:11:58 2026 -0500

    adding one more test spec
---
 .../gorm/specs/DetachedCriteriaJoinSpec.groovy     | 30 ++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
index 1beabf3a83..ad028f391f 100644
--- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
+++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
@@ -128,4 +128,34 @@ class DetachedCriteriaJoinSpec extends GrailsDataTckSpec<GrailsDataHibernate5Tck
         then:
         result == 'Inter'
     }
+
+    def 'check list with association subquery plus join and projection works'() {
+        given:
+        def club = new Club(name: 'Ajax').save(flush: true)
+        new Team(name: 'Amsterdammers', club: club).save(flush: true)
+
+        when:
+        def result = Team.where {
+            club {
+                name == 'Ajax'
+            }
+        }.join('club').property('club.name').list()
+
+        then:
+        result == ['Ajax']
+    }
+
+    def 'check list can sort by joined association property'() {
+        given:
+        def clubA = new Club(name: 'A Club').save(flush: true)
+        def clubB = new Club(name: 'B Club').save(flush: true)
+        new Team(name: 'Team B', club: clubB).save(flush: true)
+        new Team(name: 'Team A', club: clubA).save(flush: true)
+
+        when:
+        def result = Team.where {}.join('club').sort('club.name', 'asc').property('name').list()
+
+        then:
+        result == ['Team A', 'Team B']
+    }
 }


(grails-core) 01/03: Fix DetachedCriteria join handling for get() in Hibernate

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davydotcom pushed a commit to branch fix-detachedcriteria-join-get-hibernate7
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 6e2fb58aef3f2ecb136a6793cecc7457cf6bbc68
Author: David Estes <da...@gmail.com>
AuthorDate: Wed Feb 25 17:49:57 2026 -0500

    Fix DetachedCriteria join handling for get() in Hibernate
    
    DetachedCriteria joins with explicit join types were not updating HibernateQuery join state used by single-result execution. This caused get()/singleResult() paths to diverge from list() behavior for join fetch application.\n\nAdd an override of join(String, JoinType) in AbstractHibernateQuery that sets hasJoins, records joinTypes, and applies FetchMode.JOIN to criteria/detachedCriteria.\n\nAlso add a regression test in DetachedCriteriaJoinSpec proving Team.where { ... }.join('club'). [...]
---
 .../grails/orm/hibernate/query/AbstractHibernateQuery.java | 11 +++++++++++
 .../grails/gorm/specs/DetachedCriteriaJoinSpec.groovy      | 14 ++++++++++++++
 2 files changed, 25 insertions(+)

diff --git a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java
index eed1e54bea..ef96b55d66 100644
--- a/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java
+++ b/grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/query/AbstractHibernateQuery.java
@@ -695,6 +695,17 @@ public abstract class AbstractHibernateQuery extends Query {
         return this;
     }
 
+    @Override
+    public Query join(String property, JoinType joinType) {
+        this.hasJoins = true;
+        this.joinTypes.put(property, joinType);
+        if (criteria != null)
+            criteria.setFetchMode(property, FetchMode.JOIN);
+        else if (detachedCriteria != null)
+            detachedCriteria.setFetchMode(property, FetchMode.JOIN);
+        return this;
+    }
+
     @Override
     public Query select(String property) {
         this.hasJoins = true;
diff --git a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
index 4cf03df933..1193e3eeee 100644
--- a/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
+++ b/grails-data-hibernate5/core/src/test/groovy/grails/gorm/specs/DetachedCriteriaJoinSpec.groovy
@@ -25,6 +25,7 @@ import org.apache.grails.data.hibernate5.core.GrailsDataHibernate5TckManager
 import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
 import org.grails.datastore.gorm.finders.DynamicFinder
 import org.grails.orm.hibernate.query.HibernateQuery
+import org.hibernate.Hibernate
 
 import jakarta.persistence.criteria.JoinType
 
@@ -90,4 +91,17 @@ class DetachedCriteriaJoinSpec extends GrailsDataTckSpec<GrailsDataHibernate5Tck
         expect:
         joinType == org.hibernate.sql.JoinType.RIGHT_OUTER_JOIN
     }
+
+    def 'check get honours join and eagerly loads association'() {
+        given:
+        def club = new Club(name: 'Juventus').save(flush: true)
+        new Team(name: 'Torino', club: club).save(flush: true)
+
+        when:
+        Team team = Team.where { name == 'Torino' }.join('club').get()
+
+        then:
+        team != null
+        Hibernate.isInitialized(team.club)
+    }
 }