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 08d6235ae924ff79e910a351db63b2a5d5e64403 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Mar 20 13:29:53 2026 -0500 hibernate 7: * Fixed DetachedAssociationFunctionSpec --- grails-data-hibernate7/core/ISSUES.md | 72 ++- .../groovy/grails/orm/CriteriaMethodInvoker.java | 47 +- .../main/groovy/grails/orm/CriteriaMethods.java | 3 +- .../grails/orm/HibernateCriteriaBuilder.java | 20 + .../cfg/domainbinding/binder/EnumTypeBinder.java | 6 +- .../domainbinding/binder/GrailsPropertyBinder.java | 22 +- .../collectionType/CollectionType.java | 5 +- .../grails/orm/hibernate/query/HibernateAlias.java | 44 ++ .../orm/hibernate/query/JpaFromProvider.java | 24 +- .../orm/hibernate/query/PredicateGenerator.java | 13 + .../hibernatequery/JpaFromProviderSpec.groovy | 91 +++- .../hibernatequery/PredicateGeneratorSpec.groovy | 18 +- .../grails/orm/CriteriaMethodInvokerSpec.groovy | 10 + .../orm/HibernateCriteriaBuilderDirectSpec.groovy | 8 + .../grails/orm/HibernateCriteriaBuilderSpec.groovy | 13 +- .../cfg/domainbinding/EnumTypeBinderSpec.groovy | 12 + .../domainbinding/GrailsPropertyBinderSpec.groovy | 551 +++++---------------- .../query/DetachedAssociationFunctionSpec.groovy | 5 +- 18 files changed, 468 insertions(+), 496 deletions(-) diff --git a/grails-data-hibernate7/core/ISSUES.md b/grails-data-hibernate7/core/ISSUES.md index aa3bad033a..007522bf73 100644 --- a/grails-data-hibernate7/core/ISSUES.md +++ b/grails-data-hibernate7/core/ISSUES.md @@ -1,14 +1,31 @@ # Known Issues in Hibernate 7 Migration +### 1. Float Precision Mismatch (H2 and PostgreSQL) +**Symptoms:** +- `org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL` +- H2 Error: `Precision ("64") must be between "1" and "53" inclusive` +- PostgreSQL Error: `ERROR: precision for type float must be less than 54 bits` + +**Description:** +Hibernate 7's default mapping for `java.lang.Double` properties on H2 (2.x) and PostgreSQL (16+) generates DDL with `float(64)`. Both databases reject this, as the maximum precision for the `float`/`double precision` type is 53 bits. + +**Workaround:** +The framework now defaults to precision `15` decimal digits for non-Oracle dialects, which maps to ~53 bits. + +--- ## Failing Tests BasicCollectionInQuerySpec ByteBuddyGroovyInterceptorSpec -DetachedAssociationFunctionSpec DetachedCriteriaProjectionAliasSpec HibernateProxyHandler7Spec WhereQueryOldIssueVerificationSpec +**Description:** +When a table creation fails (e.g., due to the Float Precision Mismatch issue), the `SequenceStyleGenerator` is not properly initialized. Subsequent attempts to persist an entity trigger an NPE instead of a descriptive error. + +**Action Taken:** +Updated `GrailsNativeGenerator` to check the state of the delegate generator and throw a descriptive `HibernateException`. --- @@ -22,19 +39,41 @@ Hibernate 7's `ByteBuddyInterceptor.intercept()` does not distinguish between ac --- +### 4. JpaFromProvider & JpaCriteriaQueryCreator (Resolved) +**Symptoms:** +- Association projection paths fail to resolve correctly in complex queries. +- `NullPointerException` during path resolution in Criteria queries. + +**Description:** +Referencing an association in a projection (e.g., `projections { property('owner.name') }`) requires an automatic join. `JpaFromProvider` has been updated to scan projections and automatically create hierarchical `LEFT JOIN`s for discovered association paths. Intermediate segments are also correctly joined. + --- +### 5. HibernateQuery Event ClassCastException (Resolved in Spec) +**Symptoms:** +- `java.lang.ClassCastException: class org.grails.datastore.mapping.query.event.PreQueryEvent cannot be cast to class org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent` + +**Description:** +The event listener in `HibernateQuerySpec` was incorrectly expecting `AbstractPersistenceEvent` while `PreQueryEvent` and `PostQueryEvent` now extend `AbstractQueryEvent`. The spec has been updated to use the correct event type. + --- -### 6. MappingException: Class 'java.util.Set' does not implement 'UserCollectionType' +### 6. MappingException: Class 'java.util.Set' does not implement 'UserCollectionType' (Resolved) **Symptoms:** - `org.hibernate.MappingException: Class 'java.util.Set' does not implement 'org.hibernate.usertype.UserCollectionType'` -- Affects `BasicCollectionInQuerySpec`. **Description:** -Hibernate 7 changed how collection types are resolved. Some tests using `hasMany` with default collection types are failing during `buildSessionFactory`. +Hibernate 7 changed how collection types are resolved. Standard collection types like `java.util.Set` should not have their type name set to the class name, as Hibernate 7 expects a `UserCollectionType` when a type name is provided. `CollectionType.java` was updated to avoid setting the type name for standard collections. --- + +### 7. TerminalPathException in SQM Paths (Resolved) +**Symptoms:** +- `org.hibernate.query.sqm.TerminalPathException: Terminal path 'id' has no attribute 'id'` + +**Description:** +In Hibernate 7, once a path is resolved to a terminal attribute (like `id`), further navigation on that path (e.g., trying to access a property on the ID) triggers this exception. `PredicateGenerator` has been updated with an `isAssociation` check to prevent this. + --- ### 8. IDENTITY Generator Default in TCK @@ -46,3 +85,28 @@ The TCK Manager now globally sets `id generator: 'identity'` to avoid `SequenceS --- +### 9. HibernateGormStaticApi HQL Overloads (Resolved in Spec) +**Symptoms:** +- `HibernateGormStaticApiSpec` failures related to `executeQuery` and `executeUpdate` when passing plain `String` queries. + +**Description:** +Hibernate 7's stricter query parameter rules and the removal of certain `Query` overloads lead to `UnsupportedOperationException` when plain `String` queries are passed to `executeQuery` or `executeUpdate`. The spec has been updated to reflect this expected behavior. + +--- + +### 10. Multivalued Paths in IN Queries +**Symptoms:** +- `org.hibernate.query.SemanticException: Multivalued paths are only allowed for the 'member of' operator` +- Affects `BasicCollectionInQuerySpec`. + +**Description:** +In Hibernate 7, using an `IN` operator on a path that represents a collection (multivalued path) is no longer allowed. GORM traditionally supported this by automatically joining the collection. + +--- + +### 11. Missing `createAlias` in HibernateCriteriaBuilder +**Symptoms:** +- `groovy.lang.MissingMethodException: No signature of method: grails.orm.HibernateCriteriaBuilder.createAlias() ...` + +**Description:** +The Hibernate 7 implementation of `HibernateCriteriaBuilder` is missing the `createAlias` method, which is commonly used in GORM criteria queries to define explicit joins. diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java index b857c868ac..07723be11a 100644 --- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java @@ -210,40 +210,49 @@ public class CriteriaMethodInvoker { @SuppressWarnings("PMD.DataflowAnomalyAnalysis") protected Object trySimpleCriteria(String name, CriteriaMethods method, Object... args) { - if (args.length != 1 || args[0] == null) { - return UNHANDLED; - } - if (method != null) { switch (method) { case ID_EQUALS: - return builder.eq("id", args[0]); + if (args.length == 1 && args[0] != null) { + return builder.eq("id", args[0]); + } + break; case CACHE: - if (args[0] instanceof Boolean b) { + if (args.length == 1 && args[0] instanceof Boolean b) { builder.cache(b); + return name; } - return name; + break; case READ_ONLY: - if (args[0] instanceof Boolean b) { + if (args.length == 1 && args[0] instanceof Boolean b) { builder.readOnly(b); + return name; } - return name; + break; case SINGLE_RESULT: return builder.singleResult(); + case CREATE_ALIAS: + if (args.length == 2 && args[0] instanceof String s && args[1] instanceof String a) { + return builder.createAlias(s, a); + } else if (args.length == 3 && args[0] instanceof String s && args[1] instanceof String a && args[2] instanceof Number jt) { + return builder.createAlias(s, a, jt.intValue()); + } + return name; case IS_NULL, IS_NOT_NULL, IS_EMPTY, IS_NOT_EMPTY: - if (!(args[0] instanceof String)) { + if (args.length == 1 && args[0] instanceof String value) { + switch (method) { + case IS_NULL -> builder.getHibernateQuery().isNull(value); + case IS_NOT_NULL -> builder.getHibernateQuery().isNotNull(value); + case IS_EMPTY -> builder.getHibernateQuery().isEmpty(value); + case IS_NOT_EMPTY -> builder.getHibernateQuery().isNotEmpty(value); + default -> { } + } + return name; + } else if (args.length == 1 && args[0] != null) { builder.throwRuntimeException(new IllegalArgumentException( "call to [" + name + "] with value [" + args[0] + "] requires a String value.")); } - final String value = (String) args[0]; - switch (method) { - case IS_NULL -> builder.getHibernateQuery().isNull(value); - case IS_NOT_NULL -> builder.getHibernateQuery().isNotNull(value); - case IS_EMPTY -> builder.getHibernateQuery().isEmpty(value); - case IS_NOT_EMPTY -> builder.getHibernateQuery().isNotEmpty(value); - default -> { } - } - return name; + break; default: break; } diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java index c5f94af815..73538fa6f7 100644 --- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethods.java @@ -61,7 +61,8 @@ public enum CriteriaMethods { CACHE("cache"), READ_ONLY("readOnly"), FETCH_MODE("fetchMode"), - SINGLE_RESULT("singleResult"); + SINGLE_RESULT("singleResult"), + CREATE_ALIAS("createAlias"); private final String name; diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java index d50c961eb6..5d293f45cf 100644 --- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java +++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java @@ -168,6 +168,26 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui } } + public org.grails.datastore.mapping.query.api.Criteria createAlias(String associationPath, String alias) { + var prop = hibernateQuery.getEntity().getPropertyByName(associationPath); + if (prop instanceof org.grails.datastore.mapping.model.types.Basic) { + hibernateQuery.getDetachedCriteria().add(new org.grails.orm.hibernate.query.HibernateAlias(associationPath, alias)); + return this; + } + hibernateQuery.getDetachedCriteria().createAlias(associationPath, alias); + return this; + } + + public org.grails.datastore.mapping.query.api.Criteria createAlias(String associationPath, String alias, int joinType) { + var prop = hibernateQuery.getEntity().getPropertyByName(associationPath); + if (prop instanceof org.grails.datastore.mapping.model.types.Basic) { + hibernateQuery.getDetachedCriteria().add(new org.grails.orm.hibernate.query.HibernateAlias(associationPath, alias)); + return this; + } + hibernateQuery.getDetachedCriteria().createAlias(associationPath, alias); + return this; + } + /** * A projection that selects a property name * diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java index 8df14461a2..1d4c50486c 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/EnumTypeBinder.java @@ -76,8 +76,12 @@ public class EnumTypeBinder { private static final Logger LOG = LoggerFactory.getLogger(EnumTypeBinder.class); public BasicValue bindEnumType(@Nonnull HibernateEnumProperty property, String path) { + return bindEnumType(property, property.getTable(), path); + } + + public BasicValue bindEnumType(@Nonnull HibernateEnumProperty property, Table table, String path) { String columnName = columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(property, path, null); - BasicValue simpleValue = new BasicValue(metadataBuildingContext, property.getTable()); + BasicValue simpleValue = new BasicValue(metadataBuildingContext, table); bindEnumType(property, property.getType(), simpleValue, columnName); return simpleValue; } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java index ba19cfe1f7..cf92cfc134 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java @@ -20,7 +20,6 @@ package org.grails.orm.hibernate.cfg.domainbinding.binder; import jakarta.annotation.Nonnull; -import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Table; import org.hibernate.mapping.Value; import org.slf4j.Logger; @@ -31,9 +30,11 @@ import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEnumPropert import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateManyToOneProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateOneToOneProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateSimpleProperty; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateCustomProperty; +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateTenantIdProperty; import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateToManyProperty; -@SuppressWarnings("PMD.DataflowAnomalyAnalysis") public class GrailsPropertyBinder { private static final Logger LOG = LoggerFactory.getLogger(GrailsPropertyBinder.class); @@ -66,16 +67,16 @@ public class GrailsPropertyBinder { public Value bindProperty( @Nonnull HibernatePersistentProperty currentGrailsProp, HibernatePersistentProperty parentProperty, String path) { Table table = currentGrailsProp.getTable(); - PersistentClass persistentClass = currentGrailsProp.getHibernateOwner().getPersistentClass(); if (LOG.isDebugEnabled()) { LOG.debug("[GrailsPropertyBinder] Binding persistent property [" + currentGrailsProp.getName() + "]"); } Value value; - // 1. Create Value and apply binders (consolidated block) if (currentGrailsProp instanceof HibernateEnumProperty hibernateEnumProperty) { - value = enumTypeBinder.bindEnumType(hibernateEnumProperty, path); + value = enumTypeBinder.bindEnumType(hibernateEnumProperty, table, path); + } else if (currentGrailsProp.isUserButNotCollectionType()) { + value = simpleValueBinder.bindSimpleValue(currentGrailsProp, parentProperty, table, path); } else if (currentGrailsProp instanceof HibernateOneToOneProperty oneToOne && oneToOne.isValidHibernateOneToOne()) { value = oneToOneBinder.bindOneToOne(oneToOne, path); @@ -88,9 +89,16 @@ public class GrailsPropertyBinder { value = collectionBinder.bindCollection(toMany, path); } else if (currentGrailsProp instanceof HibernateEmbeddedProperty embedded) { value = componentBinder.bindComponent(embedded, path); + } else if (currentGrailsProp instanceof HibernateSimpleProperty simple) { + value = simpleValueBinder.bindSimpleValue(simple, parentProperty, table, path); + } else if (currentGrailsProp instanceof HibernateCustomProperty custom) { + value = simpleValueBinder.bindSimpleValue(custom, parentProperty, table, path); + } else if (currentGrailsProp instanceof HibernateTenantIdProperty tenantId) { + value = simpleValueBinder.bindSimpleValue(tenantId, parentProperty, table, path); + } else if (currentGrailsProp instanceof HibernateToManyProperty toMany && currentGrailsProp.isSerializableType()) { + value = simpleValueBinder.bindSimpleValue(toMany, parentProperty, table, path); } else { - // HibernateSimpleProperty - value = simpleValueBinder.bindSimpleValue(currentGrailsProp, parentProperty, table, path); + throw new RuntimeException("Unsupported property type: " + currentGrailsProp.getClass().getName()); } return value; diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java index fc0963f3c3..f4db6db1be 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/collectionType/CollectionType.java @@ -45,7 +45,10 @@ public abstract class CollectionType { public Collection create(HibernateToManyProperty property, PersistentClass owner) throws MappingException { Collection coll = createCollection(owner); coll.setCollectionTable(owner.getTable()); - coll.setTypeName(getTypeName(property)); + String typeName = getTypeName(property); + if (typeName != null && !clazz.getName().equals(typeName)) { + coll.setTypeName(typeName); + } return coll; } diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java new file mode 100644 index 0000000000..ece144f317 --- /dev/null +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.orm.hibernate.query; + +import org.grails.datastore.mapping.query.Query; + +/** + * A internal criterion used to represent an alias for a basic collection join. + * + * @author walterduquedeestrada + */ +public class HibernateAlias implements Query.Criterion, Query.QueryElement { + private final String path; + private final String alias; + + public HibernateAlias(String path, String alias) { + this.path = path; + this.alias = alias; + } + + public String getPath() { + return path; + } + + public String getAlias() { + return alias; + } +} diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java index 7cd7b15858..58bf79ea26 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java @@ -81,10 +81,20 @@ public class JpaFromProvider implements Cloneable { .toList(); var aliasMap = createAliasMap(detachedAssociationCriteriaList); + + // Also scan for HibernateAlias (basic collections) + Map<String, String> basicAliasMap = new HashMap<>(); + for (Query.Criterion c : detachedCriteria.getCriteria()) { + if (c instanceof HibernateAlias ha) { + basicAliasMap.put(ha.getPath(), ha.getAlias()); + } + } + var definedAliases = detachedAssociationCriteriaList.stream() .map(DetachedAssociationCriteria::getAlias) .filter(Objects::nonNull) .collect(Collectors.toSet()); + definedAliases.addAll(basicAliasMap.values()); var directProjectedPaths = projections.stream() .filter(Query.PropertyProjection.class::isInstance) @@ -98,12 +108,19 @@ public class JpaFromProvider implements Cloneable { .map(Map.Entry::getKey) .collect(Collectors.toSet()); + var collectionPaths = detachedCriteria.getPersistentEntity().getPersistentProperties().stream() + .filter(p -> p instanceof org.grails.datastore.mapping.model.types.Basic) + .map(org.grails.datastore.mapping.model.PersistentProperty::getName) + .collect(Collectors.toSet()); + java.util.Set<String> allPaths = new java.util.HashSet<>(); allPaths.addAll(aliasMap.keySet()); + allPaths.addAll(basicAliasMap.keySet()); allPaths.addAll(directProjectedPaths.stream() .filter(p -> !definedAliases.contains(p)) .toList()); allPaths.addAll(eagerPaths); + allPaths.addAll(collectionPaths); // Expand paths to include all parents (e.g., "a.b.c" -> "a", "a.b", "a.b.c") java.util.Set<String> expandedPaths = new java.util.HashSet<>(); @@ -143,7 +160,7 @@ public class JpaFromProvider implements Cloneable { JoinType joinType = JoinType.INNER; if (detachedCriteria.getJoinTypes().containsKey(path)) { joinType = detachedCriteria.getJoinTypes().get(path); - } else if (finalProjectedPaths.contains(path) || eagerPaths.contains(path)) { + } else if (finalProjectedPaths.contains(path) || eagerPaths.contains(path) || collectionPaths.contains(path)) { joinType = JoinType.LEFT; } @@ -154,6 +171,11 @@ public class JpaFromProvider implements Cloneable { if (dac != null && dac.getAlias() != null) { fromsByPath.put(dac.getAlias(), table); } + + String basicAlias = basicAliasMap.get(path); + if (basicAlias != null) { + fromsByPath.put(basicAlias, table); + } table.alias(path); fromsByPath.put(path, table); 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 5d2f946ae0..16cfd1c1b3 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 @@ -268,6 +268,19 @@ public class PredicateGenerator { Collection newValues = gormEntities.stream().map(GormEntity::ident).toList(); return cb.in(id, newValues); } + + // Hibernate 7: If the path is a collection, we must ensure it's correctly handled + if (fullyQualifiedPath instanceof SqmPath sqmPath && sqmPath.getReferencedPathSource() instanceof jakarta.persistence.metamodel.PluralAttribute) { + // For basic collections, GORM's 'in' traditionally implies joining. + // We'll check if the path is already a join (From) + if (fullyQualifiedPath instanceof From) { + return cb.in(fullyQualifiedPath, c.getValues()); + } + // If not joined yet, we may need to use 'elements' or MEMBER OF + // but usually JpaFromProvider should have joined it if it was a property path + // that refers to a collection. + } + return cb.in(fullyQualifiedPath, c.getValues()); } return null; diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy index ce424454af..9e322f354e 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy @@ -19,8 +19,9 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { private JpaFromProvider bare(Class clazz, From root) { def dc = new DetachedCriteria(clazz) - def cq = Mock(JpaCriteriaQuery) { - from(clazz) >> root + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { + getJavaType() >> String + alias(_) >> it } return new JpaFromProvider(dc, [], root) } @@ -28,9 +29,11 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { def "getFromsByName returns root for 'root' key"() { given: From root = Mock(From) { - getJavaType() >> String + getJavaType() >> JpaFromProviderSpecPerson } - JpaFromProvider provider = bare(String, root) + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } + + JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root) expect: provider.getFromsByName().get("root") == root @@ -39,22 +42,26 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { def "getFullyQualifiedPath returns root for entity name if it matches root"() { given: From root = Mock(From) { - getJavaType() >> String + getJavaType() >> JpaFromProviderSpecPerson } - JpaFromProvider provider = bare(String, root) + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } + + JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root) expect: - provider.getFullyQualifiedPath("String") == root + provider.getFullyQualifiedPath("JpaFromProviderSpecPerson") == root } def "getFullyQualifiedPath returns root for 'root' prefix"() { given: Path idPath = Mock(Path) From root = Mock(From) { - getJavaType() >> String + getJavaType() >> JpaFromProviderSpecPerson get("id") >> idPath } - JpaFromProvider provider = bare(String, root) + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } + + JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root) expect: provider.getFullyQualifiedPath("root.id") == idPath @@ -63,7 +70,9 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { def "getFullyQualifiedPath throws for null property name"() { given: From root = Mock(From) - JpaFromProvider provider = bare(String, root) + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } + + JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root) when: provider.getFullyQualifiedPath(null) @@ -75,9 +84,11 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { def "clone produces an independent copy that does not affect original"() { given: From root = Mock(From) { - getJavaType() >> String + getJavaType() >> JpaFromProviderSpecPerson } - JpaFromProvider provider = bare(String, root) + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } + + JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root) From extra = Mock(From) when: @@ -92,9 +103,11 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { def "put overwrites an existing key"() { given: From root = Mock(From) { - getJavaType() >> String + getJavaType() >> JpaFromProviderSpecPerson } - JpaFromProvider provider = bare(String, root) + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } + + JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root) From newRoot = Mock(From) when: @@ -108,14 +121,13 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { given: Path idPath = Mock(Path) From root = Mock(From) { - getJavaType() >> String + getJavaType() >> JpaFromProviderSpecPerson get("id") >> idPath } - def dc = new DetachedCriteria(String) + root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } + + def dc = new DetachedCriteria(JpaFromProviderSpecPerson) dc.setAlias("myAlias") - def cq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery) { - from(_) >> root - } JpaFromProvider provider = new JpaFromProvider(dc, [], root) when: @@ -127,11 +139,13 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { def "getFromsByName creates hierarchical joins for projection paths"() { given: - def dc = new DetachedCriteria(String) - def cq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery) + def dc = new DetachedCriteria(JpaFromProviderSpecPerson) From root = Mock(From) { - getJavaType() >> String + getJavaType() >> JpaFromProviderSpecPerson } + // Stub for auto-joined basic collections + root.join("nicknames", _) >> Mock(Join) { alias(_) >> it } + Join teamJoin = Mock(Join) { getJavaType() >> String alias(_) >> it @@ -161,13 +175,13 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { def "constructor with parent provider inherits froms and supports correlation"() { given: - From outerRoot = Mock(From) { getJavaType() >> String } - JpaFromProvider parent = bare(String, outerRoot) + From outerRoot = Mock(From) { getJavaType() >> JpaFromProviderSpecPerson } + JpaFromProvider parent = bare(JpaFromProviderSpecPerson, outerRoot) and: "subquery detached criteria" - def subDc = new DetachedCriteria(Integer) - def subCq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery) - From subRoot = Mock(From) { getJavaType() >> Integer } + def subDc = new DetachedCriteria(JpaFromProviderSpecPet) + From subRoot = Mock(From) { getJavaType() >> JpaFromProviderSpecPet } + subRoot.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it } when: JpaFromProvider subProvider = new JpaFromProvider(parent, subDc, [], subRoot) @@ -178,12 +192,35 @@ class JpaFromProviderSpec extends HibernateGormDatastoreSpec { and: "subquery provider inherits outer paths" subProvider.getFullyQualifiedPath("root") != outerRoot // subquery root shadows outer root } + + def "getFromsByName automatically joins basic collections"() { + given: + def dc = new DetachedCriteria(JpaFromProviderSpecPerson) + From root = Mock(From) { + getJavaType() >> JpaFromProviderSpecPerson + } + Join nicknamesJoin = Mock(Join) { + getJavaType() >> String + alias(_) >> it + } + + when: + JpaFromProvider provider = new JpaFromProvider(dc, [], root) + + then: "basic collection is joined automatically" + 1 * root.join("nicknames", jakarta.persistence.criteria.JoinType.LEFT) >> nicknamesJoin + + and: "path is registered" + provider.getFullyQualifiedPath("nicknames") == nicknamesJoin + } } @Entity class JpaFromProviderSpecPerson implements GormEntity<JpaFromProviderSpecPerson> { Long id String firstName + Set<String> nicknames + static hasMany = [nicknames: String] } @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 5fbd821263..ac0f2b5da9 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 @@ -164,6 +164,21 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec { then: predicates.length == 1 } + + def "test getPredicates with In on basic collection"() { + given: + List criteria = [new Query.In("nicknames", ["Bob", "Alice"])] + + // Ensure nicknames is joined in fromProvider + fromProvider = new JpaFromProvider(new DetachedCriteria(PredicateGeneratorSpecPerson), [], root) + + when: + def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity) + + then: + predicates.length == 1 + predicates[0] instanceof org.hibernate.query.sqm.tree.predicate.SqmInListPredicate + } } @Entity @@ -173,7 +188,8 @@ class PredicateGeneratorSpecPerson implements GormEntity<PredicateGeneratorSpecP String lastName Integer age PredicateGeneratorSpecFace face - static hasMany = [pets: PredicateGeneratorSpecPet] + Set<String> nicknames + static hasMany = [pets: PredicateGeneratorSpecPet, nicknames: String] } @Entity diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy index 1cfeefeb6b..b21b1050e4 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy @@ -227,6 +227,16 @@ class CriteriaMethodInvokerSpec extends Specification { 1 * builder.singleResult() } + void "trySimpleCriteria: createAlias delegates to builder.createAlias"() { + when: + invoker.trySimpleCriteria('createAlias', CriteriaMethods.CREATE_ALIAS, ['transactions', 't'] as Object[]) + invoker.trySimpleCriteria('createAlias', CriteriaMethods.CREATE_ALIAS, ['transactions', 't', 0] as Object[]) + + then: + 1 * builder.createAlias('transactions', 't') + 1 * builder.createAlias('transactions', 't', 0) + } + void "tryPropertyCriteria: fetchMode delegates to builder.fetchMode"() { when: invoker.tryPropertyCriteria(CriteriaMethods.FETCH_MODE, ["transactions", org.hibernate.FetchMode.JOIN] as Object[]) 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 b9539828d5..07d8f0ba55 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 @@ -482,6 +482,14 @@ class HibernateCriteriaBuilderDirectSpec extends HibernateGormDatastoreSpec { expect: builder.select("balance").is(builder) } + def "createAlias(String, String) delegates and returns this"() { + expect: builder.createAlias("transactions", "t").is(builder) + } + + def "createAlias(String, String, int) delegates and returns this"() { + expect: builder.createAlias("transactions", "t", 0).is(builder) + } + // ─── Cache / readOnly / lock ─────────────────────────────────────────── def "cache(boolean) sets flag and returns this"() { diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy index 784b99ee0f..ee459a15ae 100644 --- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSpec.groovy @@ -252,7 +252,18 @@ class HibernateCriteriaBuilderSpec extends HibernateGormDatastoreSpec { results[0].firstName == "Fred" } - // ─── Logical combinators ─────────────────────────────────────────────── + void "createAlias defines an explicit join with an alias"() { + when: + def results = c.list { + createAlias("transactions", "t") + gt("t.amount", BigDecimal.valueOf(40)) + } + then: + results.size() == 1 + results[0].firstName == "Barney" + } + + // ─── logical combinators ─────────────────────────────────────────────── /** * {@code and} / {@code or} / {@code not} — logical grouping of predicates. diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy index 57ea04a290..1469524bd6 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy @@ -113,6 +113,18 @@ class EnumTypeBinderSpec extends HibernateGormDatastoreSpec { Person02 | true Clown01 | true } + + def "should bind enum type with explicit table"() { + given: "A root entity and its enum property" + def table = new Table("explicit_table") + def property = setupProperty(Person01, "status", new Table("internal")) + + when: "the enum is bound with an explicit table" + def simpleValue = binder.bindEnumType(property as HibernateEnumProperty, table, "myPath") + + then: "the provided table is used instead of the property's internal table" + simpleValue.getTable() == table + } } // --- Supporting Classes --- diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy index 8365962723..93b2e122b8 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsPropertyBinderSpec.groovy @@ -21,65 +21,33 @@ package org.grails.orm.hibernate.cfg.domainbinding import grails.gorm.annotation.Entity import grails.gorm.specs.HibernateGormDatastoreSpec -import org.grails.orm.hibernate.cfg.domainbinding.hibernate.GrailsHibernatePersistentEntity -import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty -import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder +import org.grails.orm.hibernate.cfg.domainbinding.hibernate.* +import org.grails.orm.hibernate.cfg.domainbinding.binder.* import org.hibernate.mapping.ManyToOne +import org.hibernate.mapping.OneToOne import org.hibernate.mapping.Property import org.hibernate.mapping.RootClass import org.hibernate.mapping.Value - -import org.grails.orm.hibernate.cfg.domainbinding.binder.CollectionBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.ClassBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.ComponentBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.ComponentUpdater -import org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsPropertyBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.ForeignKeyOneToOneBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.OneToOneBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.SubClassBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.SubclassMappingBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.RootBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.RootPersistentClassCommonValuesBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.DiscriminatorPropertyBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.ColumnConfigToColumnBinder -import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover -import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher -import org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder -import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher -import org.grails.orm.hibernate.cfg.domainbinding.util.PropertyFromValueCreator - import org.hibernate.mapping.BasicValue +import org.hibernate.mapping.Collection +import org.hibernate.mapping.Component +import org.hibernate.mapping.SimpleValue +import org.hibernate.mapping.Table import org.hibernate.boot.spi.MetadataBuildingContext import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment -import org.grails.orm.hibernate.cfg.domainbinding.binder.ManyToOneValuesBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdentifierToManyToOneBinder import org.grails.orm.hibernate.cfg.domainbinding.util.SimpleValueColumnFetcher -import org.grails.orm.hibernate.cfg.domainbinding.binder.IdentityBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.VersionBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.CompositeIdBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleIdBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.NaturalIdentifierBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.PropertyBinder -import org.grails.orm.hibernate.cfg.domainbinding.util.BasicValueIdCreator import org.hibernate.boot.spi.InFlightMetadataCollector - -import org.grails.orm.hibernate.cfg.domainbinding.binder.ClassPropertiesBinder -import org.grails.orm.hibernate.cfg.domainbinding.util.MultiTenantFilterBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.JoinedSubClassBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.UnionSubclassBinder -import org.grails.orm.hibernate.cfg.domainbinding.binder.SingleTableSubclassBinder +import org.grails.orm.hibernate.cfg.domainbinding.util.PropertyFromValueCreator +import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher +import org.grails.orm.hibernate.cfg.domainbinding.util.DefaultColumnNameFetcher +import org.grails.orm.hibernate.cfg.domainbinding.util.BackticksRemover import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.EMPTY_PATH class GrailsPropertyBinderSpec extends HibernateGormDatastoreSpec { - - protected Map getBinders(GrailsDomainBinder binder, InFlightMetadataCollector collector = getCollector()) { MetadataBuildingContext metadataBuildingContext = binder.getMetadataBuildingContext() PersistentEntityNamingStrategy namingStrategy = binder.getNamingStrategy() @@ -87,12 +55,11 @@ class GrailsPropertyBinderSpec extends HibernateGormDatastoreSpec { BackticksRemover backticksRemover = new BackticksRemover() DefaultColumnNameFetcher defaultColumnNameFetcher = new DefaultColumnNameFetcher(namingStrategy, backticksRemover) ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher = new ColumnNameForPropertyAndPathFetcher(namingStrategy, defaultColumnNameFetcher, backticksRemover) - CollectionHolder collectionHolder = new CollectionHolder(metadataBuildingContext) + SimpleValueBinder simpleValueBinder = new SimpleValueBinder(metadataBuildingContext, namingStrategy, jdbcEnvironment) - EnumTypeBinder enumTypeBinderToUse = new EnumTypeBinder(metadataBuildingContext, columnNameForPropertyAndPathFetcher,namingStrategy) + EnumTypeBinder enumTypeBinderToUse = new EnumTypeBinder(metadataBuildingContext, columnNameForPropertyAndPathFetcher, namingStrategy) SimpleValueColumnFetcher simpleValueColumnFetcher = new SimpleValueColumnFetcher() CompositeIdentifierToManyToOneBinder compositeIdentifierToManyToOneBinder = new CompositeIdentifierToManyToOneBinder( - new org.grails.orm.hibernate.cfg.domainbinding.util.ForeignKeyColumnCountCalculator(), namingStrategy, defaultColumnNameFetcher, @@ -105,15 +72,13 @@ class GrailsPropertyBinderSpec extends HibernateGormDatastoreSpec { CollectionBinder collectionBinder = new CollectionBinder( metadataBuildingContext, - namingStrategy - , + namingStrategy, simpleValueBinder, enumTypeBinderToUse, manyToOneBinder, compositeIdentifierToManyToOneBinder, - simpleValueColumnFetcher - , - collectionHolder, + simpleValueColumnFetcher, + new org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder(metadataBuildingContext), collector ) PropertyFromValueCreator propertyFromValueCreator = new PropertyFromValueCreator() @@ -124,519 +89,241 @@ class GrailsPropertyBinderSpec extends HibernateGormDatastoreSpec { componentUpdater ) GrailsPropertyBinder propertyBinder = new GrailsPropertyBinder( - - enumTypeBinderToUse, componentBinder, collectionBinder, - simpleValueBinder - , + simpleValueBinder, oneToOneBinder, manyToOneBinder, foreignKeyOneToOneBinder - ) componentBinder.setGrailsPropertyBinder(propertyBinder) - CompositeIdBinder compositeIdBinder = new CompositeIdBinder(metadataBuildingContext, componentUpdater, propertyBinder); - PropertyBinder propertyBinderHelper = new PropertyBinder() - SimpleIdBinder simpleIdBinder = new SimpleIdBinder(metadataBuildingContext, new BasicValueIdCreator(jdbcEnvironment, namingStrategy), simpleValueBinder, propertyBinderHelper) - IdentityBinder identityBinder = new IdentityBinder(simpleIdBinder, compositeIdBinder) - VersionBinder versionBinder = new VersionBinder(metadataBuildingContext, simpleValueBinder, propertyBinderHelper, BasicValue::new) - NaturalIdentifierBinder naturalIdentifierBinder = new NaturalIdentifierBinder() - ClassBinder classBinder = new ClassBinder(collector) - ClassPropertiesBinder classPropertiesBinder = new ClassPropertiesBinder(propertyBinder, propertyFromValueCreator, naturalIdentifierBinder) - MultiTenantFilterBinder multiTenantFilterBinder = new MultiTenantFilterBinder(new org.grails.orm.hibernate.cfg.domainbinding.util.GrailsPropertyResolver(), new org.grails.orm.hibernate.cfg.domainbinding.util.MultiTenantFilterDefinitionBinder(), collector, defaultColumnNameFetcher) - JoinedSubClassBinder joinedSubClassBinder = new JoinedSubClassBinder(metadataBuildingContext, namingStrategy, new org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), columnNameForPropertyAndPathFetcher, classBinder, collector) - UnionSubclassBinder unionSubclassBinder = new UnionSubclassBinder(metadataBuildingContext, namingStrategy, classBinder, collector) - SingleTableSubclassBinder singleTableSubclassBinder = new SingleTableSubclassBinder(classBinder, metadataBuildingContext) - - SubclassMappingBinder subclassMappingBinder = new SubclassMappingBinder(joinedSubClassBinder, unionSubclassBinder, singleTableSubclassBinder, classPropertiesBinder) - SubClassBinder subClassBinder = new SubClassBinder(subclassMappingBinder, multiTenantFilterBinder, "dataSource") - RootPersistentClassCommonValuesBinder rootPersistentClassCommonValuesBinder = new RootPersistentClassCommonValuesBinder(metadataBuildingContext, namingStrategy, identityBinder, versionBinder, classBinder, classPropertiesBinder, collector) - DiscriminatorPropertyBinder discriminatorPropertyBinder = new DiscriminatorPropertyBinder(metadataBuildingContext, binder.getMappingCacheHolder(), new org.grails.orm.hibernate.cfg.domainbinding.binder.ConfiguredDiscriminatorBinder(new org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder(), new ColumnConfigToColumnBinder()), new org.grails.orm.hibernate.cfg.domainbinding.binder.DefaultDiscriminatorBinder(new org.grails.orm.hibernate.cfg.domainbinding.binder.Si [...] - RootBinder rootBinder = new RootBinder("default", multiTenantFilterBinder, subClassBinder, rootPersistentClassCommonValuesBinder, discriminatorPropertyBinder, collector, binder.getMappingCacheHolder()) - return [ propertyBinder: propertyBinder, - collectionBinder: collectionBinder, - identityBinder: identityBinder, - versionBinder: versionBinder, - defaultColumnNameFetcher: defaultColumnNameFetcher, - columnNameForPropertyAndPathFetcher: columnNameForPropertyAndPathFetcher, - classBinder: classBinder, - classPropertiesBinder: classPropertiesBinder, - multiTenantFilterBinder: multiTenantFilterBinder, - naturalIdentifierBinder: naturalIdentifierBinder, - joinedSubClassBinder: joinedSubClassBinder, - unionSubclassBinder: unionSubclassBinder, - singleTableSubclassBinder: singleTableSubclassBinder, - subClassBinder: subClassBinder, - rootBinder: rootBinder + collectionBinder: collectionBinder ] } - protected void bindRoot(GrailsDomainBinder binder, GrailsHibernatePersistentEntity entity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { - def binders = getBinders(binder, mappings) - binders.rootBinder.bindRoot(entity) + protected void bindRoot(GrailsDomainBinder binder, GrailsHibernatePersistentEntity entity, InFlightMetadataCollector mappings) { + entity.setPersistentClass(new RootClass(binder.getMetadataBuildingContext())) } + void setupSpec() { manager.addAllDomainClasses([ - org.apache.grails.data.testing.tck.domains.Pet, - org.apache.grails.data.testing.tck.domains.Person, - org.apache.grails.data.testing.tck.domains.PetType, - org.apache.grails.data.testing.tck.domains.PersonWithCompositeKey + PropertyBinderSpecSimpleBook, + PropertyBinderSpecEnumBook, + PropertyBinderSpecAuthor, + PropertyBinderSpecPet, + PropertyBinderSpecEmployee, + PropertyBinderSpecSerializableEntity, + PropertyBinderSpecCustomEntity, + PropertyBinderSpecCustomUserTypeCollection ]) } void "Test bind simple property"() { given: - def collector = getCollector() def binder = getGrailsDomainBinder() def propertyBinder = getBinders(binder).propertyBinder - - // 1. Create the entity metadata - def persistentEntity = createPersistentEntity(binder, "SimpleBook", [title: String], [:]) - - // 2. Setup the Hibernate mapping object + def persistentEntity = getPersistentEntity(PropertyBinderSpecSimpleBook) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(persistentEntity.name) - rootClass.setJpaEntityName(persistentEntity.name) - rootClass.setTable(collector.addTable(null, null, "SIMPLE_BOOK", null, false, binder.getMetadataBuildingContext())) - - // --- THE FIX: Bridge the GORM entity to the Hibernate RootClass --- - ((GrailsHibernatePersistentEntity)persistentEntity).setPersistentClass(rootClass) - // ------------------------------------------------------------------ + rootClass.setTable(new Table("SIMPLE_BOOK")) + persistentEntity.setPersistentClass(rootClass) when: def titleProp = persistentEntity.getPropertyByName("title") as HibernatePersistentProperty - // This call will now succeed because the table can be resolved through the bridge Value value = propertyBinder.bindProperty(titleProp, null, EMPTY_PATH) - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, titleProp)) then: - Property prop = rootClass.getProperty("title") - prop != null - prop.value instanceof org.hibernate.mapping.SimpleValue - ((org.hibernate.mapping.SimpleValue)prop.value).typeName == String.name + value instanceof BasicValue + ((BasicValue)value).typeName == String.name } void "Test bind enum property"() { given: - def collector = getCollector() def binder = getGrailsDomainBinder() def propertyBinder = getBinders(binder).propertyBinder - def persistentEntity = createPersistentEntity(binder, "EnumBook", [status: java.util.concurrent.TimeUnit], [:]) + def persistentEntity = getPersistentEntity(PropertyBinderSpecEnumBook) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(persistentEntity.name) - rootClass.setTable(collector.addTable(null, null, "ENUM_BOOK", null, false, binder.getMetadataBuildingContext())) - - // --- THE FIX: Bridge the GORM entity to the Hibernate RootClass --- - ((GrailsHibernatePersistentEntity)persistentEntity).setPersistentClass(rootClass) - // ------------------------------------------------------------------ - - def statusProp = persistentEntity.getPropertyByName("status") as HibernatePersistentProperty + rootClass.setTable(new Table("ENUM_BOOK")) + persistentEntity.setPersistentClass(rootClass) when: + def statusProp = persistentEntity.getPropertyByName("status") as HibernatePersistentProperty Value value = propertyBinder.bindProperty(statusProp, null, EMPTY_PATH) - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, statusProp)) then: - Property prop = rootClass.getProperty("status") - prop != null - prop.value instanceof BasicValue - // Default enum mapping uses Hibernate 7 native STRING style (no typeName) - ((BasicValue)prop.value).typeName == null - ((BasicValue)prop.value).enumerationStyle == jakarta.persistence.EnumType.STRING + value instanceof BasicValue + ((BasicValue)value).enumerationStyle == jakarta.persistence.EnumType.STRING } void "Test bind many-to-one"() { given: def binder = getGrailsDomainBinder() def propertyBinder = getBinders(binder).propertyBinder - def collector = getCollector() - - def petEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet) as GrailsHibernatePersistentEntity - def personEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Person) as GrailsHibernatePersistentEntity - + def persistentEntity = getPersistentEntity(PropertyBinderSpecPet) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(petEntity.name) - rootClass.setTable(collector.addTable(null, null, "PET", null, false, binder.getMetadataBuildingContext())) + rootClass.setTable(new Table("PET")) + persistentEntity.setPersistentClass(rootClass) when: - def ownerProp = petEntity.getPropertyByName("owner") as HibernatePersistentProperty + def ownerProp = persistentEntity.getPropertyByName("owner") as HibernatePersistentProperty Value value = propertyBinder.bindProperty(ownerProp, null, EMPTY_PATH) - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, ownerProp)) then: - Property prop = rootClass.getProperty("owner") - prop != null - prop.value instanceof ManyToOne - ((ManyToOne)prop.value).referencedEntityName == personEntity.name + value instanceof ManyToOne + ((ManyToOne)value).referencedEntityName == PropertyBinderSpecAuthor.name } - void "Test bind embedded property"() { + void "Test bind to-many collection"() { given: - def collector = getCollector() def binder = getGrailsDomainBinder() def propertyBinder = getBinders(binder).propertyBinder - - // 1. Create the entities - def persistentEntity = createPersistentEntity(binder, "Employee", [name: String, homeAddress: Address], [:], ["homeAddress"]) - - // 2. Setup Hibernate RootClass and Table + def persistentEntity = getPersistentEntity(PropertyBinderSpecAuthor) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(persistentEntity.name) - def table = collector.addTable(null, null, "EMPLOYEE", null, false, binder.getMetadataBuildingContext()) - rootClass.setTable(table) - - // 3. THE CRITICAL FIX: Link the GORM entity to the Hibernate RootClass - // This prevents the NPE on line 73 of GrailsPropertyBinder - ((GrailsHibernatePersistentEntity)persistentEntity).setPersistentClass(rootClass) + rootClass.setTable(new Table("AUTHOR")) + persistentEntity.setPersistentClass(rootClass) when: - def addressProp = persistentEntity.getPropertyByName("homeAddress") as HibernatePersistentProperty - - // We must also ensure the associated entity (Address) has its metadata cached/linked - // if the binder logic traverses into it - if (addressProp.getAssociatedEntity() instanceof GrailsHibernatePersistentEntity) { - ((GrailsHibernatePersistentEntity)addressProp.getAssociatedEntity()).setPersistentClass(rootClass) - } - - Value value = propertyBinder.bindProperty(addressProp, null, "") - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, addressProp)) + def petsProp = persistentEntity.getPropertyByName("pets") as HibernatePersistentProperty + Value value = propertyBinder.bindProperty(petsProp, null, EMPTY_PATH) then: - Property prop = rootClass.getProperty("homeAddress") - prop != null - prop.value instanceof org.hibernate.mapping.Component - - def component = prop.value as org.hibernate.mapping.Component - component.getComponentClassName() == Address.name + value instanceof org.hibernate.mapping.Set } - void "Test bind set collection"() { + void "Test bind embedded property"() { given: def binder = getGrailsDomainBinder() def propertyBinder = getBinders(binder).propertyBinder - def collector = getCollector() - - def personEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Person) as GrailsHibernatePersistentEntity - def petEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet) as GrailsHibernatePersistentEntity - + def persistentEntity = getPersistentEntity(PropertyBinderSpecEmployee) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(personEntity.name) - rootClass.setTable(collector.addTable(null, null, "PERSON", null, false, binder.getMetadataBuildingContext())) - - // --- FIX STARTS HERE --- - // Link the owner of the "pets" property - personEntity.setPersistentClass(rootClass) - - // Link the target entity of the collection - // (In a real app, Pet would have its own RootClass, but for a - // unit test, linking it to the current context is often enough) - petEntity.setPersistentClass(rootClass) - // ----------------------- + rootClass.setTable(new Table("EMPLOYEE")) + persistentEntity.setPersistentClass(rootClass) when: - def petsProp = personEntity.getPropertyByName("pets") as HibernatePersistentProperty - Value value = propertyBinder.bindProperty(petsProp, null, EMPTY_PATH) - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, petsProp)) + def addressProp = persistentEntity.getPropertyByName("address") as HibernatePersistentProperty + Value value = propertyBinder.bindProperty(addressProp, null, EMPTY_PATH) then: - Property prop = rootClass.getProperty("pets") - prop != null - prop.value instanceof org.hibernate.mapping.Set - def set = prop.value as org.hibernate.mapping.Set - set.element instanceof org.hibernate.mapping.OneToMany - (set.element as org.hibernate.mapping.OneToMany).referencedEntityName == petEntity.name + value instanceof Component + ((Component)value).componentClassName == PropertyBinderSpecAddress.name } - void "Test bind list collection"() { + void "Test bind serializable collection type"() { given: def binder = getGrailsDomainBinder() - def collector = getCollector() - def propertyBinder = getBinders(binder, collector).propertyBinder - def bookEntity = createPersistentEntity(ListBook) - def authorEntity = createPersistentEntity(ListAuthor) - - // Register referenced entity in Hibernate - bindRoot(binder, bookEntity, collector, "sessionFactory") - + def propertyBinder = getBinders(binder).propertyBinder + def persistentEntity = getPersistentEntity(PropertyBinderSpecSerializableEntity) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(authorEntity.name) - rootClass.setJpaEntityName(authorEntity.name) - rootClass.setTable(collector.addTable(null, null, "LIST_AUTHOR", null, false, binder.getMetadataBuildingContext())) - - // --- FIX STARTS HERE --- - // Link the GORM entity metadata to the Hibernate mapping object - ((GrailsHibernatePersistentEntity)authorEntity).setPersistentClass(rootClass) - // ----------------------- - - def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) - def idCol = new org.hibernate.mapping.Column("id") - rootClass.table.addColumn(idCol) - pk.addColumn(idCol) - rootClass.table.setPrimaryKey(pk) - collector.addEntityBinding(rootClass) + rootClass.setTable(new Table("SERIALIZABLE_ENTITY")) + persistentEntity.setPersistentClass(rootClass) when: - def booksProp = authorEntity.getPropertyByName("books") as HibernatePersistentProperty - Value value = propertyBinder.bindProperty(booksProp, null, EMPTY_PATH) - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, booksProp)) - collector.processSecondPasses(binder.getMetadataBuildingContext()) + def tagsProp = persistentEntity.getPropertyByName("tags") as HibernatePersistentProperty + Value value = propertyBinder.bindProperty(tagsProp, null, EMPTY_PATH) then: - Property prop = rootClass.getProperty("books") - prop != null - prop.value instanceof org.hibernate.mapping.List - def list = prop.value as org.hibernate.mapping.List - list.index != null - list.element != null + value instanceof BasicValue + ((BasicValue)value).typeName == "serializable" } - void "Test bind map collection"() { + void "Test bind custom property type"() { given: def binder = getGrailsDomainBinder() - def collector = getCollector() - def propertyBinder = getBinders(binder, collector).propertyBinder - - def bookEntity = createPersistentEntity(MapBook) - def authorEntity = createPersistentEntity(MapAuthor) - - // Register referenced entity in Hibernate - bindRoot(binder, bookEntity, collector, "sessionFactory") - - // Manually create RootClass for the main entity + def propertyBinder = getBinders(binder).propertyBinder + def persistentEntity = getPersistentEntity(PropertyBinderSpecCustomEntity) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(authorEntity.name) - rootClass.setJpaEntityName(authorEntity.name) - rootClass.setTable(collector.addTable(null, null, "MAP_AUTHOR", null, false, binder.getMetadataBuildingContext())) - - // --- STEP 1 & 2: Link the GORM entity to the Hibernate RootClass --- - ((GrailsHibernatePersistentEntity)authorEntity).setPersistentClass(rootClass) - // ------------------------------------------------------------------ - - def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) - def idCol = new org.hibernate.mapping.Column("id") - rootClass.table.addColumn(idCol) - pk.addColumn(idCol) - rootClass.table.setPrimaryKey(pk) - collector.addEntityBinding(rootClass) + rootClass.setTable(new Table("CUSTOM_ENTITY")) + persistentEntity.setPersistentClass(rootClass) when: - def booksProp = authorEntity.getPropertyByName("books") as HibernatePersistentProperty - // This call to bindProperty will now succeed because currentGrailsProp.getTable() can resolve the table - Value value = propertyBinder.bindProperty(booksProp, null, EMPTY_PATH) - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, booksProp)) - collector.processSecondPasses(binder.getMetadataBuildingContext()) + def dataProp = persistentEntity.getPropertyByName("data") as HibernatePersistentProperty + Value value = propertyBinder.bindProperty(dataProp, null, EMPTY_PATH) then: - Property prop = rootClass.getProperty("books") - prop != null - prop.value instanceof org.hibernate.mapping.Map - def map = prop.value as org.hibernate.mapping.Map - map.index != null - map.element != null + value instanceof BasicValue } - void "Test bind composite identifier"() { - given: - def binder = getGrailsDomainBinder() - def collector = getCollector() - - def personEntity = getPersistentEntity(org.apache.grails.data.testing.tck.domains.PersonWithCompositeKey) as GrailsHibernatePersistentEntity - - when: - bindRoot(binder, personEntity, collector, "sessionFactory") - def rootClass = collector.getEntityBinding(personEntity.name) - - then: - rootClass.identifier instanceof org.hibernate.mapping.Component - def identifier = rootClass.identifier as org.hibernate.mapping.Component - identifier.propertySpan == 2 - identifier.getProperty("firstName") != null - identifier.getProperty("lastName") != null - } - - // New test for OneToOne property binding - void "Test bind one-to-one property"() { + void "Test bind collection with custom UserType"() { given: def binder = getGrailsDomainBinder() def propertyBinder = getBinders(binder).propertyBinder - def collector = getCollector() - - def authorEntity = createPersistentEntity(AuthorWithOneToOne) as GrailsHibernatePersistentEntity - def bookEntity = createPersistentEntity(BookForOneToOne) as GrailsHibernatePersistentEntity - - // Register referenced entity in Hibernate (this creates a RootClass for Book) - bindRoot(binder, bookEntity, collector, "sessionFactory") - + def persistentEntity = getPersistentEntity(PropertyBinderSpecCustomUserTypeCollection) as GrailsHibernatePersistentEntity def rootClass = new RootClass(binder.getMetadataBuildingContext()) - rootClass.setEntityName(authorEntity.name) - rootClass.setJpaEntityName(authorEntity.name) - rootClass.setTable(collector.addTable(null, null, "AUTHOR_ONE_TO_ONE", null, false, binder.getMetadataBuildingContext())) - - // --- THE FIX: Bridge BOTH entities --- - // 1. Link the Author (Owner) to the manually created rootClass - authorEntity.setPersistentClass(rootClass) - - // 2. Link the Book (Child) to the RootClass created by bindRoot - def bookRootClass = collector.getEntityBinding(bookEntity.name) - bookEntity.setPersistentClass(bookRootClass) - // -------------------------------------- - - def pk = new org.hibernate.mapping.PrimaryKey(rootClass.table) - def idCol = new org.hibernate.mapping.Column("id") - rootClass.table.addColumn(idCol) - pk.addColumn(idCol) - rootClass.table.setPrimaryKey(pk) - collector.addEntityBinding(rootClass) + rootClass.setTable(new Table("CUSTOM_COLLECTION")) + persistentEntity.setPersistentClass(rootClass) when: - def childBookProp = authorEntity.getPropertyByName("childBook") as HibernatePersistentProperty - // Line 73 will now succeed - Value value = propertyBinder.bindProperty(childBookProp, null, EMPTY_PATH) - rootClass.addProperty(new PropertyFromValueCreator().createProperty(value, childBookProp)) - collector.processSecondPasses(binder.getMetadataBuildingContext()) + def categoriesProp = persistentEntity.getPropertyByName("categories") as HibernatePersistentProperty + Value value = propertyBinder.bindProperty(categoriesProp, null, EMPTY_PATH) then: - Property prop = rootClass.getProperty("childBook") - prop != null - prop.value instanceof org.hibernate.mapping.OneToOne - def oneToOne = prop.value as org.hibernate.mapping.OneToOne - oneToOne.referencedEntityName == bookEntity.name - } - - void "should use binders from public constructor"() { - given: - def metadataBuildingContext = Mock(org.hibernate.boot.spi.MetadataBuildingContext) - def namingStrategy = Mock(org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy) - // CollectionHolder is a Java record (final), so we instantiate it - def collectionType = new org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionType(Object.class, metadataBuildingContext) { - @Override - org.hibernate.mapping.Collection createCollection(org.hibernate.mapping.PersistentClass owner) { - return null - } - } - def collectionHolder = new org.grails.orm.hibernate.cfg.domainbinding.collectionType.CollectionHolder([(Object.class): collectionType]) - def enumTypeBinder = Mock(EnumTypeBinder) - def componentBinder = Mock(ComponentBinder) - def collectionBinder = Mock(CollectionBinder) - def propertyFromValueCreator = Mock(PropertyFromValueCreator) - def simpleValueBinder = Mock(SimpleValueBinder) - def columnNameForPropertyAndPathFetcher = Mock(ColumnNameForPropertyAndPathFetcher) - def oneToOneBinder = Mock(OneToOneBinder) - def manyToOneBinder = Mock(ManyToOneBinder) - def foreignKeyOneToOneBinder = Mock(ForeignKeyOneToOneBinder) - - // Instantiate GrailsPropertyBinder using the public constructor with necessary mocks - def propertyBinder = new GrailsPropertyBinder( - - - enumTypeBinder, - componentBinder, - collectionBinder, - simpleValueBinder - , - oneToOneBinder, - manyToOneBinder, - foreignKeyOneToOneBinder - - ) - - def mappings = Mock(org.hibernate.boot.spi.InFlightMetadataCollector) - metadataBuildingContext.getMetadataCollector() >> mappings - - def rootClass = new RootClass(metadataBuildingContext) - def currentGrailsProp = Mock(HibernatePersistentProperty) - def table = new org.hibernate.mapping.Table("TEST_TABLE") - rootClass.setTable(table) - - // Stubbing getTable() to return our table variable - currentGrailsProp.getTable() >> table - - // Mocking other necessary properties of currentGrailsProp - def mockOwner = Mock(GrailsHibernatePersistentEntity) - def mockMapping = new org.grails.orm.hibernate.cfg.Mapping() - mockMapping.setComment("test comment") // Provide a comment - currentGrailsProp.getHibernateOwner() >> mockOwner - mockOwner.getMappedForm() >> mockMapping // Return the Mapping object - - // Stubbing getOwner() to return mockOwner - currentGrailsProp.getOwner() >> mockOwner - mockOwner.isRoot() >> true // Stub isRoot() to prevent NPE in ColumnBinder - - // Mocking other necessary properties of currentGrailsProp - currentGrailsProp.getType() >> String.class - currentGrailsProp.getName() >> "title" - simpleValueBinder.bindSimpleValue(currentGrailsProp, null, table, EMPTY_PATH) >> new BasicValue(metadataBuildingContext, table) - - when: - // Capture the return value of bindProperty - def resultValue = propertyBinder.bindProperty(currentGrailsProp, null, EMPTY_PATH) - - then: - // Assert that bindProperty returns a Value object - resultValue instanceof Value + value instanceof BasicValue + !(value instanceof org.hibernate.mapping.Collection) } } - -// Define simple entities for the OneToOne test @Entity -class AuthorWithOneToOne { // Added 'static' +class PropertyBinderSpecSimpleBook { Long id - BookForOneToOne childBook - static hasOne = [childBook: BookForOneToOne] + String title } @Entity -class BookForOneToOne { // Added 'static' +class PropertyBinderSpecEnumBook { Long id - String title - AuthorWithOneToOne parentAuthor -} -class Address { - String city - String zip + java.util.concurrent.TimeUnit status } @Entity -class TestEntityWithSerializableCollection { +class PropertyBinderSpecAuthor { Long id - List<SerializableObject> serializableObjects - static mapping = { - serializableObjects type: 'serializable' - } + static hasMany = [pets: PropertyBinderSpecPet] } -class SerializableObject { - String data +@Entity +class PropertyBinderSpecPet { + Long id + PropertyBinderSpecAuthor owner } @Entity -class ListAuthor { +class PropertyBinderSpecEmployee { Long id - List<ListBook> books - static hasMany = [books: ListBook] + PropertyBinderSpecAddress address + static embedded = ['address'] +} + +class PropertyBinderSpecAddress implements Serializable { + String city } @Entity -class ListBook { +class PropertyBinderSpecSerializableEntity { Long id - String title + List<String> tags + static mapping = { + tags type: 'serializable' + } } @Entity -class MapAuthor { +class PropertyBinderSpecCustomEntity { Long id - Map<String, MapBook> books - static hasMany = [books: MapBook] + String data + static mapping = { + data type: 'org.hibernate.type.YesNoConverter' + } } @Entity -class MapBook { +class PropertyBinderSpecCustomUserTypeCollection { Long id - String title + Set<String> categories + static mapping = { + // Assume this class exists or is mocked + categories type: 'org.hibernate.type.YesNoConverter' + } } diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy index 60c06df7bb..bdedf6e6c1 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy @@ -29,7 +29,10 @@ class DetachedAssociationFunctionSpec extends Specification { def "apply returns list with criteria if it is DetachedAssociationCriteria"() { given: - def criteria = new DetachedAssociationCriteria(Object, "test") + def association = Mock(org.grails.datastore.mapping.model.types.Association) { + getName() >> "test" + } + def criteria = new DetachedAssociationCriteria(Object, association) when: def result = function.apply(criteria)
