mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
1049 lines
30 KiB
Java
1049 lines
30 KiB
Java
package com.avaje.ebeaninternal.server.deploy;
|
|
|
|
import com.avaje.ebean.EbeanServer;
|
|
import com.avaje.ebean.Expression;
|
|
import com.avaje.ebean.Query;
|
|
import com.avaje.ebean.SqlUpdate;
|
|
import com.avaje.ebean.Transaction;
|
|
import com.avaje.ebean.bean.BeanCollection;
|
|
import com.avaje.ebean.bean.BeanCollection.ModifyListenMode;
|
|
import com.avaje.ebean.bean.BeanCollectionAdd;
|
|
import com.avaje.ebean.bean.BeanCollectionLoader;
|
|
import com.avaje.ebean.bean.EntityBean;
|
|
import com.avaje.ebean.text.PathProperties;
|
|
import com.avaje.ebeaninternal.api.SpiExpressionRequest;
|
|
import com.avaje.ebeaninternal.api.SpiQuery;
|
|
import com.avaje.ebeaninternal.server.core.DefaultSqlUpdate;
|
|
import com.avaje.ebeaninternal.server.deploy.id.ImportedId;
|
|
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
|
|
import com.avaje.ebeaninternal.server.el.ElPropertyChainBuilder;
|
|
import com.avaje.ebeaninternal.server.el.ElPropertyValue;
|
|
import com.avaje.ebeaninternal.server.query.SqlBeanLoad;
|
|
import com.avaje.ebeaninternal.server.text.json.ReadJson;
|
|
import com.avaje.ebeaninternal.server.text.json.WriteJson;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import javax.persistence.PersistenceException;
|
|
import java.io.IOException;
|
|
import java.sql.SQLException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Property mapped to a List Set or Map.
|
|
*/
|
|
public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(BeanPropertyAssocMany.class);
|
|
|
|
private final BeanPropertyAssocManyJsonHelp jsonHelp;
|
|
|
|
/**
|
|
* Join for manyToMany intersection table.
|
|
*/
|
|
private final TableJoin intersectionJoin;
|
|
private final String intersectionPublishTable;
|
|
private final String intersectionDraftTable;
|
|
|
|
/**
|
|
* For ManyToMany this is the Inverse join used to build reference queries.
|
|
*/
|
|
private final TableJoin inverseJoin;
|
|
|
|
/**
|
|
* Flag to indicate that this is a unidirectional relationship.
|
|
*/
|
|
private final boolean unidirectional;
|
|
|
|
/**
|
|
* Flag to indicate manyToMany relationship.
|
|
*/
|
|
private final boolean manyToMany;
|
|
|
|
/**
|
|
* Order by used when fetch joining the associated many.
|
|
*/
|
|
private final String fetchOrderBy;
|
|
|
|
/**
|
|
* Order by used when lazy loading the associated many.
|
|
*/
|
|
private String lazyFetchOrderBy;
|
|
|
|
private final String mapKey;
|
|
|
|
/**
|
|
* The type of the many, set, list or map.
|
|
*/
|
|
private final ManyType manyType;
|
|
|
|
private final ModifyListenMode modifyListenMode;
|
|
|
|
private BeanProperty mapKeyProperty;
|
|
|
|
private String exportedPropertyBindProto = "?";
|
|
|
|
/**
|
|
* Property on the 'child' bean that links back to the 'master'.
|
|
*/
|
|
protected BeanPropertyAssocOne<?> childMasterProperty;
|
|
|
|
private String childMasterIdProperty;
|
|
|
|
private boolean embeddedExportedProperties;
|
|
|
|
private BeanCollectionHelp<T> help;
|
|
|
|
private ImportedId importedId;
|
|
|
|
private String deleteByParentIdSql;
|
|
|
|
private String deleteByParentIdInSql;
|
|
|
|
/**
|
|
* Create this property.
|
|
*/
|
|
public BeanPropertyAssocMany(BeanDescriptor<?> descriptor, DeployBeanPropertyAssocMany<T> deploy) {
|
|
super(descriptor, deploy);
|
|
this.unidirectional = deploy.isUnidirectional();
|
|
this.manyToMany = deploy.isManyToMany();
|
|
this.manyType = deploy.getManyType();
|
|
this.mapKey = deploy.getMapKey();
|
|
this.fetchOrderBy = deploy.getFetchOrderBy();
|
|
this.intersectionJoin = deploy.createIntersectionTableJoin();
|
|
if (intersectionJoin != null) {
|
|
this.intersectionPublishTable = intersectionJoin.getTable();
|
|
this.intersectionDraftTable = deploy.getIntersectionDraftTable();
|
|
} else {
|
|
this.intersectionPublishTable = null;
|
|
this.intersectionDraftTable = null;
|
|
}
|
|
this.inverseJoin = deploy.createInverseTableJoin();
|
|
this.modifyListenMode = deploy.getModifyListenMode();
|
|
this.jsonHelp = new BeanPropertyAssocManyJsonHelp(this);
|
|
}
|
|
|
|
public void initialise() {
|
|
super.initialise();
|
|
|
|
if (!isTransient) {
|
|
this.help = BeanCollectionHelpFactory.create(this);
|
|
|
|
if (manyToMany) {
|
|
// only manyToMany's have imported properties
|
|
importedId = createImportedId(this, targetDescriptor, tableJoin);
|
|
|
|
} else {
|
|
// find the property in the many that matches
|
|
// back to the master (Order in the OrderDetail bean)
|
|
childMasterProperty = initChildMasterProperty();
|
|
if (childMasterProperty != null) {
|
|
childMasterProperty.setRelationshipProperty(this);
|
|
}
|
|
}
|
|
|
|
if (mapKey != null) {
|
|
mapKeyProperty = initMapKeyProperty();
|
|
}
|
|
|
|
exportedProperties = createExported();
|
|
if (exportedProperties.length > 0) {
|
|
embeddedExportedProperties = exportedProperties[0].isEmbedded();
|
|
exportedPropertyBindProto = deriveExportedPropertyBindProto();
|
|
|
|
if (fetchOrderBy != null) {
|
|
// derive lazyFetchOrderBy
|
|
StringBuilder sb = new StringBuilder(50);
|
|
for (int i = 0; i < exportedProperties.length; i++) {
|
|
if (i > 0) {
|
|
sb.append(", ");
|
|
}
|
|
// these fk columns are either on the intersection (int_) or base table (t0)
|
|
String fkTableAlias = isManyToMany() ? "int_" : "t0";
|
|
sb.append(fkTableAlias).append(".").append(exportedProperties[i].getForeignDbColumn());
|
|
}
|
|
sb.append(", ").append(fetchOrderBy);
|
|
lazyFetchOrderBy = sb.toString().trim();
|
|
}
|
|
}
|
|
|
|
String delStmt;
|
|
if (manyToMany) {
|
|
delStmt = "delete from " + inverseJoin.getTable() + " where ";
|
|
} else {
|
|
delStmt = "delete from " + targetDescriptor.getBaseTable() + " where ";
|
|
}
|
|
deleteByParentIdSql = delStmt + deriveWhereParentIdSql(false, "");
|
|
deleteByParentIdInSql = delStmt + deriveWhereParentIdSql(true, "");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise after the target bean descriptors have been all set.
|
|
*/
|
|
public void initialisePostTarget() {
|
|
if (childMasterProperty != null) {
|
|
BeanProperty masterId = childMasterProperty.getTargetDescriptor().getIdProperty();
|
|
childMasterIdProperty = childMasterProperty.getName() + "." + masterId.getName();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void docStoreIncludeByDefault(PathProperties pathProps) {
|
|
// by default not including "Many" properties in document store
|
|
}
|
|
|
|
/**
|
|
* Copy collection value if existing is empty.
|
|
*/
|
|
@Override
|
|
public void merge(EntityBean bean, EntityBean existing) {
|
|
|
|
Object existingCollection = getVal(existing);
|
|
if (existingCollection instanceof BeanCollection<?>) {
|
|
BeanCollection<?> toBC = (BeanCollection<?>)existingCollection;
|
|
if (!toBC.isPopulated()) {
|
|
Object fromCollection = getVal(bean);
|
|
if (fromCollection instanceof BeanCollection<?>) {
|
|
BeanCollection<?> fromBC = (BeanCollection<?>)fromCollection;
|
|
if (fromBC.isPopulated()) {
|
|
toBC.loadFrom(fromBC);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the bean to the appropriate collection on the parent bean.
|
|
*/
|
|
public void addBeanToCollectionWithCreate(EntityBean parentBean, EntityBean detailBean, boolean withCheck) {
|
|
BeanCollection<?> bc = (BeanCollection<?>) super.getValue(parentBean);
|
|
if (bc == null) {
|
|
bc = help.createEmpty(parentBean);
|
|
setValue(parentBean, bc);
|
|
}
|
|
help.add(bc, detailBean, withCheck);
|
|
}
|
|
|
|
/**
|
|
* Return true if this is considered 'empty' from a save perspective.
|
|
*/
|
|
public boolean isSkipSaveBeanCollection(EntityBean bean, boolean insertedParent) {
|
|
Object val = getValue(bean);
|
|
if (val == null) {
|
|
return true;
|
|
}
|
|
if ((val instanceof BeanCollection<?>)) {
|
|
return ((BeanCollection<?>) val).isSkipSave();
|
|
}
|
|
if (insertedParent) {
|
|
// check 'vanilla' collection types
|
|
if (val instanceof Collection<?>){
|
|
return ((Collection<?>) val).isEmpty();
|
|
}
|
|
if (val instanceof Map<?,?>) {
|
|
return ((Map<?,?>) val).isEmpty();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Reset the many properties to be empty and ready for reloading.
|
|
* <p>
|
|
* Used in bean refresh.
|
|
*/
|
|
public void resetMany(EntityBean bean) {
|
|
Object value = getValue(bean);
|
|
if (value instanceof BeanCollection) {
|
|
// reset the collection back to empty
|
|
((BeanCollection)value).reset(bean, name);
|
|
} else {
|
|
createReference(bean);
|
|
}
|
|
}
|
|
|
|
public ElPropertyValue buildElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) {
|
|
return createElPropertyValue(propName, remainder, chain, propertyDeploy);
|
|
}
|
|
|
|
@Override
|
|
public void buildRawSqlSelectChain(String prefix, List<String> selectChain) {
|
|
// do not add to the selectChain at the top level of the Many bean
|
|
}
|
|
|
|
public SqlUpdate deleteByParentId(Object parentId, List<Object> parentIdist) {
|
|
if (parentId != null) {
|
|
return deleteByParentId(parentId);
|
|
} else {
|
|
return deleteByParentIdList(parentIdist);
|
|
}
|
|
}
|
|
|
|
private SqlUpdate deleteByParentId(Object parentId) {
|
|
DefaultSqlUpdate sqlDelete = new DefaultSqlUpdate(deleteByParentIdSql);
|
|
bindWhereParendId(sqlDelete, parentId);
|
|
return sqlDelete;
|
|
}
|
|
|
|
/**
|
|
* Find the Id's of detail beans given a parent Id or list of parent Id's.
|
|
*/
|
|
public List<Object> findIdsByParentId(Object parentId, List<Object> parentIdList, Transaction t, ArrayList<Object> excludeDetailIds) {
|
|
if (parentId != null) {
|
|
return findIdsByParentId(parentId, t, excludeDetailIds);
|
|
} else {
|
|
return findIdsByParentIdList(parentIdList, t, excludeDetailIds);
|
|
}
|
|
}
|
|
|
|
private List<Object> findIdsByParentId(Object parentId, Transaction t, ArrayList<Object> excludeDetailIds) {
|
|
|
|
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();
|
|
|
|
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
|
|
Expression idIn = q.getExpressionFactory().idIn(excludeDetailIds);
|
|
q.where().not(idIn);
|
|
}
|
|
|
|
return server.findIds(q, t);
|
|
}
|
|
|
|
/**
|
|
* Exclude many properties from bean cache data.
|
|
*/
|
|
@Override
|
|
public boolean isCacheDataInclude() {
|
|
// this would change for DB Array type support
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add the loaded current bean to its associated parent.
|
|
*/
|
|
public void lazyLoadMany(EntityBean current) {
|
|
EntityBean parentBean = childMasterProperty.getValueAsEntityBean(current);
|
|
if (parentBean != null) {
|
|
addBeanToCollectionWithCreate(parentBean, current, true);
|
|
}
|
|
}
|
|
|
|
public void addWhereParentIdIn(SpiQuery<?> query, List<Object> parentIds, boolean useDocStore) {
|
|
if (useDocStore) {
|
|
// assumes the ManyToOne property is included
|
|
query.where().in(childMasterIdProperty, parentIds);
|
|
} else {
|
|
addWhereParentIdIn(query, parentIds);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a where clause to the query for a given list of parent Id's.
|
|
*/
|
|
private void addWhereParentIdIn(SpiQuery<?> query, List<Object> parentIds) {
|
|
|
|
String tableAlias = manyToMany ? "int_." : "t0.";
|
|
if (manyToMany) {
|
|
query.setM2MIncludeJoin(inverseJoin);
|
|
}
|
|
String rawWhere = deriveWhereParentIdSql(true, tableAlias);
|
|
String expr = descriptor.getParentIdInExpr(parentIds.size(), rawWhere);
|
|
|
|
// Flatten the bind values if needed (embeddedId)
|
|
List<Object> bindValues = getBindParentIds(parentIds);
|
|
|
|
query.where().raw(expr, bindValues.toArray());
|
|
}
|
|
|
|
private List<Object> findIdsByParentIdList(List<Object> parentIdList, Transaction t, ArrayList<Object> excludeDetailIds) {
|
|
|
|
String rawWhere = deriveWhereParentIdSql(true, "");
|
|
String inClause = buildInClauseBinding(parentIdList.size(), exportedPropertyBindProto);
|
|
|
|
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();
|
|
|
|
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
|
|
Expression idIn = q.getExpressionFactory().idIn(excludeDetailIds);
|
|
q.where().not(idIn);
|
|
}
|
|
|
|
return server.findIds(q, t);
|
|
}
|
|
|
|
private SqlUpdate deleteByParentIdList(List<Object> parentIdist) {
|
|
|
|
StringBuilder sb = new StringBuilder(100);
|
|
sb.append(deleteByParentIdInSql);
|
|
|
|
String inClause = buildInClauseBinding(parentIdist.size(), exportedPropertyBindProto);
|
|
sb.append(inClause);
|
|
|
|
DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString());
|
|
for (Object aParentIdist : parentIdist) {
|
|
bindWhereParendId(delete, aParentIdist);
|
|
}
|
|
|
|
return delete;
|
|
}
|
|
|
|
private String deriveExportedPropertyBindProto() {
|
|
if (exportedProperties.length == 1) {
|
|
return "?";
|
|
}
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("(");
|
|
for (int i = 0; i < exportedProperties.length; i++) {
|
|
if (i > 0) {
|
|
sb.append(",");
|
|
}
|
|
sb.append("?");
|
|
}
|
|
sb.append(")");
|
|
return sb.toString();
|
|
}
|
|
|
|
private String buildInClauseBinding(int size, String bindProto) {
|
|
|
|
StringBuilder sb = new StringBuilder(10 + (size * (bindProto.length() + 1)));
|
|
sb.append(" in");
|
|
|
|
sb.append(" (");
|
|
for (int i = 0; i < size; i++) {
|
|
if (i > 0) {
|
|
sb.append(",");
|
|
}
|
|
sb.append(bindProto);
|
|
}
|
|
sb.append(") ");
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Set the lazy load server to help create reference collections (that lazy
|
|
* load on demand).
|
|
*/
|
|
public void setLoader(BeanCollectionLoader loader) {
|
|
if (help != null) {
|
|
help.setLoader(loader);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the mode for listening to modifications to collections for this
|
|
* association.
|
|
*/
|
|
public ModifyListenMode getModifyListenMode() {
|
|
return modifyListenMode;
|
|
}
|
|
|
|
@Override
|
|
public void appendSelect(DbSqlContext ctx, boolean subQuery) {
|
|
}
|
|
|
|
@Override
|
|
public void loadIgnore(DbReadContext ctx) {
|
|
// nothing to ignore for Many
|
|
}
|
|
|
|
@Override
|
|
public void load(SqlBeanLoad sqlBeanLoad) {
|
|
// do nothing, as a lazy loading BeanCollection 'reference'
|
|
// is created and registered with the loading context
|
|
// in SqlTreeNodeBean.createListProxies()
|
|
}
|
|
|
|
@Override
|
|
public Object readSet(DbReadContext ctx, EntityBean bean) throws SQLException {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public Object read(DbReadContext ctx) throws SQLException {
|
|
return null;
|
|
}
|
|
|
|
public void add(BeanCollection<?> collection, EntityBean bean) {
|
|
help.add(collection, bean, false);
|
|
}
|
|
|
|
@Override
|
|
public String getAssocIsEmpty(SpiExpressionRequest request, String path) {
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
SpiQuery<?> query = request.getQueryRequest().getQuery();
|
|
if (manyToMany) {
|
|
sb.append(query.isAsDraft() ? intersectionDraftTable : intersectionPublishTable);
|
|
} else {
|
|
sb.append(targetDescriptor.getBaseTable(query.getTemporalMode()));
|
|
}
|
|
sb.append(" where ");
|
|
for (int i = 0; i < exportedProperties.length; i++) {
|
|
if (i > 0) {
|
|
sb.append(" and ");
|
|
}
|
|
exportedProperties[i].appendWhere(sb, path);
|
|
}
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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 isMany() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean isAssocMany() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean isAssocId() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean isAssocProperty() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns true.
|
|
*/
|
|
@Override
|
|
public boolean containsMany() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return the many type.
|
|
*/
|
|
public ManyType getManyType() {
|
|
return manyType;
|
|
}
|
|
|
|
/**
|
|
* Return true if this is many to many.
|
|
*/
|
|
public boolean isManyToMany() {
|
|
return manyToMany;
|
|
}
|
|
|
|
/**
|
|
* ManyToMany only, join from local table to intersection table.
|
|
*/
|
|
public TableJoin getIntersectionTableJoin() {
|
|
return intersectionJoin;
|
|
}
|
|
|
|
/**
|
|
* Set the join properties from the parent bean to the child bean.
|
|
* This is only valid for OneToMany and NOT valid for ManyToMany.
|
|
*/
|
|
public void setJoinValuesToChild(EntityBean parent, EntityBean child, Object mapKeyValue) {
|
|
|
|
if (mapKeyProperty != null) {
|
|
mapKeyProperty.setValue(child, mapKeyValue);
|
|
}
|
|
|
|
if (!manyToMany && childMasterProperty != null) {
|
|
// bidirectional in the sense that the 'master' property
|
|
// exists on the 'detail' bean
|
|
childMasterProperty.setValue(child, parent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the order by clause used to order the fetching of the data for
|
|
* this list, set or map.
|
|
*/
|
|
public String getFetchOrderBy() {
|
|
return fetchOrderBy;
|
|
}
|
|
|
|
/**
|
|
* Return the order by for use when lazy loading the associated collection.
|
|
*/
|
|
public String getLazyFetchOrderBy() {
|
|
return lazyFetchOrderBy;
|
|
}
|
|
|
|
/**
|
|
* Return the default mapKey when returning a Map.
|
|
*/
|
|
public String getMapKey() {
|
|
return mapKey;
|
|
}
|
|
|
|
public BeanCollection<?> createReferenceIfNull(EntityBean parentBean) {
|
|
|
|
Object v = getValue(parentBean);
|
|
if (v instanceof BeanCollection<?>) {
|
|
BeanCollection<?> bc = (BeanCollection<?>) v;
|
|
return bc.isReference() ? bc : null;
|
|
} else if (v != null) {
|
|
return null;
|
|
} else {
|
|
return createReference(parentBean);
|
|
}
|
|
}
|
|
|
|
public BeanCollection<?> createReference(EntityBean parentBean) {
|
|
|
|
BeanCollection<?> ref = help.createReference(parentBean);
|
|
setValue(parentBean, ref);
|
|
return ref;
|
|
}
|
|
|
|
public BeanCollection<T> createEmpty(EntityBean parentBean) {
|
|
return help.createEmpty(parentBean);
|
|
}
|
|
|
|
public BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey) {
|
|
return help.getBeanCollectionAdd(bc, mapKey);
|
|
}
|
|
|
|
public Object getParentId(EntityBean parentBean) {
|
|
return descriptor.getId(parentBean);
|
|
}
|
|
|
|
public List<Object> getBindParentIds(List<Object> parentIds) {
|
|
if (exportedProperties.length == 1) {
|
|
return parentIds;
|
|
}
|
|
List<Object> expandedList = new ArrayList<>(parentIds.size() * exportedProperties.length);
|
|
for (Object parentId : parentIds) {
|
|
for (ExportedProperty exportedProperty : exportedProperties) {
|
|
Object compId = parentId;
|
|
expandedList.add(exportedProperty.getValue((EntityBean) compId));
|
|
}
|
|
}
|
|
return expandedList;
|
|
}
|
|
|
|
private void bindWhereParendId(DefaultSqlUpdate sqlUpd, Object parentId) {
|
|
|
|
if (exportedProperties.length == 1) {
|
|
sqlUpd.addParameter(parentId);
|
|
return;
|
|
}
|
|
EntityBean parent = (EntityBean) parentId;
|
|
for (ExportedProperty exportedProperty : exportedProperties) {
|
|
Object embVal = exportedProperty.getValue(parent);
|
|
sqlUpd.addParameter(embVal);
|
|
}
|
|
}
|
|
|
|
public void addSelectExported(DbSqlContext ctx, String tableAlias) {
|
|
|
|
String alias = manyToMany ? "int_" : tableAlias;
|
|
if (alias == null) {
|
|
alias = "t0";
|
|
}
|
|
for (ExportedProperty exportedProperty : exportedProperties) {
|
|
ctx.appendColumn(alias, exportedProperty.getForeignDbColumn());
|
|
}
|
|
}
|
|
|
|
private String deriveWhereParentIdSql(boolean inClause, String tableAlias) {
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
if (inClause) {
|
|
sb.append("(");
|
|
}
|
|
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(tableAlias).append(fkColumn);
|
|
if (!inClause) {
|
|
sb.append("=? ");
|
|
}
|
|
}
|
|
if (inClause) {
|
|
sb.append(")");
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
// public void setPredicates(SpiQuery<?> query, EntityBean parentBean) {
|
|
//
|
|
// if (manyToMany){
|
|
// // for ManyToMany lazy loading we need to include a
|
|
// // join to the intersection table. The predicate column
|
|
// // is not on the 'destination many table'.
|
|
// query.setIncludeTableJoin(inverseJoin);
|
|
// }
|
|
//
|
|
// if (embeddedExportedProperties) {
|
|
// // use the EmbeddedId object instead of the parentBean
|
|
// BeanProperty idProp = descriptor.getIdProperty();
|
|
// parentBean = (EntityBean)idProp.getValue(parentBean);
|
|
// }
|
|
//
|
|
// for (int i = 0; i < exportedProperties.length; i++) {
|
|
// Object val = exportedProperties[i].getValue(parentBean);
|
|
// String fkColumn = exportedProperties[i].getForeignDbColumn();
|
|
// if (!manyToMany){
|
|
// fkColumn = targetDescriptor.getBaseTableAlias()+"."+fkColumn;
|
|
// } else {
|
|
// // use hard coded alias for intersection table
|
|
// fkColumn = "int_."+fkColumn;
|
|
// }
|
|
// query.where().eq(fkColumn, val);
|
|
// }
|
|
//
|
|
// if (extraWhere != null){
|
|
// // replace the table alias place holder
|
|
// String ta = targetDescriptor.getBaseTableAlias();
|
|
// String where = StringHelper.replaceString(extraWhere, "${ta}", ta);
|
|
// query.where().raw(where);
|
|
// }
|
|
//
|
|
// if (fetchOrderBy != null){
|
|
// query.order(fetchOrderBy);
|
|
// }
|
|
// }
|
|
|
|
/**
|
|
* 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
|
|
logger.error("Could not find a exported property?", e);
|
|
}
|
|
|
|
} 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 embedded, BeanProperty prop) {
|
|
|
|
String matchColumn = prop.getDbColumn();
|
|
|
|
String searchTable;
|
|
TableJoinColumn[] columns;
|
|
if (manyToMany) {
|
|
// look for column going to intersection
|
|
columns = intersectionJoin.columns();
|
|
searchTable = intersectionJoin.getTable();
|
|
|
|
} else {
|
|
columns = tableJoin.columns();
|
|
searchTable = tableJoin.getTable();
|
|
}
|
|
for (TableJoinColumn column : columns) {
|
|
String matchTo = column.getLocalDbColumn();
|
|
|
|
if (matchColumn.equalsIgnoreCase(matchTo)) {
|
|
String foreignCol = column.getForeignDbColumn();
|
|
return new ExportedProperty(embedded, 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);
|
|
}
|
|
|
|
/**
|
|
* Return the child property that links back to the master bean.
|
|
* <p>
|
|
* Note that childMasterProperty will be null if a field is used instead of
|
|
* a ManyToOne bean association.
|
|
* </p>
|
|
*/
|
|
private BeanPropertyAssocOne<?> initChildMasterProperty() {
|
|
|
|
if (unidirectional) {
|
|
return null;
|
|
}
|
|
|
|
// search for the property, to see if it exists
|
|
Class<?> beanType = descriptor.getBeanType();
|
|
BeanDescriptor<?> targetDesc = getTargetDescriptor();
|
|
|
|
BeanPropertyAssocOne<?>[] ones = targetDesc.propertiesOne();
|
|
for (BeanPropertyAssocOne<?> prop : ones) {
|
|
if (mappedBy != null) {
|
|
// match using mappedBy as property name
|
|
if (mappedBy.equalsIgnoreCase(prop.getName())) {
|
|
return prop;
|
|
}
|
|
} else {
|
|
// assume only one property that matches parent object type
|
|
if (prop.getTargetType().equals(beanType)) {
|
|
// found it, stop search
|
|
return prop;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new RuntimeException("Can not find Master [" + beanType + "] in Child[" + targetDesc + "]");
|
|
}
|
|
|
|
/**
|
|
* Search for and return the mapKey property.
|
|
*/
|
|
private BeanProperty initMapKeyProperty() {
|
|
|
|
// search for the property
|
|
BeanDescriptor<?> targetDesc = getTargetDescriptor();
|
|
for (BeanProperty prop : targetDesc.propertiesAll()) {
|
|
if (mapKey.equalsIgnoreCase(prop.getName())) {
|
|
return prop;
|
|
}
|
|
}
|
|
|
|
String from = descriptor.getFullName();
|
|
String to = targetDesc.getFullName();
|
|
throw new PersistenceException(from + ": Could not find mapKey property [" + mapKey + "] on [" + to + "]");
|
|
}
|
|
|
|
public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, ArrayList<Object> excludeDetailIds) {
|
|
|
|
IntersectionRow row = new IntersectionRow(tableJoin.getTable(), targetDescriptor);
|
|
if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) {
|
|
row.setExcludeIds(excludeDetailIds, getTargetDescriptor());
|
|
}
|
|
buildExport(row, parentBean);
|
|
return row;
|
|
}
|
|
|
|
public IntersectionRow buildManyToManyDeleteChildren(EntityBean parentBean, boolean publish) {
|
|
|
|
String tableName = publish ? intersectionPublishTable : intersectionDraftTable;
|
|
IntersectionRow row = new IntersectionRow(tableName);
|
|
buildExport(row, parentBean);
|
|
return row;
|
|
}
|
|
|
|
public IntersectionRow buildManyToManyMapBean(EntityBean parent, EntityBean other, boolean publish) {
|
|
|
|
String tableName = publish ? intersectionPublishTable : intersectionDraftTable;
|
|
IntersectionRow row = new IntersectionRow(tableName);
|
|
buildExport(row, parent);
|
|
buildImport(row, other);
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* Register the mapping of intersection table to associated draft table.
|
|
*/
|
|
public void registerDraftIntersectionTable(Map<String, String> draftTableMap) {
|
|
if (hasDraftIntersection()) {
|
|
draftTableMap.put(intersectionPublishTable, intersectionDraftTable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if the relationship is a ManyToMany with the intersection having an associated draft table.
|
|
*/
|
|
private boolean hasDraftIntersection() {
|
|
return intersectionDraftTable != null && !intersectionDraftTable.equals(intersectionPublishTable);
|
|
}
|
|
|
|
private void buildExport(IntersectionRow row, EntityBean parentBean) {
|
|
|
|
if (embeddedExportedProperties) {
|
|
BeanProperty idProp = descriptor.getIdProperty();
|
|
parentBean = (EntityBean) idProp.getValue(parentBean);
|
|
}
|
|
for (ExportedProperty exportedProperty : exportedProperties) {
|
|
Object val = exportedProperty.getValue(parentBean);
|
|
String fkColumn = exportedProperty.getForeignDbColumn();
|
|
|
|
row.put(fkColumn, val);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the predicates for lazy loading of the association.
|
|
* Handles predicates for both OneToMany and ManyToMany.
|
|
*/
|
|
private void buildImport(IntersectionRow row, EntityBean otherBean) {
|
|
|
|
importedId.buildImport(row, otherBean);
|
|
}
|
|
|
|
/**
|
|
* Return true if the otherBean has an Id value.
|
|
*/
|
|
public boolean hasImportedId(EntityBean otherBean) {
|
|
|
|
return null != targetDescriptor.getId(otherBean);
|
|
}
|
|
|
|
public void jsonWrite(WriteJson ctx, EntityBean bean) throws IOException {
|
|
if (!this.jsonSerialize) {
|
|
return;
|
|
}
|
|
Boolean include = ctx.includeMany(name);
|
|
if (Boolean.FALSE.equals(include)) {
|
|
return;
|
|
}
|
|
|
|
Object value = getValueIntercept(bean);
|
|
if (value != null) {
|
|
ctx.pushParentBeanMany(bean);
|
|
if (help != null) {
|
|
help.jsonWrite(ctx, name, value, include != null);
|
|
} else {
|
|
if (isTransient && targetDescriptor == null) {
|
|
ctx.writeValueUsingObjectMapper(name, value);
|
|
} else {
|
|
Collection<?> collection = (Collection<?>)value;
|
|
if (!collection.isEmpty() || ctx.isIncludeEmpty()) {
|
|
ctx.toJson(name, collection);
|
|
}
|
|
}
|
|
}
|
|
ctx.popParentBeanMany();
|
|
}
|
|
}
|
|
|
|
public void jsonRead(ReadJson readJson, EntityBean parentBean) throws IOException {
|
|
jsonHelp.jsonRead(readJson, parentBean);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public void publishMany(EntityBean draft, EntityBean live) {
|
|
|
|
// collections will not be null due to enhancement
|
|
BeanCollection<T> draftVal = (BeanCollection<T>)getValueIntercept(draft);
|
|
BeanCollection<T> liveVal = (BeanCollection<T>)getValueIntercept(live);
|
|
|
|
// Organise the existing live beans into map keyed by id
|
|
Map<Object, T> liveBeansAsMap = liveBeansAsMap(liveVal);
|
|
|
|
// publish from each draft to live bean creating new live beans as required
|
|
draftVal.size();
|
|
Collection<T> actualDetails = draftVal.getActualDetails();
|
|
for (T bean : actualDetails) {
|
|
Object id = targetDescriptor.getId((EntityBean) bean);
|
|
T liveBean = liveBeansAsMap.remove(id);
|
|
|
|
if (isManyToMany()) {
|
|
if (liveBean == null) {
|
|
// add new relationship (Map not allowed here)
|
|
liveVal.addBean(targetDescriptor.createReference(Boolean.FALSE, false, id, null));
|
|
}
|
|
|
|
} else {
|
|
// recursively publish the OneToMany child bean
|
|
T newLive = targetDescriptor.publish(bean, liveBean);
|
|
if (liveBean == null) {
|
|
// Map not allowed here
|
|
liveVal.addBean(newLive);
|
|
}
|
|
}
|
|
}
|
|
|
|
// anything remaining should be deleted (so remove from modify aware collection)
|
|
Collection<T> values = liveBeansAsMap.values();
|
|
for (T value : values) {
|
|
liveVal.removeBean(value);
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private Map<Object,T> liveBeansAsMap(BeanCollection<?> liveVal) {
|
|
|
|
liveVal.size();
|
|
Collection<?> liveBeans = liveVal.getActualDetails();
|
|
Map<Object,T> liveMap = new LinkedHashMap<>();
|
|
|
|
for (Object liveBean : liveBeans) {
|
|
Object id = targetDescriptor.getId((EntityBean) liveBean);
|
|
liveMap.put(id, (T)liveBean);
|
|
}
|
|
return liveMap;
|
|
}
|
|
|
|
}
|