From c98dcfa8b8bc2af4bbed73fd891676ecf42ecb6a Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 31 Aug 2025 12:27:41 +1200 Subject: [PATCH 1/4] #3664 Fix / support for extra JoinColumns on ManyToOne Allows for extra JoinColumns on ManyToOne. The extra JoinColumn(s) are expected to be useful for the case of table partitioning where the extra join column is used to partition the table. In the test case, the partition column would be the org_id column and common to both tables (same partition key). --- .../server/deploy/BeanPropertyAssoc.java | 15 +++--- .../server/deploy/BeanPropertyAssocOne.java | 10 ---- .../server/deploy/TableJoin.java | 4 ++ .../build/ModelBuildPropertyVisitor.java | 2 +- .../java/org/tests/model/m2o/MTJOrder.java | 46 ++++++++++++++++++ .../java/org/tests/model/m2o/MTJTrans.java | 47 +++++++++++++++++++ .../tests/model/m2o/TestMTJoinColumns.java | 25 ++++++++++ 7 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java create mode 100644 ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java create mode 100644 ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssoc.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssoc.java index c3aeb15c07..ef10e0dbc6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssoc.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssoc.java @@ -436,14 +436,15 @@ ImportedId createImportedId(BeanPropertyAssoc owner, BeanDescriptor target } TableJoinColumn[] cols = join.columns(); if (!idProp.isEmbedded()) { - // simple single scalar id - if (cols.length != 1) { - CoreLog.log.log(ERROR, "No Imported Id column for {0} in table {1}", idProp, join.getTable()); - return null; - } else { - BeanProperty[] idProps = {idProp}; - return createImportedScalar(owner, cols[0], idProps, others); + // simple single scalar id, match on the foreign column, allow extra TableJoinColumn for #3664 + String matchColumn = idProp.dbColumn(); + for (TableJoinColumn col : cols) { + if (matchColumn.equals(col.getForeignDbColumn())) { + return createImportedScalar(owner, col, new BeanProperty[]{idProp}, others); + } } + CoreLog.log.log(ERROR, "No Imported Id column for {0} in table {1}", idProp, join.getTable()); + return null; } else { // embedded id BeanPropertyAssocOne embProp = (BeanPropertyAssocOne) idProp; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index a0b8554e70..5bb4fe2c02 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -128,16 +128,6 @@ private void initialiseAssocOne(String embeddedPrefix) { throw new PersistenceException("Cannot find imported id for " + fullName() + " from " + targetDescriptor + ". If using native-image, possibly missing reflect-config for the Id property."); } - if (importedId.isScalar()) { - // limit JoinColumn mapping to the @Id / primary key - TableJoinColumn[] columns = tableJoin.columns(); - String foreignJoinColumn = columns[0].getForeignDbColumn(); - String foreignIdColumn = targetDescriptor.idProperty().dbColumn(); - if (!foreignJoinColumn.equalsIgnoreCase(foreignIdColumn)) { - throw new PersistenceException("Mapping limitation - @JoinColumn on " + fullName() + " needs to map to a primary key as per Issue #529 " - + " - joining to " + foreignJoinColumn + " and not " + foreignIdColumn); - } - } } else { exportedProperties = createExported(); String delStmt = "delete from " + targetDescriptor.baseTable() + " where "; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java index 3618dc3c2f..9d7e978149 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java @@ -133,6 +133,10 @@ public String getTable() { return table; } +// public boolean multiColumn() { +// return columns.length > 1; +// } + public void addJoin(SqlJoinType joinType, String prefix, DbSqlContext ctx, String predicate) { String[] names = SplitName.split(prefix); String a1 = ctx.tableAlias(names[0]); diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java index d11b679b4c..ca69e2e134 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java @@ -195,7 +195,7 @@ public void visitOneImported(BeanPropertyAssocOne p) { String dbCol = column.getLocalDbColumn(); BeanProperty importedProperty = p.findMatchImport(dbCol); if (importedProperty == null) { - throw new RuntimeException("Imported BeanProperty not found?"); + continue; } String columnDefn = ctx.getColumnDefn(importedProperty, true); String refColumn = importedProperty.dbColumn(); diff --git a/ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java b/ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java new file mode 100644 index 0000000000..2c3f363dba --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java @@ -0,0 +1,46 @@ +package org.tests.model.m2o; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class MTJOrder { + + @Id + Long id; + + @Column(name = "org_id") + Long orgId; + + @Column + String other; + + + public Long id() { + return id; + } + + public MTJOrder setId(Long id) { + this.id = id; + return this; + } + + public Long orgId() { + return orgId; + } + + public MTJOrder setOrgId(Long orgId) { + this.orgId = orgId; + return this; + } + + public String other() { + return other; + } + + public MTJOrder setOther(String other) { + this.other = other; + return this; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java b/ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java new file mode 100644 index 0000000000..3364ae1c85 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java @@ -0,0 +1,47 @@ +package org.tests.model.m2o; + +import jakarta.persistence.*; + +@Entity +public class MTJTrans { + + @Id + Long id; + + @Column(name = "org_id") + Long orgId; + + @ManyToOne + @JoinColumns({ + @JoinColumn(name = "org_id", referencedColumnName = "org_id", insertable = false, updatable = false), + @JoinColumn(name = "order_id", referencedColumnName = "id") + }) + MTJOrder order; + + public Long id() { + return id; + } + + public MTJTrans setId(Long id) { + this.id = id; + return this; + } + + public Long orgId() { + return orgId; + } + + public MTJTrans setOrgId(Long orgId) { + this.orgId = orgId; + return this; + } + + public MTJOrder order() { + return order; + } + + public MTJTrans setOrder(MTJOrder order) { + this.order = order; + return this; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java b/ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java new file mode 100644 index 0000000000..e1cca3adcd --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java @@ -0,0 +1,25 @@ +package org.tests.model.m2o; + +import io.ebean.DB; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestMTJoinColumns { + + @Test + void test() { + MTJTrans parent = new MTJTrans(); + parent.setOrgId(51L); + + DB.save(parent); + + MTJTrans found = DB.find(MTJTrans.class) + .setId(parent.id()) + .fetch("order") + .findOne(); + + MTJOrder order = found.order(); + assertThat(order).isNull(); + } +} From c650b21f397623333ddedcd16e801752c8d31e9a Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 31 Aug 2025 12:29:00 +1200 Subject: [PATCH 2/4] #3664 Fix / support for extra JoinColumns on ManyToOne Allows for extra JoinColumns on ManyToOne. The extra JoinColumn(s) are expected to be useful for the case of table partitioning where the extra join column is used to partition the table. In the test case, the partition column would be the org_id column and common to both tables (same partition key). --- .../main/java/io/ebeaninternal/server/deploy/TableJoin.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java index 9d7e978149..3618dc3c2f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java @@ -133,10 +133,6 @@ public String getTable() { return table; } -// public boolean multiColumn() { -// return columns.length > 1; -// } - public void addJoin(SqlJoinType joinType, String prefix, DbSqlContext ctx, String predicate) { String[] names = SplitName.split(prefix); String a1 = ctx.tableAlias(names[0]); From 3f6c6ae392d8fea6e86046e6f5fdde53ce49710a Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 31 Aug 2025 12:35:29 +1200 Subject: [PATCH 3/4] #3664 Extend test --- .../tests/model/m2o/TestMTJoinColumns.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java b/ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java index e1cca3adcd..c7215ca307 100644 --- a/ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java +++ b/ebean-test/src/test/java/org/tests/model/m2o/TestMTJoinColumns.java @@ -1,8 +1,11 @@ package org.tests.model.m2o; import io.ebean.DB; +import io.ebean.test.LoggedSql; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; class TestMTJoinColumns { @@ -19,7 +22,27 @@ void test() { .fetch("order") .findOne(); - MTJOrder order = found.order(); - assertThat(order).isNull(); + assertThat(found.order()).isNull(); + + var order = new MTJOrder() + .setOrgId(51L) + .setOther("some"); + DB.save(order); + found.setOrder(order); + DB.save(found); + + LoggedSql.start(); + MTJTrans found2 = DB.find(MTJTrans.class) + .setId(parent.id()) + .fetch("order") + .findOne(); + + assertThat(found2.order()).isNotNull(); + assertThat(found2.order().id()).isEqualTo(order.id()); + assertThat(found2.order().other()).isEqualTo("some"); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("from mtjtrans t0 left join mtjorder t1 on t1.org_id = t0.org_id and t1.id = t0.order_id where t0.id = ?"); } } From c6702aae933165adfaeb1a46a83de4b277950434 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 31 Aug 2025 12:37:55 +1200 Subject: [PATCH 4/4] #3664 Tidy up test --- .../src/test/java/org/tests/model/m2o/MTJOrder.java | 7 +++---- .../src/test/java/org/tests/model/m2o/MTJTrans.java | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java b/ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java index 2c3f363dba..160e252256 100644 --- a/ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java +++ b/ebean-test/src/test/java/org/tests/model/m2o/MTJOrder.java @@ -8,14 +8,13 @@ public class MTJOrder { @Id - Long id; + private long id; @Column(name = "org_id") - Long orgId; + private long orgId; @Column - String other; - + private String other; public Long id() { return id; diff --git a/ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java b/ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java index 3364ae1c85..589f004e97 100644 --- a/ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java +++ b/ebean-test/src/test/java/org/tests/model/m2o/MTJTrans.java @@ -6,17 +6,17 @@ public class MTJTrans { @Id - Long id; + private long id; @Column(name = "org_id") - Long orgId; + private long orgId; @ManyToOne @JoinColumns({ - @JoinColumn(name = "org_id", referencedColumnName = "org_id", insertable = false, updatable = false), + @JoinColumn(name = "org_id", referencedColumnName = "org_id"), // extra join column, not strictly needed @JoinColumn(name = "order_id", referencedColumnName = "id") }) - MTJOrder order; + private MTJOrder order; public Long id() { return id;