diff --git a/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java b/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java index 719dcbfcc..ba90cdd1c 100644 --- a/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java +++ b/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java @@ -11,6 +11,7 @@ import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.deploy.IndexDefinition; import io.ebeaninternal.server.deploy.InheritInfo; +import io.ebeaninternal.server.deploy.TableJoin; import io.ebeaninternal.server.deploy.TableJoinColumn; import io.ebeaninternal.server.deploy.id.ImportedId; @@ -148,8 +149,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor { TableJoinColumn[] columns = p.getTableJoin().columns(); if (columns.length == 0) { - String msg = "No join columns for " + p.getFullBeanName(); - throw new RuntimeException(msg); + throw new RuntimeException("No join columns for " + p.getFullBeanName()); } ImportedId importedId = p.getImportedId(); @@ -231,6 +231,12 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor { if (p.getBeanDescriptor().isUseIdGenerator()) { col.setIdentity(true); } + TableJoin primaryKeyJoin = p.getBeanDescriptor().getPrimaryKeyJoin(); + if (primaryKeyJoin != null) { + TableJoinColumn[] columns = primaryKeyJoin.columns(); + col.setReferences(primaryKeyJoin.getTable() + "." + columns[0].getForeignDbColumn()); + col.setForeignKeyName(determineForeignKeyConstraintName(col.getName())); + } } else { col.setDefaultValue(p.getDbColumnDefault()); col.setDbMigrationInfos(p.getDbMigrationInfos()); diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanCascadeInfo.java b/src/main/java/io/ebeaninternal/server/deploy/BeanCascadeInfo.java index 54801eae9..af231db1d 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanCascadeInfo.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanCascadeInfo.java @@ -22,7 +22,7 @@ public class BeanCascadeInfo { } } - private void setType(CascadeType type) { + public void setType(CascadeType type) { switch (type) { case ALL: save = true; diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 3a225cc92..c5b82b19d 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -179,6 +179,7 @@ public class BeanDescriptor implements BeanType { private final String baseTableAsOf; private final String baseTableVersionsBetween; private final boolean historySupport; + private final TableJoin primaryKeyJoin; private final BeanProperty softDeleteProperty; private final boolean softDelete; @@ -454,6 +455,7 @@ public class BeanDescriptor implements BeanType { this.draftTable = deploy.getDraftTable(); this.baseTable = InternString.intern(deploy.getBaseTable()); this.baseTableAsOf = deploy.getBaseTableAsOf(); + this.primaryKeyJoin = deploy.getPrimaryKeyJoin(); this.baseTableVersionsBetween = deploy.getBaseTableVersionsBetween(); this.dependentTables = deploy.getDependentTables(); this.dbComment = deploy.getDbComment(); @@ -2888,6 +2890,10 @@ public class BeanDescriptor implements BeanType { } } + public TableJoin getPrimaryKeyJoin() { + return primaryKeyJoin; + } + @Override public BeanProperty getIdProperty() { return idProperty; diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 20c7396e7..c9341c74a 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -1546,6 +1546,14 @@ public class BeanDescriptorManager implements BeanDescriptorMap { return changeLogListener; } + public void addPrimaryKeyJoin(DeployBeanPropertyAssocOne prop, DeployTableJoin inverse) { + + TableJoin inverseJoin = new TableJoin(inverse); + + DeployBeanInfo target = deployInfoMap.get(prop.getTargetType()); + target.setPrimaryKeyJoin(inverseJoin); + } + /** * Comparator to sort the BeanDescriptors by name. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index 6aabd4927..2bc1bd27c 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -39,15 +39,14 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { private final boolean importedPrimaryKey; + private final boolean primaryKeyExport; + private AssocOneHelp localHelp; protected final BeanProperty[] embeddedProps; private final HashMap embeddedPropsMap; - /** - * The information for Imported foreign Keys. - */ protected ImportedId importedId; private String deleteByParentIdSql; @@ -70,6 +69,7 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { super(descriptor, deploy); + primaryKeyExport = deploy.isPrimaryKeyExport(); importedPrimaryKey = deploy.isImportedPrimaryKey(); oneToOne = deploy.isOneToOne(); oneToOneExported = deploy.isOneToOneExported(); @@ -550,13 +550,17 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { @Override public void appendSelect(DbSqlContext ctx, boolean subQuery) { if (!isTransient) { - localHelp.appendSelect(ctx, subQuery); + if (primaryKeyExport) { + descriptor.getIdProperty().appendSelect(ctx, subQuery); + } else { + localHelp.appendSelect(ctx, subQuery); + } } } @Override public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { - if (!isTransient) { + if (!isTransient && !primaryKeyExport) { localHelp.appendFrom(ctx, joinType); if (sqlFormulaJoin != null) { ctx.appendFormulaJoin(sqlFormulaJoin, joinType); @@ -734,6 +738,11 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { */ public void setParentBeanToChild(EntityBean parent, EntityBean child) { + if (primaryKeyExport) { + Object parentId = descriptor.getId(parent); + targetDescriptor.convertSetId(parentId, child); + } + if (mappedBy != null) { BeanProperty beanProperty = targetDescriptor.getBeanProperty(mappedBy); if (beanProperty != null && beanProperty.getValue(child) == null) { diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanTable.java b/src/main/java/io/ebeaninternal/server/deploy/BeanTable.java index a091aa979..d9e3e1413 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanTable.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanTable.java @@ -121,4 +121,13 @@ public class BeanTable { } + /** + * Return the primary key DB column. + */ + public String getIdColumn() { + if (idProperties.length != 1) { + throw new IllegalStateException("Expecting only one Id column to join to on "+beanType); + } + return idProperties[0].dbColumn; + } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 396c3e06e..2e8b4165c 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -27,6 +27,7 @@ import io.ebeaninternal.server.deploy.ChainedBeanPostLoad; import io.ebeaninternal.server.deploy.ChainedBeanQueryAdapter; import io.ebeaninternal.server.deploy.IndexDefinition; import io.ebeaninternal.server.deploy.InheritInfo; +import io.ebeaninternal.server.deploy.TableJoin; import io.ebeaninternal.server.deploy.parse.DeployBeanInfo; import io.ebeaninternal.server.idgen.UuidIdGenerator; import io.ebeaninternal.server.rawsql.SpiRawSql; @@ -55,10 +56,7 @@ public class DeployBeanDescriptor { @Override public int compare(DeployBeanProperty o1, DeployBeanProperty o2) { - - int v2 = o1.getSortOrder(); - int v1 = o2.getSortOrder(); - return (v1 < v2 ? -1 : (v1 == v2 ? 0 : 1)); + return Integer.compare(o2.getSortOrder(), o1.getSortOrder()); } } @@ -203,6 +201,7 @@ public class DeployBeanDescriptor { private DocStoreMode docStoreDelete; private List idProperties; + private TableJoin primaryKeyJoin; private short profileId; @@ -215,6 +214,20 @@ public class DeployBeanDescriptor { this.beanType = beanType; } + /** + * PK is also a FK. + */ + public void setPrimaryKeyJoin(TableJoin join) { + this.primaryKeyJoin = join; + this.idType = IdType.EXTERNAL; + this.idGeneratorName = null; + this.idGenerator = null; + } + + public TableJoin getPrimaryKeyJoin() { + return primaryKeyJoin; + } + /** * Return the DeployBeanInfo for the given bean class. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java index fff547d9c..dc21bbe82 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java @@ -9,6 +9,8 @@ public class DeployBeanPropertyAssocOne extends DeployBeanPropertyAssoc { private boolean oneToOneExported; + private boolean primaryKeyExport; + private boolean importedPrimaryKey; private DeployBeanEmbedded deployEmbedded; @@ -115,4 +117,12 @@ public class DeployBeanPropertyAssocOne extends DeployBeanPropertyAssoc { public String getColumnPrefix() { return columnPrefix; } + + public void setPrimaryKeyExport() { + this.primaryKeyExport = true; + } + + public boolean isPrimaryKeyExport() { + return primaryKeyExport; + } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java index be49ab616..985608df7 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java @@ -6,9 +6,15 @@ import io.ebean.config.NamingConvention; import io.ebeaninternal.server.deploy.BeanDescriptorManager; import io.ebeaninternal.server.deploy.BeanTable; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssoc; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne; +import io.ebeaninternal.server.deploy.meta.DeployTableJoin; +import io.ebeaninternal.server.deploy.meta.DeployTableJoinColumn; import io.ebeaninternal.server.query.SqlJoinType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.EmbeddedId; @@ -17,6 +23,7 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToOne; import javax.persistence.OneToOne; +import javax.persistence.PrimaryKeyJoinColumn; import javax.validation.constraints.NotNull; /** @@ -24,12 +31,14 @@ import javax.validation.constraints.NotNull; */ public class AnnotationAssocOnes extends AnnotationParser { + private static final Logger log = LoggerFactory.getLogger(AnnotationAssocOnes.class); + private final BeanDescriptorManager factory; /** * Create with the deploy Info. */ - public AnnotationAssocOnes(DeployBeanInfo info, boolean javaxValidationAnnotations, BeanDescriptorManager factory) { + AnnotationAssocOnes(DeployBeanInfo info, boolean javaxValidationAnnotations, BeanDescriptorManager factory) { super(info, javaxValidationAnnotations); this.factory = factory; } @@ -88,6 +97,11 @@ public class AnnotationAssocOnes extends AnnotationParser { prop.setExtraWhere(where.clause()); } + PrimaryKeyJoinColumn primaryKeyJoin = get(prop, PrimaryKeyJoinColumn.class); + if (primaryKeyJoin != null) { + readPrimaryKeyJoin(primaryKeyJoin, prop); + } + FetchPreference fetchPreference = get(prop, FetchPreference.class); if (fetchPreference != null) { prop.setFetchPreference(fetchPreference.value()); @@ -156,18 +170,21 @@ public class AnnotationAssocOnes extends AnnotationParser { return "Error with association to [" + type + "] from [" + from + "]. Is " + type + " registered?"; } + private BeanTable beanTable(DeployBeanPropertyAssoc prop) { + BeanTable assoc = factory.getBeanTable(prop.getPropertyType()); + if (assoc == null) { + throw new RuntimeException(errorMsgMissingBeanTable(prop.getPropertyType(), prop.getFullBeanName())); + } + return assoc; + } + private void readManyToOne(ManyToOne propAnn, DeployBeanProperty prop) { DeployBeanPropertyAssocOne beanProp = (DeployBeanPropertyAssocOne) prop; setCascadeTypes(propAnn.cascade(), beanProp.getCascadeInfo()); - BeanTable assoc = factory.getBeanTable(beanProp.getPropertyType()); - if (assoc == null) { - String msg = errorMsgMissingBeanTable(beanProp.getPropertyType(), prop.getFullBeanName()); - throw new RuntimeException(msg); - } - beanProp.setBeanTable(assoc); + beanProp.setBeanTable(beanTable(beanProp)); beanProp.setDbInsertable(true); beanProp.setDbUpdateable(true); beanProp.setNullable(propAnn.optional()); @@ -187,14 +204,37 @@ public class AnnotationAssocOnes extends AnnotationParser { } setCascadeTypes(propAnn.cascade(), prop.getCascadeInfo()); + prop.setBeanTable(beanTable(prop)); + } - BeanTable assoc = factory.getBeanTable(prop.getPropertyType()); - if (assoc == null) { - String msg = errorMsgMissingBeanTable(prop.getPropertyType(), prop.getFullBeanName()); - throw new RuntimeException(msg); + private void readPrimaryKeyJoin(PrimaryKeyJoinColumn primaryKeyJoin, DeployBeanPropertyAssocOne prop) { + + if (!prop.isOneToOne()) { + throw new IllegalStateException("Expecting property " + prop.getFullBeanName() + " with PrimaryKeyJoinColumn to be a OneToOne?"); + } + prop.setPrimaryKeyExport(); + prop.setOneToOneExported(); + if (!prop.getCascadeInfo().isSave()) { + // we pretty much need to cascade save so turning that on automatically ... + prop.getCascadeInfo().setType(CascadeType.ALL); } - prop.setBeanTable(assoc); + if (!primaryKeyJoin.name().isEmpty()) { + log.warn("Automatically determining join columns and ignoring PrimaryKeyJoinColumn.name {} on {}", primaryKeyJoin.name(), prop.getFullBeanName()); + } + if (!primaryKeyJoin.referencedColumnName().isEmpty()) { + log.warn("Automatically determining join columns and Ignoring PrimaryKeyJoinColumn.referencedColumnName {} on {}", primaryKeyJoin.referencedColumnName(), prop.getFullBeanName()); + } + + BeanTable baseBeanTable = factory.getBeanTable(info.getDescriptor().getBeanType()); + + String localPrimaryKey = baseBeanTable.getIdColumn(); + String foreignColumn = beanTable(prop).getIdColumn(); + + prop.getTableJoin().addJoinColumn(new DeployTableJoinColumn(localPrimaryKey, foreignColumn, false, false)); + + DeployTableJoin inverse = prop.getTableJoin().createInverse(baseBeanTable.getBaseTable()); + factory.addPrimaryKeyJoin(prop, inverse); } private void readEmbedded(DeployBeanPropertyAssocOne prop, Embedded embedded) { diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/DeployBeanInfo.java b/src/main/java/io/ebeaninternal/server/deploy/parse/DeployBeanInfo.java index f84a9dd69..553e263c8 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/DeployBeanInfo.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/DeployBeanInfo.java @@ -1,11 +1,12 @@ package io.ebeaninternal.server.deploy.parse; import io.ebean.RawSql; -import io.ebeaninternal.server.rawsql.SpiRawSql; +import io.ebeaninternal.server.deploy.TableJoin; import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne; import io.ebeaninternal.server.deploy.meta.DeployTableJoin; import io.ebeaninternal.server.query.SqlJoinType; +import io.ebeaninternal.server.rawsql.SpiRawSql; import java.util.HashMap; @@ -91,4 +92,11 @@ public class DeployBeanInfo { public void addNamedQuery(String name, String query) { descriptor.addNamedQuery(name, query); } + + /** + * Set that the PK is also a foreign key. + */ + public void setPrimaryKeyJoin(TableJoin join) { + descriptor.setPrimaryKeyJoin(join); + } } diff --git a/src/test/java/org/tests/model/onetoone/OtoPrime.java b/src/test/java/org/tests/model/onetoone/OtoPrime.java new file mode 100644 index 000000000..d1f2ddb20 --- /dev/null +++ b/src/test/java/org/tests/model/onetoone/OtoPrime.java @@ -0,0 +1,67 @@ +package org.tests.model.onetoone; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.PrimaryKeyJoinColumn; +import javax.persistence.Version; + +@Entity +public class OtoPrime { + + @Id + Long pid; + + String name; + + /** + * Automatically set Cascade PERSIST and mapped by. + */ + @OneToOne(optional = false, cascade = CascadeType.ALL) //, mappedBy = "prime") + @PrimaryKeyJoinColumn + OtoPrimeExtra extra; + + @Version + Long version; + + public OtoPrime(String name) { + this.name = name; + } + + public String toString() { + return "id:"+ pid +" name:"+name+" extra:"+extra; + } + + public Long getPid() { + return pid; + } + + public void setPid(Long pid) { + this.pid = pid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OtoPrimeExtra getExtra() { + return extra; + } + + public void setExtra(OtoPrimeExtra extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/model/onetoone/OtoPrimeExtra.java b/src/test/java/org/tests/model/onetoone/OtoPrimeExtra.java new file mode 100644 index 000000000..acb6fc28e --- /dev/null +++ b/src/test/java/org/tests/model/onetoone/OtoPrimeExtra.java @@ -0,0 +1,49 @@ +package org.tests.model.onetoone; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; + +@Entity +public class OtoPrimeExtra { + + @Id + Long eid; + + String extra; + + @Version + Long version; + + public OtoPrimeExtra(String extra) { + this.extra = extra; + } + + public String toString() { + return "exId:"+ eid +" "+extra; + } + + public Long getEid() { + return eid; + } + + public void setEid(Long eid) { + this.eid = eid; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoin.java b/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoin.java new file mode 100644 index 000000000..b2ab2d1c6 --- /dev/null +++ b/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoin.java @@ -0,0 +1,87 @@ +package org.tests.model.onetoone; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Query; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestOneToOnePrimaryKeyJoin extends BaseTestCase { + + + private OtoPrime insert(String desc) { + OtoPrime prime = new OtoPrime("p" + desc); + OtoPrimeExtra extra = new OtoPrimeExtra("e" + desc); + prime.setExtra(extra); + Ebean.save(prime); + return prime; + } + + @Test + public void insertUpdateDelete() { + + String desc = "" + System.currentTimeMillis(); + OtoPrime p1 = insert(desc); + + assertThat(p1.getExtra().getEid()).isEqualTo(p1.getPid()).as("Same id values"); + + + Query query = Ebean.find(OtoPrime.class).setId(p1.getPid()); + + OtoPrime found = query.findOne(); + + assertThat(found).isNotNull(); + assertThat(sqlOf(query)).contains("select t0.pid, t0.name, t0.version, t0.pid from oto_prime t0 where t0.pid = ?") + .as("we don't join to oto_prime_extra"); + + assertThat(found.getName()).isEqualTo("p" + desc); + + Query queryWithFetch = Ebean.find(OtoPrime.class) + .setId(p1.getPid()) + .fetch("extra"); + + OtoPrime oneWith = queryWithFetch.findOne(); + + assertThat(oneWith).isNotNull(); + assertThat(sqlOf(queryWithFetch)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_prime t0 join oto_prime_extra t1 on t1.eid = t0.pid where t0.pid = ?") + .as("we join to oto_prime_extra"); + + + assertThat(oneWith.getExtra().getExtra()).isEqualTo("e" + desc); + + thenUpdate(oneWith); + thenDelete(found); + } + + private void thenUpdate(OtoPrime oneWith) { + + OtoPrimeExtra extra = oneWith.getExtra(); + extra.setExtra("modified"); + + Ebean.save(oneWith); + + extra.setExtra("mod2"); + oneWith.setName("mod2"); + + Ebean.save(oneWith); + } + + private void thenDelete(OtoPrime found) { + + + OtoPrime bean = Ebean.find(OtoPrime.class, found.getPid()); + + LoggedSqlCollector.start(); + Ebean.delete(bean); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from oto_prime_extra where"); + assertThat(sql.get(1)).contains("delete from oto_prime where"); + + } +}