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


##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/GrailsHibernatePersistentEntity.java:
##########
@@ -384,9 +385,10 @@ default boolean isLazy(HibernatePersistentProperty 
property) {
     default void sortOrIndexForeignKeyColumns(SimpleValue value) {
         PersistentClass pc = getPersistentClass();
         KeyValue identifier = pc != null ? pc.getIdentifier() : null;
-        int[] originalOrder = identifier instanceof Component c ? 
c.sortProperties() : null;
+        Component component = identifier instanceof Component c ? c : null;
+        int[] originalOrder = component != null ? component.sortProperties() : 
null;
         if (originalOrder != null) {
-            value.sortColumns(originalOrder);
+            value.sortColumns(toColumnPermutation(originalOrder, 
partColumnSpans(component, originalOrder)));
         } else {

Review Comment:
   This branch runs when the referenced entity hasn't been bound yet. 
`getPersistentClass()` is still null, so the columns keep their declared order 
and only get sequential type indexes. `markValueSorted` then sets `sorted`, so 
Hibernate's `ToOne.sortProperties()` skips them later too. Whether a key gets 
sorted depends on whether the entity it references was registered first.
   
   The spec always registers the referenced entity first. Here are the same 
classes in alphabetical order:
   
   ```groovy
   new HibernateDatastore(config, PrbGrand, PrbLeaf, PrbMiddle)
   ```
   
   The leaf's key columns stay `prb_middle_name, prb_middle_grand_parent_alpha, 
prb_middle_grand_parent_zeta` while the values are bound in key order, and 
saving a leaf fails:
   
   ```
   Referential integrity constraint violation: "FKDEN36EQGOPJY1BD3T41V9BTYS: 
PUBLIC.PRB_LEAF FOREIGN KEY(PRB_MIDDLE_NAME, PRB_MIDDLE_GRAND_PARENT_ALPHA, 
PRB_MIDDLE_GRAND_PARENT_ZETA) REFERENCES PUBLIC.PRB_MIDDLE(NAME, 
PRB_GRAND_ALPHA, PRB_GRAND_ZETA) ('a', 'z', 'm')"
   ```
   
   The middle's own key has the same problem when the middle is registered 
before its grand parent. Applications don't choose this order; their domain 
classes come from the class-path scan. Leaving `sorted` unset would not be 
enough on its own, because Hibernate's sort applies the property permutation 
and runs into the column-span problem this commit fixes.
   
   This predates the PR: 8.0.x fails the same way, with the columns crossed 
differently. Are you willing to handle it here, for example by deferring the 
sort until the referenced identifier is bound? Or would you rather we open a 
follow-up issue for it?



##########
grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CompositeForeignKeyColumnTypesSpec.groovy:
##########
@@ -0,0 +1,393 @@
+/*
+ *  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.binder
+
+import java.sql.Connection
+
+import grails.gorm.annotation.Entity
+import grails.gorm.hibernate.mapping.MappingBuilder
+import org.grails.orm.hibernate.HibernateDatastore
+import org.hibernate.dialect.H2Dialect
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Specification
+
+/**
+ * A foreign key to an entity whose composite identifier has parts of 
different types must name
+ * and type each column after the referenced primary key column it points at.
+ */
+class CompositeForeignKeyColumnTypesSpec extends Specification {
+
+    @Shared
+    @AutoCleanup
+    HibernateDatastore datastore = new HibernateDatastore(
+            [
+                    'dataSource.url'      : 
'jdbc:h2:mem:compositeFkTypesDB;LOCK_TIMEOUT=10000',
+                    'dataSource.dbCreate' : 'create-drop',
+                    'dataSource.dialect'  : H2Dialect.name,
+                    'hibernate.hbm2ddl.auto': 'create',
+            ],
+            CfkParent, CfkChild, CfkGrandParent, CfkMiddle, CfkLeaf,
+            CfkOrdParent, CfkOrdChild, CfkOrdGrand, CfkOrdMiddle, CfkOrdLeaf,
+            PrbGrand, PrbMiddle, PrbLeaf, PrbSpanLeaf)
+
+    private List<List> columns(String table) {
+        List<List> rows = []
+        datastore.sessionFactory.openSession().withCloseable { session ->
+            session.doWork { Connection c ->
+                c.createStatement().withCloseable { st ->
+                    st.executeQuery(
+                            "select column_name, data_type from 
information_schema.columns where table_name = '${table}' order by 
ordinal_position".toString())
+                            .withCloseable { rs ->
+                                while (rs.next()) {
+                                    rows << [rs.getString(1).toLowerCase(), 
rs.getString(2).toUpperCase()]
+                                }
+                            }
+                }
+            }
+        }
+        rows
+    }
+
+
+    /**
+     * The foreign keys of a table, each as its column pairs [foreign key 
column, referenced column] in
+     * {@code KEY_SEQ} order, which is the order the database matches the 
columns of a key by.
+     */
+    private Map<String, List<List<String>>> foreignKeyPairs(String table) {
+        Map<String, List<List<String>>> keys = [:]
+        datastore.sessionFactory.openSession().withCloseable { session ->
+            session.doWork { Connection c ->
+                c.metaData.getImportedKeys(null, null, table).withCloseable { 
rs ->
+                    List<List> rows = []
+                    while (rs.next()) {
+                        rows << [rs.getString('FK_NAME'), rs.getInt('KEY_SEQ'),
+                                 rs.getString('PKTABLE_NAME').toLowerCase(),
+                                 rs.getString('FKCOLUMN_NAME').toLowerCase(), 
rs.getString('PKCOLUMN_NAME').toLowerCase()]
+                    }
+                    rows.sort { it[1] }.each { List row ->
+                        keys.get(row[0] + ' -> ' + row[2], []) << [row[3], 
row[4]]
+                    }
+                }
+            }
+        }
+        keys.collectEntries { String name, List<List<String>> pairs -> 
[(name.substring(name.indexOf(' -> ') + 4)): pairs] }
+    }
+
+    void "a foreign key to a composite parent pairs its columns with the 
referenced key columns in key order"() {
+        expect:
+        foreignKeyPairs('CFK_CHILD') == [
+                cfk_parent: [['cfk_parent_lucky_number', 'lucky_number'], 
['cfk_parent_name', 'name']]
+        ]
+    }
+
+    void "a foreign key to a composite parent whose parts are declared out of 
name order pairs the same-typed parts in key order"() {
+        expect: 'the parts are declared as zeta, alpha, and the primary key 
and the foreign key both follow the name order'
+        foreignKeyPairs('CFK_ORD_CHILD') == [
+                cfk_ord_parent: [['cfk_ord_parent_alpha', 'alpha'], 
['cfk_ord_parent_zeta', 'zeta']]
+        ]
+    }
+
+    void "the foreign keys of a three level composite chain pair their columns 
with the referenced key columns in key order"() {
+        expect:
+        foreignKeyPairs('CFK_MIDDLE') == [
+                cfk_grand_parent: [['cfk_grand_parent_lucky_number', 
'lucky_number'], ['cfk_grand_parent_name', 'name']]
+        ]
+        foreignKeyPairs('CFK_LEAF') == [
+                cfk_middle: [
+                        ['cfk_middle_grand_parent_lucky_number', 
'cfk_grand_parent_lucky_number'],
+                        ['cfk_middle_grand_parent_name', 
'cfk_grand_parent_name'],
+                        ['cfk_middle_name', 'name']]
+        ]
+    }
+
+    void "the foreign keys of a three level chain with out of order, 
same-typed parts pair them in key order"() {
+        expect: 'the nested composite is declared as zeta, alpha, so only its 
order tells the columns apart'
+        foreignKeyPairs('CFK_ORD_MIDDLE') == [
+                cfk_ord_grand: [['cfk_ord_grand_alpha', 'alpha'], 
['cfk_ord_grand_zeta', 'zeta']]
+        ]
+        foreignKeyPairs('CFK_ORD_LEAF') == [
+                cfk_ord_middle: [
+                        ['cfk_ord_middle_grand_parent_alpha', 
'cfk_ord_grand_alpha'],
+                        ['cfk_ord_middle_grand_parent_zeta', 
'cfk_ord_grand_zeta'],
+                        ['cfk_ord_middle_name', 'name']]
+        ]
+    }
+
+    void "foreign key columns carry the type of the primary key column they 
are named after"() {
+        when:
+        Map<String, String> parent = columns('CFK_PARENT').collectEntries { 
[(it[0]): it[1]] }
+        Map<String, String> child = columns('CFK_CHILD').collectEntries { 
[(it[0]): it[1]] }
+
+        then:
+        parent.name == 'CHARACTER VARYING'
+        parent.lucky_number == 'INTEGER'
+        child.cfk_parent_name == parent.name
+        child.cfk_parent_lucky_number == parent.lucky_number
+    }
+
+    void "foreign key columns of a three level composite chain are named and 
typed after the referenced key"() {
+        when:
+        Map<String, String> grand = columns('CFK_GRAND_PARENT').collectEntries 
{ [(it[0]): it[1]] }
+        Map<String, String> middle = columns('CFK_MIDDLE').collectEntries { 
[(it[0]): it[1]] }
+
+        Map<String, String> leaf = columns('CFK_LEAF').collectEntries { 
[(it[0]): it[1]] }
+
+        then:
+        grand.name
+        middle.cfk_grand_parent_name == grand.name
+        middle.cfk_grand_parent_lucky_number == grand.lucky_number
+        leaf.cfk_middle_grand_parent_name == grand.name
+        leaf.cfk_middle_grand_parent_lucky_number == grand.lucky_number
+    }
+
+    void "a leaf of a three level composite chain is saved, reloaded and found 
through the association"() {
+        when:
+        CfkGrandParent.withNewTransaction {
+            CfkGrandParent grand = new CfkGrandParent(name: 'Fred', 
luckyNumber: 7).save(failOnError: true)
+            CfkMiddle middle = new CfkMiddle(name: 'Bob', grandParent: 
grand).save(failOnError: true)
+            new CfkLeaf(name: 'Chuck', middle: middle).save(failOnError: true, 
flush: true)
+        }
+
+        then:
+        CfkLeaf.withNewSession {
+            CfkLeaf leaf = CfkLeaf.findByName('Chuck')
+            leaf.middle.name == 'Bob' && leaf.middle.grandParent.luckyNumber 
== 7
+        }
+    }
+
+    void "a child referencing a composite parent is saved, reloaded and found 
through the association"() {
+        when:
+        CfkParent.withNewTransaction {
+            CfkParent parent = new CfkParent(name: 'Fred', luckyNumber: 
7).save(failOnError: true)
+            new CfkChild(label: 'kid', parent: parent).save(failOnError: true, 
flush: true)
+        }
+
+        then:
+        CfkChild.withNewSession {
+            CfkChild child = CfkChild.findByLabel('kid')
+            child.parent.name == 'Fred' && child.parent.luckyNumber == 7
+        }
+        CfkChild.withNewSession {
+            CfkChild.where { parent.name == 'Fred' && parent.luckyNumber == 7 
}.count() == 1
+        }
+    }
+
+    void "a nested composite part that sorts after a plain part moves with all 
of its columns"() {
+        expect: 'the middle key is declared as name, grandParent, so 
grandParent spans the columns that sort before and after name'
+        foreignKeyPairs('PRB_MIDDLE') == [
+                prb_grand: [['prb_grand_alpha', 'alpha'], ['prb_grand_zeta', 
'zeta']]
+        ]
+        foreignKeyPairs('PRB_LEAF') == [
+                prb_middle: [
+                        ['prb_middle_grand_parent_alpha', 'prb_grand_alpha'],
+                        ['prb_middle_grand_parent_zeta', 'prb_grand_zeta'],
+                        ['prb_middle_name', 'name']]
+        ]
+    }
+
+    void "a leaf whose composite key nests a part declared after a plain part 
is saved and reloaded"() {
+        when:
+        PrbGrand.withNewTransaction {
+            PrbGrand grand = new PrbGrand(zeta: 'z', alpha: 
'a').save(failOnError: true)
+            PrbMiddle middle = new PrbMiddle(name: 'm', grandParent: 
grand).save(failOnError: true)
+            new PrbLeaf(name: 'l', middle: middle).save(failOnError: true, 
flush: true)
+        }
+
+        then:
+        PrbLeaf.withNewSession {
+            PrbLeaf leaf = PrbLeaf.findByName('l')
+            leaf.middle.name == 'm' && leaf.middle.grandParent.alpha == 'a' && 
leaf.middle.grandParent.zeta == 'z'
+        }
+    }
+
+    void "a multi-column nested part between two plain parts keeps its columns 
together in key order"() {

Review Comment:
   `PrbSpanLeaf`'s foreign key points at `PrbMiddle`, so these two features 
check the same key as the `PrbLeaf` ones. `composite('zed', 'middle', 'ace')` 
only lays out `PrbSpanLeaf`'s own primary key. Hibernate orders that key 
itself, and nothing references it, so the permutation for a multi-column part 
between two plain parts never runs here.
   
   An entity that references a key with that layout does exercise it:
   
   ```groovy
   @Entity
   class PrbHub implements Serializable {
       String zed
       String ace
       PrbGrand grand
       static hasMany = [tags: PrbHubTag]
   
       static mapping = MappingBuilder.define {
           composite('zed', 'grand', 'ace')
       }
   }
   
   @Entity
   class PrbHubTag {
       String label
   }
   
   @Entity
   class PrbHubRef {
       String name
       PrbHub hub
   }
   ```
   
   With this commit, the `PRB_HUB_REF` key pairs are `[prb_hub_ace, ace], 
[prb_hub_grand_alpha, prb_grand_alpha], [prb_hub_grand_zeta, prb_grand_zeta], 
[prb_hub_zed, zed]`. The `PRB_HUB_PRB_HUB_TAG` join table gets the same pairs, 
and a hub with a tag and a ref saves and reloads. On 8.0.x the pairs come out 
as `[prb_hub_grand_alpha, prb_grand_zeta], [prb_hub_grand_zeta, 
prb_grand_alpha], [prb_hub_zed, zed], [prb_hub_ace, ace]`, and the save fails 
with a referential integrity violation.
   
   Could these two features use this layout instead? The join table also covers 
the `CollectionWithJoinTableBinder` path. The Tests section of the description 
would need to change to match.



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