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 <[email protected]> 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' + } }
