jdaugherty commented on code in PR #16052:
URL: https://github.com/apache/grails-core/pull/16052#discussion_r3652156240
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java:
##########
@@ -226,12 +226,14 @@ default String
joinTableColumName(PersistentEntityNamingStrategy namingStrategy)
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());
Review Comment:
`HibernateToManyPropertySpec` is the direct unit test for this method and it
was not touched. Its enum case currently asserts nothing useful:
```groovy
void "joinTableColumName returns derived column name for enum collection"() {
given:
def property = createTestHibernateToManyProperty(HTMPEntityWithEnum,
"statuses")
def namingStrategy = getGrailsDomainBinder().namingStrategy
expect:
property.joinTableColumName(namingStrategy) != null
}
```
That passed before this change and passes after it, so the behaviour you are
fixing here has no unit-level guard. Since the sibling case two features down
(`"joinTableColumName uses explicit join table column name when present"`)
already asserts an exact string, please pin this one the same way — `==
"htmp_status"` or whatever the strategy resolves for that enum's simple name.
That also documents the Grails 7 parity this restores.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/GrailsPropertyBinder.java:
##########
@@ -75,7 +75,11 @@ public Value bindProperty(
Value value;
- if (currentGrailsProp instanceof HibernateEnumProperty
hibernateEnumProperty) {
+ if (currentGrailsProp instanceof HibernateEnumProperty
hibernateEnumProperty &&
+ !(currentGrailsProp instanceof HibernateToManyProperty)) {
Review Comment:
This guard is correct but states the condition indirectly — it reads as
"enums that are not to-many" when what you mean is "not the new
basic-collection variant". It also silently depends on `HibernateBasicProperty
implements HibernateToManyCollectionProperty`, which is not obvious from here.
`!(currentGrailsProp instanceof HibernateBasicEnumProperty)` says the same
thing directly and is the condition that stays true if another
`HibernateEnumProperty` implementation is added later. Alternatively, put the
decision on the interface (e.g. a `bindsOwnValue()`/`isCollectionElement()`
default) so the binder does not have to enumerate implementations at all — that
is the pattern the rest of this PR follows for table, column name and
nullability.
##########
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') }
Review Comment:
`it.contains('answer')` is too loose to cover the naming half of the fix.
Under the old FQN behaviour the column was `grails_gorm_tests_survey_answer`,
which also satisfies `contains('answer')` — so this feature would have gone
green against the buggy `joinTableColumName` as long as the *table* half of the
fix were in place. Please assert the exact name:
```groovy
columnNames == ['survey_response_id', 'survey_answer'] as Set
```
Two smaller points on the same block:
- The `PreparedStatement`/`ResultSet` are never closed, and the `ResultSet`
is drained after `doReturningWork` has returned. It happens to work because the
session's connection is still open, but the idiomatic form is to do the whole
read inside the callback and return the `Set<String>` — which also lets both
features share one helper instead of duplicating the loop.
- The refactor moved `STRING`/`ORDINAL`/`IDENTITY` handling into
`GrailsEnumType.configure` and nullability into `isEnumColumnNullable`, but
every assertion here exercises the default (STRING) path. A case with `answers
enumType: 'ordinal'` and one with an explicit `joinTable column: [name: '...']`
would cover the branches this PR actually rewrote.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicEnumProperty.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * 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.cfg.domainbinding.hibernate;
+
+import java.beans.PropertyDescriptor;
+
+import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import
org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;
+
+/**
+ * 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 {
Review Comment:
Making this type a `HibernateEnumProperty` silently changes behaviour in
`PropertyBinder`, which is outside this diff:
```java
// PropertyBinder#bindProperty
if (persistentProperty instanceof Association<?> association &&
!(persistentProperty instanceof HibernateEnumProperty)) {
prop.setCascade(cascadeBehaviorFetcher.getCascadeBehaviour(association));
}
```
`HibernateBasicProperty` extends `BasicWithMapping` -> `Basic` -> `ToMany`
-> `Association`, so a `hasMany`-of-enum **is** an `Association`. Before this
PR the `!(... instanceof HibernateEnumProperty)` clause was unreachable (the
only two implementations extended
`HibernateSimpleProperty`/`HibernateCustomProperty`, neither of which is an
`Association`); after this PR it fires, and `prop.setCascade(...)` is no longer
called for enum collections.
Two concrete consequences:
1. `CascadeBehaviorFetcher.getImpliedBehavior()` has an explicit `if
(association instanceof Basic) return ALL;` branch, so `Set<String>` still gets
`cascade="all"` while `Set<SomeEnum>` now gets none — the two basic-collection
shapes have diverged for no stated reason.
2. An explicitly configured `answers cascade: 'all-delete-orphan'` in the
mapping block is read by `getDefinedBehavior()` and is now dropped on the floor
for enum collections only.
For a collection of value types the runtime blast radius is small (there is
no associated entity to cascade to), so this is unlikely to be what CI would
catch — but it is an unintended side effect, and the same class of side effect
applies to every `instanceof HibernateEnumProperty` site once a collection type
joins that hierarchy. Please either narrow the `PropertyBinder` guard to the
scalar enum types it was written for, or add an explicit test asserting the
cascade for `hasMany`-of-enum matches `hasMany`-of-String.
##########
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.
Review Comment:
This opening line is now wrong twice over: the interface is no longer a
marker (it carries three default methods), and `HibernateBasicEnumProperty`'s
Java type is a `Collection`, not an enum. Something like "Contract for
Hibernate persistent properties that bind an enum value — either the property's
own type or a basic collection's element type" would match what the interface
has become, and the list below it already spells out the three implementations.
##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateBasicEnumProperty.java:
##########
@@ -0,0 +1,56 @@
+/*
+ * 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.cfg.domainbinding.hibernate;
+
+import java.beans.PropertyDescriptor;
+
+import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy;
+import
org.grails.orm.hibernate.cfg.domainbinding.util.ColumnNameForPropertyAndPathFetcher;
+
+/**
+ * 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 HibernateBasicEnumProperty(
+ GrailsHibernatePersistentEntity entity, MappingContext context,
PropertyDescriptor property) {
+ super(entity, context, property);
+ }
+
+ @Override
+ 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 boolean isEnumColumnNullable() {
Review Comment:
This is a behaviour change that the PR description does not call out.
Previously `bindEnumTypeForColumn` reached the shared
`column.setNullable(property.isNullable())`, so a non-nullable enum collection
element got a `NOT NULL` column; now it is unconditionally nullable.
I think the new behaviour is the right one — it matches the `true` that
`BasicCollectionElementBinder` passes to
`simpleValueColumnBinder.bindSimpleValue(...)` on the non-enum branch, and it
matches how Hibernate treats `@ElementCollection` element columns — so this
reads as an intentional alignment rather than an accident. But it does change
generated DDL for anyone who declared `answers nullable: false`, so it deserves
a line in the PR body and a test pinning the resulting column's nullability.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]