This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit 1fa29ca1eba950b36ee1c93ad70659b46f6a8278 Author: Andrus Adamchik <[email protected]> AuthorDate: Mon Jul 13 18:42:42 2026 -0400 CAY-2977 DBImport confused by multi-key relationships --- .../dbsync/reverse/dbload/DbLoadDataStore.java | 5 +- .../cayenne/dbsync/reverse/dbload/ExportedKey.java | 15 ++++ .../dbsync/reverse/dbload/ExportedKeyLoaderIT.java | 5 +- .../reverse/dbload/RelationshipsLoaderIT.java | 96 ++++++++++++++++++++-- cayenne/src/test/resources/compound.map.xml | 21 +++++ 5 files changed, 132 insertions(+), 10 deletions(-) diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java index c6513d7b1..422f96f08 100644 --- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java +++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java @@ -84,10 +84,11 @@ public class DbLoadDataStore extends DataMap { } void addExportedKey(ExportedKey key) { - Set<ExportedKey> exportedKeys = this.exportedKeys.get(key.getStrKey()); + // group by the FK constraint, so that all columns of a multi-column FK end up in a single relationship + Set<ExportedKey> exportedKeys = this.exportedKeys.get(key.getGroupKey()); if (exportedKeys == null) { exportedKeys = new TreeSet<>(); - this.exportedKeys.put(key.getStrKey(), exportedKeys); + this.exportedKeys.put(key.getGroupKey(), exportedKeys); } exportedKeys.add(key); } diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java index 374c36f1d..2ca48a9a7 100644 --- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java +++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKey.java @@ -116,6 +116,21 @@ public class ExportedKey implements Comparable<ExportedKey> { return pk + " <- " + fk; } + /** + * Returns a key that identifies the single FK constraint this row belongs to, so that all columns of a + * multi-column FK are grouped into one relationship. Uses the FK constraint name (FK_NAME) reported by the + * driver; falls back to the per-column {@link #getStrKey()} when the name is unavailable, preserving the + * historical behavior for drivers that don't report constraint names. + */ + String getGroupKey() { + String fkName = fk.getName(); + if (Util.isEmptyString(fkName)) { + return getStrKey(); + } + return fk.getCatalog() + "." + fk.getSchema() + "." + fk.getTable() + "." + fkName + + " -> " + pk.getCatalog() + "." + pk.getSchema() + "." + pk.getTable(); + } + public static class KeyData implements Comparable<KeyData> { private final String catalog; private final String schema; diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java index fa902af7c..84ad8fb8b 100644 --- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java +++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/ExportedKeyLoaderIT.java @@ -90,8 +90,9 @@ public class ExportedKeyLoaderIT extends BaseLoaderIT { private ExportedKey findArtistExportedKey() { for(Map.Entry<String, Set<ExportedKey>> entry : store.getExportedKeysEntrySet()) { - if(entry.getKey().toUpperCase().endsWith(".ARTIST_ID")) { - return entry.getValue().iterator().next(); + ExportedKey key = entry.getValue().iterator().next(); + if("ARTIST_ID".equalsIgnoreCase(key.getFk().getColumn())) { + return key; } } diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java index bd11d9d75..445b525c0 100644 --- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java +++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/RelationshipsLoaderIT.java @@ -19,16 +19,17 @@ package org.apache.cayenne.dbsync.reverse.dbload; -import java.sql.DatabaseMetaData; -import java.util.Collection; - import org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator; +import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.DbRelationship; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import java.sql.DatabaseMetaData; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; public class RelationshipsLoaderIT extends BaseLoaderIT { @@ -79,6 +80,89 @@ public class RelationshipsLoaderIT extends BaseLoaderIT { assertTrue(oneToOne.isToDependentPK(), "Relationship to PAINTING_INFO must be to-one"); } + @Test + public void compoundFkLoad() throws Exception { + if (!accessStackAdapter.supportsFKConstraints()) { + return; + } + + DatabaseMetaData metaData = connection.getMetaData(); + DbLoaderDelegate delegate = new DefaultDbLoaderDelegate(); + + new EntityLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, store); + new AttributeLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, store); + new PrimaryKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store); + new ExportedKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store); + + new RelationshipLoader(EMPTY_CONFIG, delegate, new DefaultObjectNameGenerator()).load(metaData, store); + + // COMPOUND_FK_TEST has a 2-column FK (F_KEY1, F_KEY2) -> COMPOUND_PK_TEST (KEY1, KEY2) + DbEntity fkEntity = getDbEntity("COMPOUND_FK_TEST"); + assertNotNull(fkEntity, "COMPOUND_FK_TEST entity was not loaded"); + + List<DbRelationship> toCompoundPk = relationshipsTo(fkEntity, "COMPOUND_PK_TEST"); + assertEquals(1, toCompoundPk.size(), + "The 2-column FK must produce a single to-one relationship, not one per column"); + assertEquals(2, toCompoundPk.get(0).getJoins().size(), + "The relationship must carry both FK column joins"); + + // the reverse (to-many) relationship must be a single compound relationship as well + DbEntity pkEntity = getDbEntity("COMPOUND_PK_TEST"); + assertNotNull(pkEntity, "COMPOUND_PK_TEST entity was not loaded"); + + List<DbRelationship> toCompoundFk = relationshipsTo(pkEntity, "COMPOUND_FK_TEST"); + assertEquals(1, toCompoundFk.size(), + "The reverse of the 2-column FK must be a single to-many relationship, not one per column"); + assertEquals(2, toCompoundFk.get(0).getJoins().size(), + "The reverse relationship must carry both FK column joins"); + } + + @Test + public void twoIndependentFksLoad() throws Exception { + if (!accessStackAdapter.supportsFKConstraints()) { + return; + } + + DatabaseMetaData metaData = connection.getMetaData(); + DbLoaderDelegate delegate = new DefaultDbLoaderDelegate(); + + new EntityLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, store); + new AttributeLoader(adapter, EMPTY_CONFIG, delegate).load(metaData, store); + new PrimaryKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store); + new ExportedKeyLoader(EMPTY_CONFIG, delegate).load(metaData, store); + + new RelationshipLoader(EMPTY_CONFIG, delegate, new DefaultObjectNameGenerator()).load(metaData, store); + + // TWO_FK_B has two separate single-column FKs (A_ID1, A_ID2) -> TWO_FK_A (ID) + DbEntity bEntity = getDbEntity("TWO_FK_B"); + assertNotNull(bEntity, "TWO_FK_B entity was not loaded"); + + List<DbRelationship> toA = relationshipsTo(bEntity, "TWO_FK_A"); + assertEquals(2, toA.size(), "Two independent FKs must produce two separate relationships"); + for (DbRelationship rel : toA) { + assertEquals(1, rel.getJoins().size(), "Each single-column FK relationship must have exactly one join"); + } + + DbEntity aEntity = getDbEntity("TWO_FK_A"); + assertNotNull(aEntity, "TWO_FK_A entity was not loaded"); + + List<DbRelationship> toB = relationshipsTo(aEntity, "TWO_FK_B"); + assertEquals(2, toB.size(), "Two independent FKs must produce two separate reverse relationships"); + for (DbRelationship rel : toB) { + assertEquals(1, rel.getJoins().size(), "Each reverse relationship must have exactly one join"); + } + } + + private List<DbRelationship> relationshipsTo(DbEntity entity, String targetName) { + List<DbRelationship> result = new ArrayList<>(); + for (DbRelationship rel : entity.getRelationships()) { + if (targetName.equalsIgnoreCase(rel.getTargetEntityName())) { + result.add(rel); + } + } + return result; + } + // private void assertUniqueConstraintsInRelationships(DataMap map) { // unfortunately JDBC metadata doesn't provide info for UNIQUE // constraints.... diff --git a/cayenne/src/test/resources/compound.map.xml b/cayenne/src/test/resources/compound.map.xml index 5b3d92b1a..c70bef5dd 100644 --- a/cayenne/src/test/resources/compound.map.xml +++ b/cayenne/src/test/resources/compound.map.xml @@ -43,6 +43,15 @@ <db-attribute name="order_line_number" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> <db-attribute name="order_number" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> </db-entity> + <db-entity name="TWO_FK_A"> + <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> + <db-attribute name="NAME" type="VARCHAR" length="100"/> + </db-entity> + <db-entity name="TWO_FK_B"> + <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> + <db-attribute name="A_ID1" type="INTEGER"/> + <db-attribute name="A_ID2" type="INTEGER"/> + </db-entity> <obj-entity name="CharFkTestEntity" className="org.apache.cayenne.testdo.compound.CharFkTestEntity" dbEntityName="CHAR_FK_TEST"> <obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/> </obj-entity> @@ -100,6 +109,18 @@ <db-attribute-pair source="order_number" target="order_number"/> <db-attribute-pair source="order_line_number" target="order_line_number"/> </db-relationship> + <db-relationship name="toA1" source="TWO_FK_B" target="TWO_FK_A"> + <db-attribute-pair source="A_ID1" target="ID"/> + </db-relationship> + <db-relationship name="toA2" source="TWO_FK_B" target="TWO_FK_A"> + <db-attribute-pair source="A_ID2" target="ID"/> + </db-relationship> + <db-relationship name="bs1" source="TWO_FK_A" target="TWO_FK_B" toMany="true"> + <db-attribute-pair source="ID" target="A_ID1"/> + </db-relationship> + <db-relationship name="bs2" source="TWO_FK_A" target="TWO_FK_B" toMany="true"> + <db-attribute-pair source="ID" target="A_ID2"/> + </db-relationship> <obj-relationship name="toCharPK" source="CharFkTestEntity" target="CharPkTestEntity" db-relationship-path="toCharPK"/> <obj-relationship name="charFKs" source="CharPkTestEntity" target="CharFkTestEntity" db-relationship-path="charFKs"/> <obj-relationship name="toCompoundPk" source="CompoundFkTestEntity" target="CompoundPkTestEntity" db-relationship-path="toCompoundPk"/>
