-
-
Notifications
You must be signed in to change notification settings - Fork 972
Fix join-table foreign keys to use resolved entity table names (#15736) #16028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 8.0.x
Are you sure you want to change the base?
Changes from 6 commits
bfdbbe8
d1ca11f
a77c8f2
aa02d74
ba081ef
3195e1d
0ca2559
546fea4
4da31cc
d2655f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) + | |||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I compared the generated H2 schema on this branch against the merge base with these domain classes: @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
Two consequences worth deciding on explicitly:
Could we either extend the resolution to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the detailed repro — went with your second option: kept the code narrowly scoped to what it actually fixes (the associated-entity FK column of a unidirectional |
|||||||||||
| GrailsDomainBinder.FOREIGN_KEY_SUFFIX); | |||||||||||
| } | |||||||||||
|
|
|||||||||||
|
|
@@ -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() ? | |||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both callers of If it is intended as future-proofing, I'd rather drop the ternary (or move
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dropped the ternary in 4da31cc — confirmed both real callers ( |
|||||||||||
| namingStrategy.resolveColumnName(referencedType.getName()) : | |||||||||||
| resolveAssociatedEntityTableName(namingStrategy); | |||||||||||
| var prop = namingStrategy.resolveColumnName(getName()); | |||||||||||
|
matrei marked this conversation as resolved.
|
|||||||||||
| columnName = referencedType.isEnum() ? | |||||||||||
| clazz : | |||||||||||
| new BackticksRemover().apply(prop) + UNDERSCORE + new BackticksRemover().apply(clazz); | |||||||||||
|
|
|||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,13 +21,18 @@ package org.grails.orm.hibernate.cfg.domainbinding.hibernate | |
| import grails.gorm.annotation.Entity | ||
| import grails.gorm.tests.HibernateGormDatastoreSpec | ||
| import org.hibernate.MappingException | ||
| import org.hibernate.boot.model.naming.Identifier | ||
| import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl | ||
| import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment | ||
| import org.grails.datastore.mapping.model.PersistentProperty | ||
| import org.grails.datastore.mapping.model.PersistentEntity | ||
| import org.grails.datastore.mapping.model.PropertyMapping | ||
| import org.grails.datastore.mapping.model.ClassMapping | ||
| import org.grails.datastore.mapping.reflect.EntityReflector | ||
| import org.grails.orm.hibernate.cfg.PropertyConfig | ||
| import org.grails.orm.hibernate.cfg.Mapping | ||
| import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy | ||
| import org.grails.orm.hibernate.cfg.domainbinding.util.NamingStrategyWrapper | ||
|
|
||
| class HibernateToManyPropertySpec extends HibernateGormDatastoreSpec { | ||
|
|
||
|
|
@@ -63,6 +68,27 @@ class HibernateToManyPropertySpec extends HibernateGormDatastoreSpec { | |
| columnName == "custom_book_fk" | ||
| } | ||
|
|
||
| void "resolveJoinTableForeignKeyColumnName removes a domain prefix through a physical naming strategy"() { | ||
| given: | ||
| def property = createTestHibernateToManyProperty(HTMPAuthor, "books") | ||
| def namingStrategy = new NamingStrategyWrapper( | ||
| new HTMPPrefixRemovingPhysicalNamingStrategy(), getGrailsDomainBinder().jdbcEnvironment) | ||
| hibernateFirstPass() | ||
|
|
||
| expect: | ||
| property.resolveJoinTableForeignKeyColumnName(namingStrategy) == "book_id" | ||
| } | ||
|
|
||
| void "resolveJoinTableForeignKeyColumnName uses the associated entity explicit table mapping"() { | ||
| given: | ||
| def property = createTestHibernateToManyProperty(HTMPMappedTableAuthor, "books") | ||
| def namingStrategy = getGrailsDomainBinder().namingStrategy | ||
| hibernateFirstPass() | ||
|
|
||
| expect: | ||
| property.resolveJoinTableForeignKeyColumnName(namingStrategy) == "htmp_book_id" | ||
| } | ||
|
|
||
| void "isAssociationColumnNullable returns false for ManyToMany"() { | ||
| given: "Register only entities for this specific test" | ||
| createPersistentEntity(HTMPCourse) // Course is needed because Student refers to it | ||
|
|
@@ -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"() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced it in 4da31cc — the new test boots a real |
||
| given: | ||
| def property = createTestHibernateToManyProperty(HTMPAuthor, "books") | ||
| def namingStrategy = Mock(PersistentEntityNamingStrategy) | ||
| hibernateFirstPass() | ||
|
|
||
| when: | ||
| String columnName = property.joinTableColumName(namingStrategy) | ||
|
|
||
| then: | ||
| 1 * namingStrategy.resolveTableName(_ as GrailsHibernatePersistentEntity) >> "book" | ||
| 1 * namingStrategy.resolveColumnName("books") >> "books" | ||
| 0 * namingStrategy.resolveTableName("books") | ||
| columnName == "books_book" | ||
| } | ||
|
|
||
| void "joinTableColumName returns derived column name for enum collection"() { | ||
| given: | ||
| def property = createTestHibernateToManyProperty(HTMPEntityWithEnum, "statuses") | ||
|
|
@@ -581,6 +623,33 @@ class HTMPBook { | |
| String title | ||
| } | ||
|
|
||
| @Entity | ||
| class Book { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every other domain class in this spec is
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed to |
||
| Long id | ||
| String title | ||
|
|
||
| static mapping = { | ||
| table 'htmp_book' | ||
| } | ||
| } | ||
|
|
||
| @Entity | ||
| class HTMPMappedTableAuthor { | ||
| Long id | ||
| String name | ||
| static hasMany = [books: Book] | ||
| } | ||
|
|
||
| class HTMPPrefixRemovingPhysicalNamingStrategy extends PhysicalNamingStrategyStandardImpl { | ||
|
|
||
| @Override | ||
| Identifier toPhysicalTableName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { | ||
| logicalName.text == HTMPBook.simpleName ? | ||
| Identifier.toIdentifier('book') : | ||
| super.toPhysicalTableName(logicalName, jdbcEnvironment) | ||
| } | ||
| } | ||
|
|
||
| @Entity | ||
| class HTMPAuthor { | ||
| Long id | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as the upgrade note: this states that the default 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-scoped this paragraph in 4da31cc to make the unidirectional-only condition explicit up front, and added a cross-reference to the upgrade guide's §26.9 (now titled "Join-Table Foreign-Key Column Names") so the migration guidance lives in one place. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This returns the table name verbatim, and the two call sites then treat it differently:
joinTableColumNamepasses it throughBackticksRemover, whileresolveJoinTableForeignKeyColumnNameconcatenates_idonto it directly.TableForManyCalculator.calculateTableForManyalso strips backticks fromgetTableName(...), because backtick-quoting a reserved word intableis supported and used (e.g.grails/gorm/tests/multitenancy/Usermapstable 'user').With a quoted table on the far side of a unidirectional
hasMany, the FK column name is now malformed:On 8.0.x the same mapping produces
probe_shelf_user(probe_quoted_id, probe_shelf_quoted_id). Withouthibernate.hbm2ddl.halt_on_errorthe 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
resolveJoinTableForeignKeyColumnNamecall site, matchingjoinTableColumName) fixes it. A test with a backtick-quotedtablemapping would be worth adding alongside the two new cases.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 546fea4 (pushed before your review, sorry for the noise) — stripped backticks once at the source in
HibernateAssociation#resolveAssociatedEntityTableName, which bothjoinTableColumNameandresolveJoinTableForeignKeyColumnNamego through, so it's no longer left to each caller. Added "resolveJoinTableForeignKeyColumnName strips backticks from a backtick-quoted associated entity table name" reproducing yourtable '\user`'-shaped repro (HTMPQuotedTableAuthor/HTMPQuotedTableBook` in the spec).