jdaugherty commented on code in PR #16028:
URL: https://github.com/apache/grails-core/pull/16028#discussion_r3652288249


##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateAssociation.java:
##########
@@ -80,6 +81,10 @@ default String getReferencedEntityName() {
         return getHibernateAssociatedEntity().getName();
     }
 
+    default String 
resolveAssociatedEntityTableName(PersistentEntityNamingStrategy namingStrategy) 
{

Review Comment:
   This returns the table name verbatim, and the two call sites then treat it 
differently: `joinTableColumName` passes it through `BackticksRemover`, while 
`resolveJoinTableForeignKeyColumnName` concatenates `_id` onto it directly. 
`TableForManyCalculator.calculateTableForMany` also strips backticks from 
`getTableName(...)`, because backtick-quoting a reserved word in `table` is 
supported and used (e.g. `grails/gorm/tests/multitenancy/User` maps `table 
'`user`'`).
   
   With a quoted table on the far side of a unidirectional `hasMany`, the FK 
column name is now malformed:
   
   ```groovy
   @Entity class ProbeQuoted { String label
       static mapping = { table '`user`' } }
   
   @Entity class ProbeShelf { String label
       static hasMany = [quoted: ProbeQuoted] }
   ```
   
   ```
   Error executing DDL "create table probe_shelf_user (`user`_id bigint, 
probe_shelf_quoted_id bigint, unique (probe_shelf_quoted_id, `user`_id))"
     via JDBC [Unknown data type: "_ID"]
   ```
   
   On 8.0.x the same mapping produces `probe_shelf_user(probe_quoted_id, 
probe_shelf_quoted_id)`. Without `hibernate.hbm2ddl.halt_on_error` the 
statement fails silently and the join table is simply missing from the 
generated schema, which makes it an easy one to ship unnoticed.
   
   Stripping backticks here (or at the `resolveJoinTableForeignKeyColumnName` 
call site, matching `joinTableColumName`) fixes it. A test with a 
backtick-quoted `table` mapping would be worth adding alongside the two new 
cases.



##########
grails-doc/src/en/guide/upgrading/upgrading80x.adoc:
##########
@@ -1215,6 +1215,64 @@ GORM's `createCriteria()` and `withCriteria()` DSL are 
implemented on top of the
 
 *`javax.persistence` → `jakarta.persistence`*: This migration was already 
required for Grails 7; Grails 8 continues to require `jakarta.*`.
 
+===== 26.9 Many-to-Many Join-Table Column Names

Review Comment:
   As noted in the comment on 
`HibernateToManyProperty#resolveJoinTableForeignKeyColumnName`, a bidirectional 
many-to-many join table is not affected by this change: I checked the exact 
mapping used in this section's example (`Book` with `table 'catalog_book'`, 
`Author`/`Book` many-to-many) and both join columns are still derived from the 
class names on this branch, identical to 8.0.x.
   
   As written, this section asks many-to-many users to migrate or pin a schema 
that isn't changing — which is worse than no note at all, since the suggested 
`joinTable` mapping would itself be the schema change. It needs to be re-scoped 
to what actually changes (the element FK column of a unidirectional `hasMany` 
join table), or the code change extended to cover many-to-many.
   
   One more change worth listing here if it stays in: switching the property 
prefix from `resolveTableName` to `resolveColumnName` alters the element column 
of basic and enum collections under any strategy whose table and column rules 
differ.



##########
grails-data-hibernate7/docs/src/docs/asciidoc/advancedGORMFeatures/ormdsl/customNamingStrategy.adoc:
##########
@@ -68,3 +68,5 @@ class UpperCaseNamingStrategy implements 
PhysicalNamingStrategy {
 ----
 
 TIP: Individual column or table names set explicitly in the `mapping` block 
always take precedence over what the naming strategy would produce.
+
+The default foreign-key column names in a `hasMany` join table are derived 
from the physical table names of the associated domain classes. Consequently, a 
custom strategy that changes a domain table name also changes the corresponding 
join-table foreign-key column prefix. For example, if the strategy maps `TBook` 
to the table `book`, the default foreign-key column is `book_id`, not 
`tbook_id`. Applications upgrading from an earlier GORM version should account 
for this schema change or configure the join-table columns explicitly in the 
`mapping` block.

Review Comment:
   Same issue as the upgrade note: this states that the default `hasMany` 
join-table foreign keys are derived from the physical table names, but that 
only holds for a unidirectional `hasMany`. Add `belongsTo` (or a many-to-many) 
and both columns still come from the class names, so a reader with the far more 
common bidirectional mapping will not see `book_id`.
   
   Since the paragraph doesn't say which association shape it applies to, I'd 
make that explicit, and add a cross-reference to the corresponding 
upgrade-guide section so the schema-migration advice is in one place.



##########
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy:
##########
@@ -351,6 +377,22 @@ class HibernateToManyPropertySpec extends 
HibernateGormDatastoreSpec {
         property.joinTableColumName(namingStrategy) != null
     }
 
+    void "joinTableColumName applies table naming to the associated entity and 
column naming to the property prefix"() {

Review Comment:
   This one asserts the interactions with a mocked naming strategy rather than 
an outcome, which is how it passes for a branch that binding never takes (see 
the comment on `joinTableColumName`), and it also pins 
`resolveColumnName`/`resolveTableName` call counts that are implementation 
detail rather than behavior.
   
   For a regression guard on #15736, could we add at least one test that 
completes binding and asserts the resulting join-table columns — e.g. boot a 
`HibernateDatastore` over an `Author`/`Book` pair with explicit `table` 
mappings and assert the collection table's column names? A test at that level 
is what would have surfaced the two behavioral gaps noted in the other comments.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java:
##########
@@ -227,8 +224,10 @@ default String 
joinTableColumName(PersistentEntityNamingStrategy namingStrategy)
         if (present) {
             columnName = joinColumnMappingOptional.get().getName();
         } else {
-            var clazz = 
namingStrategy.resolveColumnName(referencedType.getName());
-            var prop = namingStrategy.resolveTableName(getName());
+            var clazz = isBasic() ?

Review Comment:
   Both callers of `joinTableColumName` take a `HibernateBasicProperty` 
(`BasicCollectionElementBinder#bind` and 
`EnumTypeBinder#bindEnumTypeForColumn`), and `HibernateBasicProperty extends 
BasicWithMapping` which extends `Basic` — so `isBasic()` is always `true` here 
and the association branch never executes during binding. The only thing 
reaching it is the mocked naming strategy in the new spec.
   
   If it is intended as future-proofing, I'd rather drop the ternary (or move 
`joinTableColumName` onto the basic-collection interface, where its two callers 
already are) so the code doesn't suggest an association path that doesn't 
exist. If there is a mapping that does reach it, a test that goes through the 
binder rather than a mock would make that clear.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyProperty.java:
##########
@@ -212,10 +212,7 @@ default String 
resolveJoinTableForeignKeyColumnName(PersistentEntityNamingStrate
         return ofNullable(getHibernateMappedForm())
                 .map(PropertyConfig::getJoinTableColumnConfig)
                 .map(ColumnConfig::getName)
-                .orElseGet(() -> 
namingStrategy.resolveColumnName(getHibernateAssociatedEntity()
-                                .getHibernateRootEntity()
-                                .getJavaClass()
-                                .getSimpleName()) +
+                .orElseGet(() -> 
resolveAssociatedEntityTableName(namingStrategy) +

Review Comment:
   `resolveJoinTableForeignKeyColumnName` only runs for **unidirectional** 
`hasMany` join tables, so a bidirectional many-to-many is not affected by this 
change.
   
   `CollectionSecondPassBinder` sends a bidirectional many-to-many element to 
`ManyToOneElementBinder` → `ManyToOneBinder` → `SimpleValueBinder` → 
`DefaultColumnNameFetcher`, which for a `HibernateManyToManyProperty` returns 
`resolveForeignKeyForPropertyDomainClass(...)` — still the decapitalized class 
simple name run through `resolveColumnName`. The only production call site of 
the method changed here is `CollectionWithJoinTableBinder`, reached from 
`UnidirectionalOneToManyBinder`.
   
   I compared the generated H2 schema on this branch against the merge base 
with these domain classes:
   
   ```groovy
   @Entity class ProbeAuthor { String name
       static hasMany = [books: ProbeBook]
       static mapping = { table 'writer' } }
   
   @Entity class ProbeBook { String title
       static belongsTo = ProbeAuthor
       static hasMany = [authors: ProbeAuthor]
       static mapping = { table 'catalog_book' } }
   
   @Entity class ProbeShelf { String label
       static hasMany = [shelved: ProbeBook] }   // unidirectional
   ```
   
   | join table | 8.0.x | this branch |
   |---|---|---|
   | `writer_books` (bidirectional many-to-many) | `probe_author_id`, 
`probe_book_id` | unchanged |
   | `probe_shelf_catalog_book` (unidirectional) | `probe_book_id`, 
`probe_shelf_shelved_id` | `catalog_book_id`, `probe_shelf_shelved_id` |
   
   Two consequences worth deciding on explicitly:
   
   - the many-to-many shape described in both doc changes (and in #15736's 
example app, if its associations are bidirectional) gets no new behavior;
   - even in the path that did change, the owner-side key column 
(`probe_shelf_shelved_id`) is still derived from the owner *class* name via 
`DefaultColumnNameFetcher`, so one column of the join table now follows the 
resolved table name and the other does not.
   
   Could we either extend the resolution to 
`DefaultColumnNameFetcher#getDefaultColumnName` / 
`resolveForeignKeyForPropertyDomainClass` so both sides of a join table agree, 
or keep the code change as-is and narrow the documentation to the 
unidirectional case it actually covers?



##########
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyPropertySpec.groovy:
##########
@@ -581,6 +623,33 @@ class HTMPBook {
     String title
 }
 
+@Entity
+class Book {

Review Comment:
   Every other domain class in this spec is `HTMP`-prefixed to keep the file's 
entities namespaced, and there are already several unrelated `Book` domain 
classes elsewhere in this test source set. `HTMPMappedTableBook` with, say, 
`table 'catalog_book'` would keep the convention and still demonstrate that the 
explicit `table` mapping wins over the class name.



-- 
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]

Reply via email to