Files
ebean/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java
T
Rob Bygrave 721654d033 #1140 - Error loading embedded instance
The issue was due to the loaded status not being recursively set on embedded
beans after inserts. The test case invoked lazy loading due to that which
lead to the issue.
2017-10-03 12:47:29 +13:00

767 lines
22 KiB
Java

package io.ebeaninternal.server.deploy;
import io.ebean.EbeanServer;
import io.ebean.Query;
import io.ebean.SqlUpdate;
import io.ebean.Transaction;
import io.ebean.ValuePair;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
import io.ebeaninternal.server.cache.CacheChangeSet;
import io.ebeaninternal.server.cache.CachedBeanData;
import io.ebeaninternal.server.core.DefaultSqlUpdate;
import io.ebeaninternal.server.deploy.id.ImportedId;
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
import io.ebeaninternal.server.el.ElPropertyChainBuilder;
import io.ebeaninternal.server.el.ElPropertyValue;
import io.ebeaninternal.server.query.SplitName;
import io.ebeaninternal.server.query.SqlBeanLoad;
import io.ebeaninternal.server.query.SqlJoinType;
import io.ebeaninternal.server.text.json.ReadJson;
import io.ebeaninternal.server.text.json.SpiJsonWriter;
import javax.persistence.PersistenceException;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Property mapped to a joined bean.
*/
public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> {
private final boolean oneToOne;
private final boolean oneToOneExported;
private final boolean importedPrimaryKey;
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;
private String deleteByParentIdInSql;
private BeanPropertyAssocMany<?> relationshipProperty;
/**
* Create based on deploy information of an EmbeddedId.
*/
public BeanPropertyAssocOne(BeanDescriptorMap owner, DeployBeanPropertyAssocOne<T> deploy) {
this(owner, null, deploy);
}
/**
* Create the property.
*/
public BeanPropertyAssocOne(BeanDescriptorMap owner, BeanDescriptor<?> descriptor,
DeployBeanPropertyAssocOne<T> deploy) {
super(descriptor, deploy);
importedPrimaryKey = deploy.isImportedPrimaryKey();
oneToOne = deploy.isOneToOne();
oneToOneExported = deploy.isOneToOneExported();
if (embedded) {
// Overriding of the columns and use table alias of owning BeanDescriptor
BeanEmbeddedMeta overrideMeta = BeanEmbeddedMetaFactory.create(owner, deploy);
embeddedProps = overrideMeta.getProperties();
embeddedPropsMap = new HashMap<>();
for (BeanProperty embeddedProp : embeddedProps) {
embeddedPropsMap.put(embeddedProp.getName(), embeddedProp);
}
} else {
embeddedProps = null;
embeddedPropsMap = null;
}
}
@Override
public void initialise() {
super.initialise();
localHelp = createHelp(embedded, oneToOneExported);
if (!isTransient) {
//noinspection StatementWithEmptyBody
if (embedded) {
// no imported or exported information
} else if (!oneToOneExported) {
importedId = createImportedId(this, targetDescriptor, tableJoin);
if (importedId.isScalar()) {
// limit JoinColumn mapping to the @Id / primary key
TableJoinColumn[] columns = tableJoin.columns();
String foreignJoinColumn = columns[0].getForeignDbColumn();
String foreignIdColumn = targetDescriptor.getIdProperty().getDbColumn();
if (!foreignJoinColumn.equalsIgnoreCase(foreignIdColumn)) {
throw new PersistenceException("Mapping limitation - @JoinColumn on " + getFullBeanName() + " 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.getBaseTable() + " where ";
deleteByParentIdSql = delStmt + deriveWhereParentIdSql(false);
deleteByParentIdInSql = delStmt + deriveWhereParentIdSql(true);
}
}
}
/**
* Return the property value as an entity bean.
*/
public EntityBean getValueAsEntityBean(EntityBean owner) {
return (EntityBean) getValue(owner);
}
void setRelationshipProperty(BeanPropertyAssocMany<?> relationshipProperty) {
this.relationshipProperty = relationshipProperty;
}
/**
* Return true if this relationship needs to maintain/update L2 cache.
*/
boolean isCacheNotify() {
return targetDescriptor.isBeanCaching() && relationshipProperty != null;
}
/**
* Clear the L2 relationship cache for this property.
*/
void cacheClear() {
if (isCacheNotify()) {
targetDescriptor.cacheManyPropClear(relationshipProperty.getName());
}
}
/**
* Clear part of the L2 relationship cache for this property.
*/
void cacheDelete(boolean clear, EntityBean bean, CacheChangeSet changeSet) {
if (isCacheNotify()) {
if (clear) {
changeSet.addManyClear(targetDescriptor, relationshipProperty.getName());
} else {
Object assocBean = getValue(bean);
if (assocBean != null) {
Object parentId = targetDescriptor.getId((EntityBean) assocBean);
if (parentId != null) {
changeSet.addManyRemove(targetDescriptor, relationshipProperty.getName(), parentId);
}
}
}
}
}
@Override
public ElPropertyValue buildElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) {
if (embedded) {
BeanProperty embProp = embeddedPropsMap.get(remainder);
if (embProp == null) {
String msg = "Embedded Property " + remainder + " not found in " + getFullBeanName();
throw new PersistenceException(msg);
}
if (chain == null) {
chain = new ElPropertyChainBuilder(true, propName);
}
chain.add(this);
chain.setEmbedded(true);
return chain.add(embProp).build();
}
return createElPropertyValue(propName, remainder, chain, propertyDeploy);
}
@Override
public String getElPlaceholder(boolean encrypted) {
return encrypted ? elPlaceHolderEncrypted : elPlaceHolder;
}
public SqlUpdate deleteByParentId(Object parentId, List<Object> parentIdist) {
if (parentId != null) {
return deleteByParentId(parentId);
} else {
return deleteByParentIdList(parentIdist);
}
}
private SqlUpdate deleteByParentIdList(List<Object> parentIdist) {
StringBuilder sb = new StringBuilder(100);
sb.append(deleteByParentIdInSql);
String inClause = targetIdBinder.getIdInValueExpr(parentIdist.size());
sb.append(inClause);
DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString());
for (Object aParentIdist : parentIdist) {
targetIdBinder.bindId(delete, aParentIdist);
}
return delete;
}
private SqlUpdate deleteByParentId(Object parentId) {
DefaultSqlUpdate delete = new DefaultSqlUpdate(deleteByParentIdSql);
if (exportedProperties.length == 1) {
delete.addParameter(parentId);
} else {
targetDescriptor.getIdBinder().bindId(delete, parentId);
}
return delete;
}
public List<Object> findIdsByParentId(Object parentId, List<Object> parentIdist, Transaction t) {
if (parentId != null) {
return findIdsByParentId(parentId, t);
} else {
return findIdsByParentIdList(parentIdist, t);
}
}
private List<Object> findIdsByParentId(Object parentId, Transaction t) {
String rawWhere = deriveWhereParentIdSql(false);
List<Object> bindValues = new ArrayList<>();
bindWhereParentId(bindValues, parentId);
EbeanServer server = getBeanDescriptor().getEbeanServer();
Query<?> q = server.find(getPropertyType())
.where()
.raw(rawWhere, bindValues.toArray())
.query();
return server.findIds(q, t);
}
private List<Object> findIdsByParentIdList(List<Object> parentIdList, Transaction t) {
String rawWhere = deriveWhereParentIdSql(true);
String inClause = targetIdBinder.getIdInValueExpr(parentIdList.size());
String expr = rawWhere + inClause;
List<Object> bindValues = new ArrayList<>();
for (Object aParentIdList : parentIdList) {
bindWhereParentId(bindValues, aParentIdList);
}
EbeanServer server = getBeanDescriptor().getEbeanServer();
Query<?> q = server.find(getPropertyType())
.where().raw(expr, bindValues.toArray()).query();
return server.findIds(q, t);
}
void addFkey() {
if (importedId != null) {
importedId.addFkeys(name);
}
}
@Override
public void registerColumn(BeanDescriptor<?> desc, String prefix) {
if (embedded) {
for (BeanProperty prop : embeddedProps) {
prop.registerColumn(desc, SplitName.add(prefix, name));
}
} else {
if (targetIdProperty != null) {
BeanDescriptor<T> target = getTargetDescriptor();
String basePath = SplitName.add(prefix, name);
if (dbColumn != null) {
BeanProperty idProperty = target.getIdProperty();
desc.registerColumn(dbColumn, SplitName.add(basePath, idProperty.getName()));
}
desc.registerTable(target.getBaseTable(), this);
}
}
}
/**
* Return meta data for the deployment of the embedded bean specific to this
* property.
*/
public BeanProperty[] getProperties() {
return embeddedProps;
}
@Override
public void buildRawSqlSelectChain(String prefix, List<String> selectChain) {
prefix = SplitName.add(prefix, name);
if (!embedded) {
InheritInfo inheritInfo = targetDescriptor.getInheritInfo();
if (inheritInfo != null) {
// expect the discriminator column to be included in order
// to determine the inheritance type so we add it to the
// selectChain (so that it takes a position in the resultSet)
String discriminatorColumn = inheritInfo.getDiscriminatorColumn();
String discProperty = prefix + "." + discriminatorColumn;
selectChain.add(discProperty);
}
targetIdBinder.buildRawSqlSelectChain(prefix, selectChain);
} else {
for (BeanProperty embeddedProp : embeddedProps) {
embeddedProp.buildRawSqlSelectChain(prefix, selectChain);
}
}
}
/**
* Return true if this a OneToOne property. Otherwise assumed ManyToOne.
*/
public boolean isOneToOne() {
return oneToOne;
}
/**
* Return true if this is the exported side of a OneToOne.
*/
public boolean isOneToOneExported() {
return oneToOneExported;
}
/**
* If true this bean maps to the primary key.
*/
public boolean isImportedPrimaryKey() {
return importedPrimaryKey;
}
@Override
public void diff(String prefix, Map<String, ValuePair> map, EntityBean newBean, EntityBean oldBean) {
Object newEmb = (newBean == null) ? null : getValue(newBean);
Object oldEmb = (oldBean == null) ? null : getValue(oldBean);
if (newEmb == null && oldEmb == null) {
return;
}
if (embedded) {
prefix = (prefix == null) ? name : prefix + "." + name;
BeanDescriptor<T> targetDescriptor = getTargetDescriptor();
targetDescriptor.diff(prefix, map, (EntityBean) newEmb, (EntityBean) oldEmb);
} else {
// we are only interested in the Id value
newBean = (EntityBean) newEmb;
oldBean = (EntityBean) oldEmb;
BeanDescriptor<T> targetDescriptor = getTargetDescriptor();
BeanProperty idProperty = targetDescriptor.getIdProperty();
Object newId = (newBean == null) ? null : idProperty.getValue(newBean);
Object oldId = (oldBean == null) ? null : idProperty.getValue(oldBean);
if (newId != null || oldId != null) {
prefix = (prefix == null) ? name : prefix + "." + name;
idProperty.diffVal(prefix, map, newId, oldId);
}
}
}
/**
* Same as getPropertyType(). Return the type of the bean this property
* represents.
*/
@Override
public Class<?> getTargetType() {
return getPropertyType();
}
@Override
public Object getCacheDataValue(EntityBean bean) {
Object ap = getValue(bean);
if (ap == null) {
return null;
}
if (embedded) {
return targetDescriptor.cacheEmbeddedBeanExtract((EntityBean) ap);
} else {
return targetDescriptor.getIdProperty().getCacheDataValue((EntityBean) ap);
}
}
@Override
public void setCacheDataValue(EntityBean bean, Object cacheData, PersistenceContext context) {
if (cacheData == null) {
setValue(bean, null);
} else {
if (embedded) {
setValue(bean, targetDescriptor.cacheEmbeddedBeanLoad((CachedBeanData) cacheData, context));
} else {
if (cacheData instanceof String) {
cacheData = targetDescriptor.getIdProperty().scalarType.parse((String) cacheData);
}
// cacheData is the id value, maybe already in persistence context
Object assocBean = targetDescriptor.contextGet(context, cacheData);
if (assocBean == null) {
assocBean = targetDescriptor.createReference(Boolean.FALSE, false, cacheData, context);
}
setValue(bean, assocBean);
}
}
}
/**
* Return the Id values from the given bean.
*/
@Override
public Object[] getAssocIdValues(EntityBean bean) {
return targetDescriptor.getIdBinder().getIdValues(bean);
}
/**
* Return the Id expression to add to where clause etc.
*/
@Override
public String getAssocIdExpression(String prefix, String operator) {
return targetDescriptor.getIdBinder().getAssocOneIdExpr(prefix, operator);
}
/**
* Return the logical id value expression taking into account embedded id's.
*/
@Override
public String getAssocIdInValueExpr(int size) {
return targetDescriptor.getIdBinder().getIdInValueExpr(size);
}
/**
* Return the logical id in expression taking into account embedded id's.
*/
@Override
public String getAssocIdInExpr(String prefix) {
return targetDescriptor.getIdBinder().getAssocIdInExpr(prefix);
}
@Override
public boolean isAssocId() {
return !embedded;
}
@Override
public boolean isAssocProperty() {
return !embedded;
}
/**
* Create a bean of the target type to be used as an embeddedId
* value.
*/
public Object createEmbeddedId() {
return getTargetDescriptor().createEntityBean();
}
@Override
public Object pathGetNested(Object bean) {
Object value = getValueIntercept((EntityBean) bean);
if (value == null) {
value = targetDescriptor.createEntityBean();
setValueIntercept((EntityBean) bean, value);
}
return value;
}
public ImportedId getImportedId() {
return importedId;
}
private String deriveWhereParentIdSql(boolean inClause) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < exportedProperties.length; i++) {
String fkColumn = exportedProperties[i].getForeignDbColumn();
if (i > 0) {
String s = inClause ? "," : " and ";
sb.append(s);
}
sb.append(fkColumn);
if (!inClause) {
sb.append("=? ");
}
}
return sb.toString();
}
/**
* Create the array of ExportedProperty used to build reference objects.
*/
private ExportedProperty[] createExported() {
BeanProperty idProp = descriptor.getIdProperty();
ArrayList<ExportedProperty> list = new ArrayList<>();
if (idProp != null && idProp.isEmbedded()) {
BeanPropertyAssocOne<?> one = (BeanPropertyAssocOne<?>) idProp;
BeanDescriptor<?> targetDesc = one.getTargetDescriptor();
BeanProperty[] emIds = targetDesc.propertiesBaseScalar();
try {
for (BeanProperty emId : emIds) {
ExportedProperty expProp = findMatch(true, emId);
list.add(expProp);
}
} catch (PersistenceException e) {
// not found as individual scalar properties
e.printStackTrace();
}
} else {
if (idProp != null) {
ExportedProperty expProp = findMatch(false, idProp);
list.add(expProp);
}
}
return list.toArray(new ExportedProperty[list.size()]);
}
/**
* Find the matching foreignDbColumn for a given local property.
*/
private ExportedProperty findMatch(boolean embeddedProp, BeanProperty prop) {
String matchColumn = prop.getDbColumn();
String searchTable = tableJoin.getTable();
TableJoinColumn[] columns = tableJoin.columns();
for (TableJoinColumn column : columns) {
String matchTo = column.getLocalDbColumn();
if (matchColumn.equalsIgnoreCase(matchTo)) {
String foreignCol = column.getForeignDbColumn();
return new ExportedProperty(embeddedProp, foreignCol, prop);
}
}
String msg = "Error with the Join on [" + getFullBeanName()
+ "]. Could not find the matching foreign key for [" + matchColumn + "] in table[" + searchTable + "]?"
+ " Perhaps using a @JoinColumn with the name/referencedColumnName attributes swapped?";
throw new PersistenceException(msg);
}
@Override
public void appendSelect(DbSqlContext ctx, boolean subQuery) {
if (!isTransient) {
localHelp.appendSelect(ctx, subQuery);
}
}
@Override
public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) {
if (!isTransient) {
localHelp.appendFrom(ctx, joinType);
if (sqlFormulaJoin != null) {
ctx.appendFormulaJoin(sqlFormulaJoin, joinType);
}
}
}
@Override
public Object readSet(DbReadContext ctx, EntityBean bean) throws SQLException {
return localHelp.readSet(ctx, bean);
}
/**
* Read the data from the resultSet effectively ignoring it and returning null.
*/
@Override
public Object read(DbReadContext ctx) throws SQLException {
// just read the resultSet incrementing the column index
// pass in null for the bean so any data read is ignored
return localHelp.read(ctx);
}
@Override
public void setValue(EntityBean bean, Object value) {
super.setValue(bean, value);
if (embedded && value instanceof EntityBean) {
setEmbeddedOwner(bean, value);
}
}
/**
* For embedded bean set the owner and all properties to be loaded (recursively).
*/
void setAllLoadedEmbedded(EntityBean owner) {
Object emb = getValue(owner);
if (emb != null) {
EntityBean embeddedBean = (EntityBean) emb;
embeddedBean._ebean_getIntercept().setEmbeddedOwner(owner, propertyIndex);
targetDescriptor.setAllLoaded(embeddedBean);
}
}
private void setEmbeddedOwner(EntityBean bean, Object value) {
((EntityBean) value)._ebean_getIntercept().setEmbeddedOwner(bean, propertyIndex);
}
@Override
public void setValueIntercept(EntityBean bean, Object value) {
super.setValueIntercept(bean, value);
if (embedded && value instanceof EntityBean) {
setEmbeddedOwner(bean, value);
}
}
@Override
public void loadIgnore(DbReadContext ctx) {
localHelp.loadIgnore(ctx);
}
@Override
public void load(SqlBeanLoad sqlBeanLoad) {
Object dbVal = sqlBeanLoad.load(this);
if (embedded && sqlBeanLoad.isLazyLoad()) {
if (dbVal instanceof EntityBean) {
((EntityBean) dbVal)._ebean_getIntercept().setLoaded();
}
}
}
private AssocOneHelp createHelp(boolean embedded, boolean oneToOneExported) {
if (embedded) {
return new AssocOneHelpEmbedded(this);
} else if (oneToOneExported) {
return new AssocOneHelpRefExported(this);
} else {
if (targetInheritInfo != null) {
return new AssocOneHelpRefInherit(this);
} else {
return new AssocOneHelpRefSimple(this);
}
}
}
/**
* JSON write property (non-recursive to other beans).
*/
@Override
public void jsonWriteForInsert(SpiJsonWriter writeJson, EntityBean bean) throws IOException {
if (!jsonSerialize) {
return;
}
jsonWriteBean(writeJson, getValue(bean));
}
/**
* JSON write property value (non-recursive to other beans).
*/
@Override
public void jsonWriteValue(SpiJsonWriter writeJson, Object value) throws IOException {
if (!jsonSerialize) {
return;
}
jsonWriteBean(writeJson, value);
}
private void jsonWriteBean(SpiJsonWriter writeJson, Object value) throws IOException {
if (value instanceof EntityBean) {
if (embedded) {
writeJson.writeFieldName(name);
BeanDescriptor<?> refDesc = descriptor.getBeanDescriptor(value.getClass());
refDesc.jsonWriteForInsert(writeJson, (EntityBean)value);
} else {
jsonWriteTargetId(writeJson, (EntityBean)value);
}
}
}
/**
* Just write the Id property of the ToOne property.
*/
private void jsonWriteTargetId(SpiJsonWriter writeJson, EntityBean childBean) throws IOException {
BeanProperty idProperty = targetDescriptor.getIdProperty();
if (idProperty != null) {
writeJson.writeStartObject(name);
idProperty.jsonWriteForInsert(writeJson, childBean);
writeJson.writeEndObject();
}
}
@Override
public void jsonWrite(SpiJsonWriter writeJson, EntityBean bean) throws IOException {
if (!jsonSerialize) {
return;
}
Object value = getValueIntercept(bean);
if (value == null) {
writeJson.writeNullField(name);
} else {
if (writeJson.isParentBean(value)) {
// bi-directional and already rendered parent
} else {
// Hmmm, not writing complex non-entity bean
if (value instanceof EntityBean) {
writeJson.beginAssocOne(name, bean);
BeanDescriptor<?> refDesc = descriptor.getBeanDescriptor(value.getClass());
refDesc.jsonWrite(writeJson, (EntityBean) value, name);
writeJson.endAssocOne();
}
}
}
}
@Override
public void jsonRead(ReadJson readJson, EntityBean bean) throws IOException {
if (jsonDeserialize && targetDescriptor != null) {
T assocBean = targetDescriptor.jsonRead(readJson, name);
setValue(bean, assocBean);
}
}
public boolean isReference(Object detailBean) {
EntityBean eb = (EntityBean) detailBean;
return targetDescriptor.isReference(eb._ebean_getIntercept());
}
/**
* Set the parent bean to the child bean if it has not already been set.
*/
public void setParentBeanToChild(EntityBean parent, EntityBean child) {
if (mappedBy != null) {
BeanProperty beanProperty = targetDescriptor.getBeanProperty(mappedBy);
if (beanProperty != null && beanProperty.getValue(child) == null) {
// set the 'parent' bean to the 'child' bean
beanProperty.setValue(child, parent);
}
}
}
}