This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 3d4d4be350384efdc96466e27f75cd3831d563fd Author: Walter Duque de Estrada <[email protected]> AuthorDate: Sat Mar 21 23:06:34 2026 -0500 hibernate 7: Key Enhancements & Fixes: * Fixed Many-to-Many Persistence: Removed an erroneous setInverse call in ManyToManyElementBinder that was hardcoding bidirectional associations as inverse. This restored the correct GORM ownership rules and fixed issues where join table entries were not being created. * Resolved SemanticException in Subqueries: Refactored PredicateGenerator to accurately infer the Java return type for IN subqueries. It now dynamically detects projections—including aliased and nested properties (e.g., e1.id)—to ensure the subquery result matches the type expected by the Hibernate 7 SQM engine. * Fixed Subquery Alias Isolation: Improved PredicateGenerator to use a specialized JpaFromProvider constructor for subqueries. This correctly isolates and registers subquery-specific aliases, preventing PathElementException when using DetachedCriteria within a main query. --- grails-data-hibernate7/core/ISSUES.md | 3 +- .../secondpass/ManyToManyElementBinder.java | 1 - .../orm/hibernate/query/PredicateGenerator.java | 38 +++++++++++++++------- .../WhereQueryOldIssueVerificationSpec.groovy | 10 +++--- .../hibernatequery/PredicateGeneratorSpec.groovy | 16 +++++++++ .../orm/HibernateCriteriaBuilderDirectSpec.groovy | 37 ++++++++++++++++++++- .../secondpass/ManyToManyElementBinderSpec.groovy | 17 ---------- 7 files changed, 84 insertions(+), 38 deletions(-) diff --git a/grails-data-hibernate7/core/ISSUES.md b/grails-data-hibernate7/core/ISSUES.md index 14503ccf09..524d83cb6d 100644 --- a/grails-data-hibernate7/core/ISSUES.md +++ b/grails-data-hibernate7/core/ISSUES.md @@ -1,6 +1,5 @@ # Known Issues in Hibernate 7 Migration -DetachedCriteriaProjectionAliasSpec -WhereQueryOldIssueVerificationSpec + diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinder.java index f44024af30..43408eedd1 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinder.java @@ -46,6 +46,5 @@ public class ManyToManyElementBinder { Collection collection = property.getCollection(); collection.setElement(element); collectionForPropertyConfigBinder.bindCollectionForPropertyConfig(property); - collection.setInverse(!property.isCircular()); } } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java index 2a4ba1f7d2..4b141302b0 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java @@ -23,12 +23,12 @@ import jakarta.persistence.criteria.Subquery; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaInPredicate; import org.hibernate.query.sqm.tree.domain.SqmPath; -import org.hibernate.query.sqm.tree.predicate.SqmInListPredicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.convert.ConversionService; +import grails.gorm.DetachedCriteria; import org.grails.datastore.gorm.GormEntity; import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria; import org.grails.datastore.mapping.core.exceptions.ConfigurationException; @@ -404,13 +404,35 @@ public class PredicateGenerator { var path = fromsByProvider.getFullyQualifiedPath(criterion.getProperty()); boolean isAssociation = isAssociation(entity, criterion.getProperty()); var in = findInPredicate(cb, projection, path, subProperty, isAssociation); - var subquery = criteriaQuery.subquery(getJavaTypeOfInClause((SqmInListPredicate) in)); + PersistentEntity subEntity = queryableCriteria.getPersistentEntity(); + Class<?> subqueryType = subEntity.getJavaClass(); + if (projection instanceof Query.PropertyProjection propertyProjection) { + PersistentProperty prop = subEntity.getPropertyByName(propertyProjection.getPropertyName()); + if (prop != null) { + subqueryType = prop.getType(); + } else if (propertyProjection.getPropertyName().contains(".")) { + // Handle aliased or nested properties in projections (e.g., "e1.id") + String propName = propertyProjection.getPropertyName(); + String simplePropName = propName.substring(propName.lastIndexOf('.') + 1); + PersistentProperty simpleProp = subEntity.getPropertyByName(simplePropName); + if (simpleProp != null) { + subqueryType = simpleProp.getType(); + } else if (simplePropName.equals("id")) { + subqueryType = subEntity.getIdentity() != null ? subEntity.getIdentity().getType() : Long.class; + } + } + } else if (projection instanceof Query.IdProjection) { + subqueryType = subEntity.getIdentity() != null ? subEntity.getIdentity().getType() : Long.class; + } else if (isAssociation) { + subqueryType = subEntity.getIdentity() != null ? subEntity.getIdentity().getType() : Long.class; + } + + var subquery = criteriaQuery.subquery(subqueryType); var from = subquery.from(subEntity.getJavaClass()); - var clonedProviderByName = (JpaFromProvider) fromsByProvider.clone(); - clonedProviderByName.put("root", from); + var clonedProviderByName = new JpaFromProvider(fromsByProvider, (DetachedCriteria<?>) queryableCriteria, java.util.Collections.emptyList(), from); var predicates = getPredicates(cb, criteriaQuery, from, queryableCriteria.getCriteria(), clonedProviderByName, subEntity); - subquery.select(clonedProviderByName.getFullyQualifiedPath(subProperty)).distinct(true).where(cb.and(predicates)); + subquery.select((Expression) clonedProviderByName.getFullyQualifiedPath(subProperty)).distinct(true).where(cb.and(predicates)); return in.value(subquery); } @@ -459,12 +481,6 @@ public class PredicateGenerator { : ((Query.NotIn) criterion).getSubquery(); } - private Class getJavaTypeOfInClause(SqmInListPredicate predicate) { - return Optional.ofNullable(predicate.getTestExpression().getExpressible()) - .map(expressible -> expressible.getExpressibleJavaType().getJavaTypeClass()) - .orElse(null); - } - private Number getNumericValue(Query.PropertyCriterion criterion) { Object value = criterion.getValue(); if (value != null) { diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/WhereQueryOldIssueVerificationSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/WhereQueryOldIssueVerificationSpec.groovy index b0d844cf8f..d2664b220a 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/WhereQueryOldIssueVerificationSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/WhereQueryOldIssueVerificationSpec.groovy @@ -237,13 +237,11 @@ class WhereQueryOldIssueVerificationSpec extends Specification { @Issue('https://github.com/apache/grails-core/issues/14600') def "findAllBy works with bidirectional hasMany relation"() { given: "authors with books in a bidirectional hasMany" - def author1 = new WqBiAuthor(name: "Stephen King").save(flush: true) - def book1 = new WqBiBook(title: "IT").save(flush: true) - def book2 = new WqBiBook(title: "The Shining").save(flush: true) + def author1 = new WqBiAuthor(name: "Stephen King") + def book1 = new WqBiBook(title: "IT") + def book2 = new WqBiBook(title: "The Shining") author1.addToBooks(book1) author1.addToBooks(book2) - book1.addToAuthors(author1) - book2.addToAuthors(author1) author1.save(flush: true) when: "using withCriteria to find books by author" @@ -362,7 +360,7 @@ class WqBiBook implements HibernateEntity<WqBiBook> { String title static hasMany = [authors: WqBiAuthor] - static belongsTo = WqBiAuthor + static belongsTo = [WqBiAuthor] } @Entity diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy index ac0f2b5da9..30e7fdac86 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy @@ -105,6 +105,22 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec { predicates.length == 1 } + def "test getPredicates with subquery aliases"() { + given: "a subquery with an alias" + def subCriteria = new DetachedCriteria(PredicateGeneratorSpecPet).build { + createAlias('face', 'f') + eq('f.name', 'Funny') + } + List criteria = [new Query.In("id", subCriteria)] + + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + + then: "the alias 'f' is correctly resolved" + noExceptionThrown() + predicates.length == 1 + } + def "test getPredicates with Disjunction"() { given: List criteria = [new Query.Disjunction() diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy index 07d8f0ba55..177334cbfa 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderDirectSpec.groovy @@ -42,7 +42,7 @@ class HibernateCriteriaBuilderDirectSpec extends HibernateGormDatastoreSpec { @Shared HibernateCriteriaBuilder builder def setupSpec() { - manager.addAllDomainClasses([DirectAccount, DirectTransaction]) + manager.addAllDomainClasses([DirectAccount, DirectTransaction, DirectBiBook, DirectBiAuthor]) } def setup() { @@ -52,6 +52,28 @@ class HibernateCriteriaBuilderDirectSpec extends HibernateGormDatastoreSpec { manager.hibernateDatastore) } + void "test bidirectional many-to-many with subquery alias resolution"() { + given: "authors with books in a bidirectional hasMany" + def author1 = new DirectBiAuthor(name: "Stephen King") + def book1 = new DirectBiBook(title: "IT") + def book2 = new DirectBiBook(title: "The Shining") + author1.addToBooks(book1) + author1.addToBooks(book2) + author1.save(flush: true) + + def b = new HibernateCriteriaBuilder(DirectBiBook, manager.hibernateDatastore.sessionFactory, manager.hibernateDatastore) + + when: "using withCriteria to find books by author" + def books = b.list { + authors { + 'in'('id', [author1.id]) + } + } + + then: "books are found without error" + books.size() == 2 + } + // ─── DSL integration: data-driven scenarios ──────────────────────────── def setupData() { @@ -669,3 +691,16 @@ class DirectTransaction { BigDecimal amount static belongsTo = [account: DirectAccount] } + +@Entity +class DirectBiBook { + String title + static hasMany = [authors: DirectBiAuthor] + static belongsTo = [DirectBiAuthor] +} + +@Entity +class DirectBiAuthor { + String name + static hasMany = [books: DirectBiBook] +} diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinderSpec.groovy index 52a1dae3be..5993fe60e3 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ManyToManyElementBinderSpec.groovy @@ -59,23 +59,6 @@ class ManyToManyElementBinderSpec extends HibernateGormDatastoreSpec { collection.getElement() instanceof ManyToOne (collection.getElement() as ManyToOne).getReferencedEntityName() == MTMEItem.name } - - def "bind sets collection inverse false for a circular many-to-many"() { - given: - def property = propertyFor(MTMESubtype, "related") - def mbc = getGrailsDomainBinder().getMetadataBuildingContext() - def collection = new Bag(mbc, null) - collection.setCollectionTable(new Table("test", "mtme_subtype_mtme_base")) - - property.setCollection(collection) - - when: - binder.bind(property) - - then: - property.isCircular() - !collection.isInverse() - } } @Entity
