#107 - Mapping - Add support for @PrimaryKeyJoinColumn/@PrimaryKeyJoinColumns

This commit is contained in:
Rob Bygrave
2018-02-14 22:59:57 +13:00
parent 6db9a60f4f
commit 842292ef6b
13 changed files with 337 additions and 25 deletions
@@ -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());
@@ -22,7 +22,7 @@ public class BeanCascadeInfo {
}
}
private void setType(CascadeType type) {
public void setType(CascadeType type) {
switch (type) {
case ALL:
save = true;
@@ -179,6 +179,7 @@ public class BeanDescriptor<T> implements BeanType<T> {
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<T> implements BeanType<T> {
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<T> implements BeanType<T> {
}
}
public TableJoin getPrimaryKeyJoin() {
return primaryKeyJoin;
}
@Override
public BeanProperty getIdProperty() {
return idProperty;
@@ -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.
*/
@@ -39,15 +39,14 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> {
private final boolean importedPrimaryKey;
private final boolean primaryKeyExport;
private AssocOneHelp localHelp;
protected final BeanProperty[] embeddedProps;
private final HashMap<String, BeanProperty> embeddedPropsMap;
/**
* The information for Imported foreign Keys.
*/
protected ImportedId importedId;
private String deleteByParentIdSql;
@@ -70,6 +69,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> {
super(descriptor, deploy);
primaryKeyExport = deploy.isPrimaryKeyExport();
importedPrimaryKey = deploy.isImportedPrimaryKey();
oneToOne = deploy.isOneToOne();
oneToOneExported = deploy.isOneToOneExported();
@@ -550,13 +550,17 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> {
@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<T> extends BeanPropertyAssoc<T> {
*/
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) {
@@ -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;
}
}
@@ -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<T> {
@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<T> {
private DocStoreMode docStoreDelete;
private List<DeployBeanProperty> idProperties;
private TableJoin primaryKeyJoin;
private short profileId;
@@ -215,6 +214,20 @@ public class DeployBeanDescriptor<T> {
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.
*/
@@ -9,6 +9,8 @@ public class DeployBeanPropertyAssocOne<T> extends DeployBeanPropertyAssoc<T> {
private boolean oneToOneExported;
private boolean primaryKeyExport;
private boolean importedPrimaryKey;
private DeployBeanEmbedded deployEmbedded;
@@ -115,4 +117,12 @@ public class DeployBeanPropertyAssocOne<T> extends DeployBeanPropertyAssoc<T> {
public String getColumnPrefix() {
return columnPrefix;
}
public void setPrimaryKeyExport() {
this.primaryKeyExport = true;
}
public boolean isPrimaryKeyExport() {
return primaryKeyExport;
}
}
@@ -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) {
@@ -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<T> {
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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<OtoPrime> 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<OtoPrime> 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<String> 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");
}
}