You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/07/25 18:55:25 UTC
(grails-core) branch fix/enum-hasmany-ddl-16051 created (now 98a1acf30b)
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a change to branch fix/enum-hasmany-ddl-16051
in repository https://gitbox.apache.org/repos/asf/grails-core.git
at 98a1acf30b Fix DDL failure for hasMany collections of enums
This branch includes the following new commits:
new 98a1acf30b Fix DDL failure for hasMany collections of enums
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(grails-core) 01/01: Fix DDL failure for hasMany collections of enums
Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch fix/enum-hasmany-ddl-16051
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 98a1acf30ba59588b5a228ea10864592ef03d868
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Jul 25 13:53:51 2026 -0500
Fix DDL failure for hasMany collections of enums
HibernateBasicProperty.getTable() always resolved to the owning
entity's table instead of the collection's join table, so the enum
element column for a hasMany-of-enum property was bound to the wrong
table: it appeared (wrongly) on the owner and was missing from the
join table's own CREATE TABLE, breaking schema generation. Separately,
HibernateToManyProperty.joinTableColumName() derived the column name
from the enum's fully-qualified class name instead of its simple name.
Also introduces HibernateBasicEnumProperty so hasMany-of-enum elements
are recognized as HibernateEnumProperty like their singular
counterparts, collapsing EnumTypeBinder down to one bindEnumType()
entry point and letting each HibernateEnumProperty implementation
supply its own table, column name, nullability, and enum-storage
configuration instead of the binder branching on property shape.
Fixes #16051
Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
.../cfg/domainbinding/binder/EnumTypeBinder.java | 59 ++---------
.../domainbinding/binder/GrailsPropertyBinder.java | 6 +-
...operty.java => HibernateBasicEnumProperty.java} | 34 +++---
.../hibernate/HibernateBasicProperty.java | 13 +++
.../hibernate/HibernateEnumProperty.java | 34 +++++-
.../hibernate/HibernateMappingFactory.groovy | 7 +-
.../hibernate/HibernateToManyProperty.java | 8 +-
.../secondpass/BasicCollectionElementBinder.java | 9 +-
.../cfg/domainbinding/util/GrailsEnumType.java | 40 ++++++-
.../grails/gorm/tests/EnumHasManyDdlSpec.groovy | 117 +++++++++++++++++++++
.../cfg/domainbinding/EnumTypeBinderSpec.groovy | 5 +-
.../BasicCollectionElementBinderSpec.groovy | 11 +-
12 files changed, 254 insertions(+), 89 deletions(-)
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 30fe4d34ed..63f5e6b630 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
@@ -21,22 +21,16 @@ package org.grails.orm.hibernate.cfg.domainbinding.binder;
import java.util.Properties;
import jakarta.annotation.Nonnull;
-import jakarta.persistence.EnumType;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.mapping.BasicValue;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Table;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.grails.orm.hibernate.cfg.ColumnConfig;
-import org.grails.orm.hibernate.cfg.IdentityEnumType;
import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
import org.grails.orm.hibernate.cfg.PropertyConfig;
-import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateBasicProperty;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEnumProperty;
-import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentProperty;
import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;
import org.grails.orm.hibernate.cfg.domainbinding.util.GrailsEnumType;
@@ -44,7 +38,6 @@ import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBind
public class EnumTypeBinder {
- private static final Logger LOG = LoggerFactory.getLogger(EnumTypeBinder.class);
private final MetadataBuildingContext metadataBuildingContext;
private final ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher;
private final IndexBinder indexBinder;
@@ -77,60 +70,22 @@ public class EnumTypeBinder {
}
public BasicValue bindEnumType(@Nonnull HibernateEnumProperty property, String path) {
- String columnName = columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(property, path, null);
+ String columnName = property.resolveEnumColumnName(namingStrategy, columnNameForPropertyAndPathFetcher, path);
BasicValue simpleValue = new BasicValue(metadataBuildingContext, property.getTable());
- bindEnumType(property, property.getType(), simpleValue, columnName);
- return simpleValue;
- }
-
- public BasicValue bindEnumTypeForColumn(@Nonnull HibernateBasicProperty property) {
- String columnName = property.joinTableColumName(namingStrategy);
- BasicValue simpleValue = new BasicValue(metadataBuildingContext, property.getTable());
- bindEnumType(property, property.getComponentType(), simpleValue, columnName);
- return simpleValue;
- }
-
- protected void bindEnumType(
- HibernatePersistentProperty property, Class<?> propertyType, BasicValue simpleValue, String columnName) {
+ Class<?> propertyType = property.getEnumType();
PropertyConfig pc = property.getHibernateMappedForm();
- Properties enumProperties = new Properties();
- enumProperties.put(ENUM_CLASS_PROP, propertyType.getName());
String typeName = property.getTypeName(propertyType);
if (typeName != null) {
simpleValue.setTypeName(typeName);
} else {
- switch (GrailsEnumType.fromString(pc.getEnumType())) {
- case DEFAULT, STRING -> {
- // Hibernate 7 native string enum mapping: store by Enum.name() as VARCHAR.
- simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
- simpleValue.setEnumerationStyle(EnumType.STRING);
- }
- case ORDINAL -> {
- // Hibernate 7 native ordinal enum mapping: store by Enum.ordinal() as INTEGER.
- simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
- simpleValue.setEnumerationStyle(EnumType.ORDINAL);
- }
- case IDENTITY -> simpleValue.setTypeName(IdentityEnumType.class.getName());
- default -> throw new IllegalArgumentException("Unknown enum type: " + pc.getEnumType());
- }
+ GrailsEnumType.fromString(pc.getEnumType()).configure(simpleValue, propertyType);
}
+ Properties enumProperties = new Properties();
+ enumProperties.put(ENUM_CLASS_PROP, propertyType.getName());
simpleValue.setTypeParameters(enumProperties);
Column column = new Column();
- boolean isTablePerHierarchySubclass = property.getHibernateOwner().isTablePerHierarchySubclass();
- if (isTablePerHierarchySubclass) {
- // Properties on subclasses in a table-per-hierarchy strategy must be nullable.
- if (LOG.isDebugEnabled()) {
- LOG.debug(
- "[GrailsDomainBinder] Sub class property [{}] for column name [{}] forced to nullable",
- property.getName(),
- columnName);
- }
- column.setNullable(true);
- } else {
- column.setNullable(property.isNullable());
- }
-
+ column.setNullable(property.isEnumColumnNullable());
column.setValue(simpleValue);
column.setName(columnName);
Table t = simpleValue.getTable();
@@ -142,5 +97,7 @@ public class EnumTypeBinder {
indexBinder.bindIndex(columnName, column, columnConfig, t);
columnConfigToColumnBinder.bindColumnConfigToColumn(column, columnConfig, pc);
}
+ 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 9bc98e2316..abc499d1d7 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
@@ -75,7 +75,11 @@ public class GrailsPropertyBinder {
Value value;
- if (currentGrailsProp instanceof HibernateEnumProperty hibernateEnumProperty) {
+ if (currentGrailsProp instanceof HibernateEnumProperty hibernateEnumProperty &&
+ !(currentGrailsProp instanceof HibernateToManyProperty)) {
+ // A hasMany-of-enum property is also a HibernateEnumProperty, but it must still go
+ // through collectionBinder.bindCollection() below so its join table gets created;
+ // EnumTypeBinder only binds its element column, from BasicCollectionElementBinder.
value = enumTypeBinder.bindEnumType(hibernateEnumProperty, path);
} else if (currentGrailsProp.isUserButNotCollectionType()) {
value = simpleValueBinder.bindBasicValue(currentGrailsProp, parentProperty, path);
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicEnumProperty.java
similarity index 53%
copy from grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicProperty.java
copy to grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicEnumProperty.java
index 7aa1f98a41..58cc176603 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicProperty.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicEnumProperty.java
@@ -20,29 +20,37 @@ package org.grails.orm.hibernate.cfg.domainbinding.hibernate;
import java.beans.PropertyDescriptor;
-import org.hibernate.mapping.Collection;
-
import org.grails.datastore.mapping.model.MappingContext;
-import org.grails.datastore.mapping.model.types.mapping.BasicWithMapping;
-import org.grails.orm.hibernate.cfg.PropertyConfig;
-
-/** Hibernate implementation of {@link org.grails.datastore.mapping.model.types.Basic} */
-public class HibernateBasicProperty extends BasicWithMapping<PropertyConfig> implements HibernateToManyCollectionProperty {
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;
- private Collection collection;
+/**
+ * Hibernate basic collection element property whose element type is an enum. Created by {@link
+ * HibernateMappingFactory#createBasicCollection} when the collection's element type is an enum.
+ */
+public class HibernateBasicEnumProperty extends HibernateBasicProperty implements HibernateEnumProperty {
- public HibernateBasicProperty(
+ public HibernateBasicEnumProperty(
GrailsHibernatePersistentEntity entity, MappingContext context, PropertyDescriptor property) {
super(entity, context, property);
}
@Override
- public Collection getHibernateCollection() {
- return collection;
+ public Class<?> getEnumType() {
+ return getComponentType();
+ }
+
+ @Override
+ public String resolveEnumColumnName(
+ PersistentEntityNamingStrategy namingStrategy,
+ ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher,
+ String path) {
+ return joinTableColumName(namingStrategy);
}
+ /** A hasMany element column is always nullable, matching the non-enum sibling binding path. */
@Override
- public void setHibernateCollection(Collection collection) {
- this.collection = collection;
+ public boolean isEnumColumnNullable() {
+ return true;
}
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicProperty.java
index 7aa1f98a41..5b8e2e280f 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicProperty.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicProperty.java
@@ -21,6 +21,7 @@ package org.grails.orm.hibernate.cfg.domainbinding.hibernate;
import java.beans.PropertyDescriptor;
import org.hibernate.mapping.Collection;
+import org.hibernate.mapping.Table;
import org.grails.datastore.mapping.model.MappingContext;
import org.grails.datastore.mapping.model.types.mapping.BasicWithMapping;
@@ -45,4 +46,16 @@ public class HibernateBasicProperty extends BasicWithMapping<PropertyConfig> imp
public void setHibernateCollection(Collection collection) {
this.collection = collection;
}
+
+ /**
+ * For a basic (scalar or enum) collection element, the property's table is the
+ * collection's join table rather than the owning entity's table. Before the collection
+ * table has been assigned (e.g. while it is itself being computed), falls back to the
+ * owning entity's table, matching the pre-collection-binding default.
+ */
+ @Override
+ public Table getTable() {
+ Table collectionTable = collection != null ? collection.getCollectionTable() : null;
+ return collectionTable != null ? collectionTable : getPersistentClass().getTable();
+ }
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateEnumProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateEnumProperty.java
index a471622545..a6265b3366 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateEnumProperty.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateEnumProperty.java
@@ -18,18 +18,46 @@
*/
package org.grails.orm.hibernate.cfg.domainbinding.hibernate;
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;
+
/**
* Marker interface for Hibernate persistent properties whose Java type is an enum.
*
- * <p>Two concrete subtypes exist, corresponding to the two creation paths in {@link
+ * <p>Three concrete subtypes exist, corresponding to the three creation paths in {@link
* HibernateMappingFactory}:
*
* <ul>
* <li>{@link HibernateSimpleEnumProperty} — plain enum with no custom type marshaller
* <li>{@link HibernateCustomEnumProperty} — enum backed by a custom type marshaller
+ * <li>{@link HibernateBasicEnumProperty} — enum element of a {@code hasMany} basic collection
* </ul>
*
* <p>Use {@code instanceof HibernateEnumProperty} instead of {@code isEnumType()} to branch on
- * enum properties at binding time.
+ * enum properties at binding time. Each implementation resolves its own enum class and column
+ * name so {@link org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder} can bind any
+ * of them through a single code path.
*/
-public interface HibernateEnumProperty extends HibernatePersistentProperty {}
+public interface HibernateEnumProperty extends HibernatePersistentProperty {
+
+ /** The enum class to bind: the property's own type, or a basic collection's element type. */
+ default Class<?> getEnumType() {
+ return getType();
+ }
+
+ /** Resolves the column name to bind the enum value under. */
+ default String resolveEnumColumnName(
+ PersistentEntityNamingStrategy namingStrategy,
+ ColumnNameForPropertyAndPathFetcher columnNameForPropertyAndPathFetcher,
+ String path) {
+ return columnNameForPropertyAndPathFetcher.getColumnNameForPropertyAndPath(this, path, null);
+ }
+
+ /**
+ * Whether the enum column should allow NULL. Subclass properties in a table-per-hierarchy
+ * strategy must be nullable; otherwise this follows the property's own nullable constraint.
+ */
+ default boolean isEnumColumnNullable() {
+ return getHibernateOwner().isTablePerHierarchySubclass() || isNullable();
+ }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingFactory.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingFactory.groovy
index 4bc8430812..a69a6630a9 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingFactory.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateMappingFactory.groovy
@@ -167,10 +167,13 @@ class HibernateMappingFactory extends AbstractGormMappingFactory<Mapping, Proper
PersistentEntity entity, MappingContext context, PropertyDescriptor property, Class collectionType) {
if (entity instanceof GrailsHibernatePersistentEntity) {
GrailsHibernatePersistentEntity ghpEntity = (GrailsHibernatePersistentEntity) entity
- HibernateBasicProperty basic = new HibernateBasicProperty(ghpEntity, context, property)
+ boolean isEnumCollection = collectionType != null && collectionType.isEnum()
+ HibernateBasicProperty basic = isEnumCollection
+ ? new HibernateBasicEnumProperty(ghpEntity, context, property)
+ : new HibernateBasicProperty(ghpEntity, context, property)
basic.setMapping(createPropertyMapping(basic, entity))
CustomTypeMarshaller customTypeMarshaller = findCustomType(context, property.propertyType)
- if (collectionType != null && collectionType.isEnum()) {
+ if (isEnumCollection) {
customTypeMarshaller = findCustomType(context, collectionType)
if (customTypeMarshaller == null) {
customTypeMarshaller = findCustomType(context, Enum)
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java
index e94fdac439..27fee7a2f2 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java
@@ -226,12 +226,14 @@ public interface HibernateToManyProperty extends PropertyWithMapping<PropertyCon
String columnName;
if (present) {
columnName = joinColumnMappingOptional.get().getName();
+ } else if (referencedType.isEnum()) {
+ // Use the enum's simple name, not its fully-qualified name, so the column
+ // isn't named after the enum's package.
+ columnName = namingStrategy.resolveColumnName(referencedType.getSimpleName());
} else {
var clazz = namingStrategy.resolveColumnName(referencedType.getName());
var prop = namingStrategy.resolveTableName(getName());
- columnName = referencedType.isEnum() ?
- clazz :
- new BackticksRemover().apply(prop) + UNDERSCORE + new BackticksRemover().apply(clazz);
+ columnName = new BackticksRemover().apply(prop) + UNDERSCORE + new BackticksRemover().apply(clazz);
}
return columnName;
}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinder.java
index 576253a1e9..359eb34f96 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinder.java
@@ -31,8 +31,11 @@ import org.grails.orm.hibernate.cfg.domainbinding.binder.ColumnConfigToColumnBin
import org.grails.orm.hibernate.cfg.domainbinding.binder.EnumTypeBinder;
import org.grails.orm.hibernate.cfg.domainbinding.binder.SimpleValueColumnBinder;
import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateBasicProperty;
+import org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernateEnumProperty;
import org.grails.orm.hibernate.cfg.domainbinding.util.SimpleValueColumnFetcher;
+import static org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder.EMPTY_PATH;
+
/** Binds the element value for a basic (scalar or enum) collection. */
public class BasicCollectionElementBinder {
@@ -61,10 +64,10 @@ public class BasicCollectionElementBinder {
/** Creates and binds a {@link BasicValue} element for the given basic collection property. */
public BasicValue bind(@Nonnull HibernateBasicProperty property) {
- String columnName = property.joinTableColumName(namingStrategy);
- if (property.isEnum()) {
- return enumTypeBinder.bindEnumTypeForColumn(property);
+ if (property instanceof HibernateEnumProperty hibernateEnumProperty) {
+ return enumTypeBinder.bindEnumType(hibernateEnumProperty, EMPTY_PATH);
} else {
+ String columnName = property.joinTableColumName(namingStrategy);
final Class<?> referencedType = property.getComponentType();
String typeName = property.getTypeName(referencedType);
Collection collection = property.getCollection();
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/GrailsEnumType.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/GrailsEnumType.java
index 26842b02aa..1a2f0f0480 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/GrailsEnumType.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/util/GrailsEnumType.java
@@ -18,13 +18,42 @@
*/
package org.grails.orm.hibernate.cfg.domainbinding.util;
+import jakarta.persistence.EnumType;
+
import org.hibernate.MappingException;
+import org.hibernate.mapping.BasicValue;
+
+import org.grails.orm.hibernate.cfg.IdentityEnumType;
public enum GrailsEnumType {
- DEFAULT("default"),
- STRING("string"),
- ORDINAL("ordinal"),
- IDENTITY("identity");
+ DEFAULT("default") {
+ @Override
+ public void configure(BasicValue simpleValue, Class<?> propertyType) {
+ STRING.configure(simpleValue, propertyType);
+ }
+ },
+ // Hibernate 7 native string enum mapping: store by Enum.name() as VARCHAR.
+ STRING("string") {
+ @Override
+ public void configure(BasicValue simpleValue, Class<?> propertyType) {
+ simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
+ simpleValue.setEnumerationStyle(EnumType.STRING);
+ }
+ },
+ // Hibernate 7 native ordinal enum mapping: store by Enum.ordinal() as INTEGER.
+ ORDINAL("ordinal") {
+ @Override
+ public void configure(BasicValue simpleValue, Class<?> propertyType) {
+ simpleValue.setImplicitJavaTypeAccess(tc -> propertyType);
+ simpleValue.setEnumerationStyle(EnumType.ORDINAL);
+ }
+ },
+ IDENTITY("identity") {
+ @Override
+ public void configure(BasicValue simpleValue, Class<?> propertyType) {
+ simpleValue.setTypeName(IdentityEnumType.class.getName());
+ }
+ };
private final String type;
@@ -48,4 +77,7 @@ public enum GrailsEnumType {
public String getType() {
return type;
}
+
+ /** Configures the given {@link BasicValue} to store {@code propertyType} per this enum type. */
+ public abstract void configure(BasicValue simpleValue, Class<?> propertyType);
}
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/EnumHasManyDdlSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/EnumHasManyDdlSpec.groovy
new file mode 100644
index 0000000000..6fb71145c2
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/EnumHasManyDdlSpec.groovy
@@ -0,0 +1,117 @@
+/*
+ * 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 grails.gorm.tests
+
+import grails.gorm.annotation.Entity
+import grails.gorm.transactions.Rollback
+import org.grails.orm.hibernate.HibernateDatastore
+import org.hibernate.engine.spi.SessionImplementor
+import spock.lang.AutoCleanup
+import spock.lang.Issue
+import spock.lang.Shared
+import spock.lang.Specification
+
+import java.sql.ResultSet
+
+/**
+ * Reproduces https://github.com/apache/grails-core/issues/16051
+ *
+ * A domain with a `hasMany` collection whose related type is an enum (a
+ * Set of a basic/enum type, not an entity) produces broken join table DDL:
+ * the element column is bound against the owning entity's table instead of
+ * the join table, and its name is derived from the enum's fully-qualified
+ * class name instead of its simple name.
+ */
+@Rollback
+class EnumHasManyDdlSpec extends Specification {
+
+ @Shared @AutoCleanup HibernateDatastore datastore =
+ new HibernateDatastore(SurveyResponse)
+
+ @Issue("https://github.com/apache/grails-core/issues/16051")
+ void "join table for a hasMany of enum is created with the element column"() {
+ given:
+ SessionImplementor sessionImplementor = (SessionImplementor) datastore.sessionFactory.currentSession
+
+ expect: "the join table exists and has an answers column, not just the owner FK"
+ ResultSet columns = sessionImplementor.doReturningWork {
+ it.prepareStatement(
+ "select column_name from information_schema.columns " +
+ "where table_name = 'SURVEY_RESPONSE_ANSWERS'"
+ ).executeQuery()
+ }
+ Set<String> columnNames = []
+ while (columns.next()) {
+ columnNames << columns.getString('column_name').toLowerCase()
+ }
+ columnNames.contains('survey_response_id')
+ columnNames.any { it.contains('answer') }
+ }
+
+ @Issue("https://github.com/apache/grails-core/issues/16051")
+ void "the owner table does not get a spurious column for the hasMany enum element"() {
+ given:
+ SessionImplementor sessionImplementor = (SessionImplementor) datastore.sessionFactory.currentSession
+
+ expect: "no answer-related column leaked onto survey_response itself"
+ ResultSet columns = sessionImplementor.doReturningWork {
+ it.prepareStatement(
+ "select column_name from information_schema.columns " +
+ "where table_name = 'SURVEY_RESPONSE'"
+ ).executeQuery()
+ }
+ Set<String> columnNames = []
+ while (columns.next()) {
+ columnNames << columns.getString('column_name').toLowerCase()
+ }
+ !columnNames.any { it.contains('answer') }
+ }
+
+ @Issue("https://github.com/apache/grails-core/issues/16051")
+ void "a hasMany of enum can actually be saved and reloaded"() {
+ given:
+ def response = new SurveyResponse(respondent: "Alice")
+ response.addToAnswers(SurveyAnswer.MAYBE)
+ response.addToAnswers(SurveyAnswer.DONT_KNOW)
+
+ when:
+ response.save(flush: true)
+ response.discard()
+ def reloaded = SurveyResponse.get(response.id)
+
+ then:
+ reloaded.answers.sort() == [SurveyAnswer.MAYBE, SurveyAnswer.DONT_KNOW].sort()
+ }
+}
+
+@Entity
+class SurveyResponse {
+ String respondent
+ Set<SurveyAnswer> answers
+
+ static hasMany = [answers: SurveyAnswer]
+
+ static constraints = {
+ respondent blank: false
+ }
+}
+
+enum SurveyAnswer {
+ FOR_SURE, MAYBE, MAYBE_NOT, DONT_KNOW
+}
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 990260ba00..25c5f9ea1c 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
@@ -81,12 +81,13 @@ class EnumTypeBinderSpec extends HibernateGormDatastoreSpec {
def table = new Table("person_statuses")
def property = setupProperty(PersonWithCollection, "statuses", table)
- expect: "The property is a ToMany property"
+ expect: "The property is both a ToMany and an enum property"
property instanceof HibernateBasicProperty == true
+ property instanceof HibernateEnumProperty == true
when: "the enum is bound for the collection column"
// This will now successfully call property.getComponentType() internally
- def result = binder.bindEnumTypeForColumn(property as HibernateBasicProperty)
+ def result = binder.bindEnumType(property as HibernateEnumProperty, GrailsDomainBinder.EMPTY_PATH)
then: "The BasicValue is configured correctly"
result.getEnumerationStyle() == EnumType.STRING
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinderSpec.groovy
index 1715f03e76..d152df625c 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinderSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/BasicCollectionElementBinderSpec.groovy
@@ -76,7 +76,7 @@ class BasicCollectionElementBinderSpec extends HibernateGormDatastoreSpec {
element != null
element.getColumnSpan() > 0
// Ensure the enum binder is NOT called for a String collection
- 0 * enumTypeBinder.bindEnumTypeForColumn(_, _, _)
+ 0 * enumTypeBinder.bindEnumType(_, _)
}
void "bind delegates to enumTypeBinder for enum collection"() {
@@ -95,8 +95,7 @@ class BasicCollectionElementBinderSpec extends HibernateGormDatastoreSpec {
then:
element != null
- // Corrected: Match the 3-argument signature (Property, Class, String)
- 1 * enumTypeBinder.bindEnumTypeForColumn(property) >> mockValue
+ 1 * enumTypeBinder.bindEnumType(property, _) >> mockValue
}
void "test bind with custom column mapping and backticks"() {
@@ -182,10 +181,8 @@ class BasicCollectionElementBinderSpec extends HibernateGormDatastoreSpec {
when:
BasicValue element = binder.bind(property)
- then: "columnName is the resolved fully qualified Enum class name"
- // The namingStrategy resolves 'org.grails.orm.hibernate.cfg.domainbinding.secondpass.BCEBStatus'
- // to 'org_grails_orm_hibernate_cfg_domainbinding_secondpass_bcebstatus'
- 1 * enumTypeBinder.bindEnumTypeForColumn(property) >> mockValue
+ then: "the enum binder is delegated to for the enum element"
+ 1 * enumTypeBinder.bindEnumType(property, _) >> mockValue
element == mockValue
}