package io.ebeaninternal.server.deploy;

import io.ebean.Query;
import io.ebean.bean.EntityBean;
import io.ebean.core.type.DocPropertyType;
import io.ebean.text.PathProperties;
import io.ebean.util.SplitName;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.core.DefaultSqlUpdate;
import io.ebeaninternal.server.core.InternString;
import io.ebeaninternal.server.deploy.id.IdBinder;
import io.ebeaninternal.server.deploy.id.ImportedId;
import io.ebeaninternal.server.deploy.id.ImportedIdEmbedded;
import io.ebeaninternal.server.deploy.id.ImportedIdSimple;
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssoc;
import io.ebeaninternal.server.el.ElPropertyChainBuilder;
import io.ebeaninternal.server.el.ElPropertyValue;
import io.ebeaninternal.server.persist.MultiValueWrapper;
import io.ebeaninternal.server.query.STreePropertyAssoc;
import io.ebeaninternal.server.query.STreeType;
import io.ebeaninternal.server.query.SqlJoinType;
import io.ebeaninternal.server.querydefn.DefaultOrmQuery;
import io.ebeanservice.docstore.api.mapping.DocMappingBuilder;
import io.ebeanservice.docstore.api.mapping.DocPropertyMapping;
import io.ebeanservice.docstore.api.support.DocStructure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.List;

Abstract base for properties mapped to an associated bean, list, set or map.
/** * Abstract base for properties mapped to an associated bean, list, set or map. */
public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STreePropertyAssoc { private static final Logger logger = LoggerFactory.getLogger(BeanPropertyAssoc.class);
The descriptor of the target. This MUST be initialised after construction so as to avoid a dependency loop between BeanDescriptors.
/** * The descriptor of the target. This MUST be initialised after construction * so as to avoid a dependency loop between BeanDescriptors. */
BeanDescriptor<T> targetDescriptor; IdBinder targetIdBinder; InheritInfo targetInheritInfo; String targetIdProperty;
Derived list of exported property and matching foreignKey
/** * Derived list of exported property and matching foreignKey */
ExportedProperty[] exportedProperties;
Persist settings.
/** * Persist settings. */
final BeanCascadeInfo cascadeInfo;
Join between the beans.
/** * Join between the beans. */
final TableJoin tableJoin; final PropertyForeignKey foreignKey;
The type of the joined bean.
/** * The type of the joined bean. */
private final Class<T> targetType;
The join table information.
/** * The join table information. */
final BeanTable beanTable; final String mappedBy; private final String docStoreDoc; private final String extraWhere; private final int fetchPreference; private boolean saveRecurseSkippable;
Construct the property.
/** * Construct the property. */
BeanPropertyAssoc(BeanDescriptor<?> descriptor, DeployBeanPropertyAssoc<T> deploy) { super(descriptor, deploy); this.foreignKey = deploy.getForeignKey(); this.extraWhere = InternString.intern(deploy.getExtraWhere()); this.beanTable = deploy.getBeanTable(); this.mappedBy = InternString.intern(deploy.getMappedBy()); this.docStoreDoc = deploy.getDocStoreDoc(); this.tableJoin = new TableJoin(deploy.getTableJoin()); this.targetType = deploy.getTargetType(); this.cascadeInfo = deploy.getCascadeInfo(); this.fetchPreference = deploy.getFetchPreference(); }
Copy constructor for ManyToOne inside Embeddable.
/** * Copy constructor for ManyToOne inside Embeddable. */
@SuppressWarnings("unchecked") BeanPropertyAssoc(BeanPropertyAssoc source, BeanPropertyOverride override) { super(source, override); foreignKey = source.foreignKey; extraWhere = source.extraWhere; beanTable = source.beanTable; mappedBy = source.mappedBy; docStoreDoc = source.docStoreDoc; targetType = source.targetType; cascadeInfo = source.cascadeInfo; fetchPreference = source.fetchPreference; tableJoin = source.tableJoin.withOverrideColumn(override.getDbColumn()); }
Initialise post construction.
/** * Initialise post construction. */
@Override public void initialise(BeanDescriptorInitContext initContext) { // this *MUST* execute after the BeanDescriptor is // put into the map to stop infinite recursion initialiseTargetDescriptor(initContext); } void initialiseTargetDescriptor(BeanDescriptorInitContext initContext) { targetDescriptor = descriptor.getBeanDescriptor(targetType); if (!isTransient) { targetIdBinder = targetDescriptor.getIdBinder(); targetInheritInfo = targetDescriptor.getInheritInfo(); saveRecurseSkippable = targetDescriptor.isSaveRecurseSkippable(); if (!targetIdBinder.isComplexId()) { targetIdProperty = targetIdBinder.getIdProperty(); } } } @Override public int getFetchPreference() { return fetchPreference; }
Return the extra configuration for the foreign key.
/** * Return the extra configuration for the foreign key. */
public PropertyForeignKey getForeignKey() { return foreignKey; }
Return true if foreign key constraint is enabled on this relationship (not disabled).
/** * Return true if foreign key constraint is enabled on this relationship (not disabled). */
public boolean hasForeignKeyConstraint() { return foreignKey == null || !foreignKey.isNoConstraint(); }
Return true if foreign key index is enabled on this relationship (not disabled).
/** * Return true if foreign key index is enabled on this relationship (not disabled). */
public boolean hasForeignKeyIndex() { return foreignKey == null || !foreignKey.isNoIndex(); }
Create a ElPropertyValue for a *ToOne or *ToMany.
/** * Create a ElPropertyValue for a *ToOne or *ToMany. */
ElPropertyValue createElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) { // associated or embedded bean BeanDescriptor<?> embDesc = getTargetDescriptor(); if (chain == null) { chain = new ElPropertyChainBuilder(isEmbedded(), propName); } chain.add(this); if (containsMany()) { chain.setContainsMany(); } return embDesc.buildElGetValue(remainder, chain, propertyDeploy); }
Add table join with table alias based on prefix.
/** * Add table join with table alias based on prefix. */
@Override public SqlJoinType addJoin(SqlJoinType joinType, String prefix, DbSqlContext ctx) { return tableJoin.addJoin(joinType, prefix, ctx); }
Add table join with explicit table alias.
/** * Add table join with explicit table alias. */
@Override public SqlJoinType addJoin(SqlJoinType joinType, String a1, String a2, DbSqlContext ctx) { return tableJoin.addJoin(joinType, a1, a2, ctx); }
Return false.
/** * Return false. */
@Override public boolean isScalar() { return false; }
Return the mappedBy property. This will be null on the owning side.
/** * Return the mappedBy property. * This will be null on the owning side. */
public String getMappedBy() { return mappedBy; }
Return the Id property of the target entity type.

This will return null for multiple Id properties.

/** * Return the Id property of the target entity type. * <p> * This will return null for multiple Id properties. * </p> */
public String getTargetIdProperty() { return targetIdProperty; }
Return the BeanDescriptor of the target.
/** * Return the BeanDescriptor of the target. */
public BeanDescriptor<T> getTargetDescriptor() { return targetDescriptor; } SpiEbeanServer server() { return descriptor.getEbeanServer(); }
Create a new query for the target type.

We use target descriptor rather than target property type to support ElementCollection.

/** * Create a new query for the target type. * <p> * We use target descriptor rather than target property type to support ElementCollection. */
public SpiQuery<T> newQuery(SpiEbeanServer server) { return new DefaultOrmQuery<>(targetDescriptor, server, server.getExpressionFactory()); } @Override public IdBinder getIdBinder() { return descriptor.getIdBinder(); } @Override public STreeType target() { return targetDescriptor; }
Return true if the target side has soft delete.
/** * Return true if the target side has soft delete. */
public boolean isTargetSoftDelete() { return targetDescriptor.isSoftDelete(); }
Return true if REFRESH should cascade.
/** * Return true if REFRESH should cascade. */
boolean isCascadeRefresh() { return cascadeInfo.isRefresh(); } public boolean isSaveRecurseSkippable(Object bean) { return saveRecurseSkippable && bean instanceof EntityBean && !((EntityBean) bean)._ebean_getIntercept().isNewOrDirty(); }
Return true if save can be skipped for unmodified bean(s) of this property.

That is, if a bean of this property is unmodified we don't need to saveRecurse because none of its associated beans have cascade save set to true.

/** * Return true if save can be skipped for unmodified bean(s) of this * property. * <p> * That is, if a bean of this property is unmodified we don't need to * saveRecurse because none of its associated beans have cascade save set to * true. * </p> */
public boolean isSaveRecurseSkippable() { return saveRecurseSkippable; }
Return true if the unique id properties are all not null for this bean.
/** * Return true if the unique id properties are all not null for this bean. */
public boolean hasId(EntityBean bean) { BeanDescriptor<?> targetDesc = getTargetDescriptor(); BeanProperty idProp = targetDesc.getIdProperty(); // all the unique properties are non-null return idProp == null || idProp.getValue(bean) != null; }
Return the type of the target.

This is the class of the associated bean, or beans contained in a list, set or map.

/** * Return the type of the target. * <p> * This is the class of the associated bean, or beans contained in a list, * set or map. * </p> */
public Class<?> getTargetType() { return targetType; }
Return an extra clause to add to the query for loading or joining to this bean type.
/** * Return an extra clause to add to the query for loading or joining * to this bean type. */
@Override public String getExtraWhere() { return extraWhere; }
Return the elastic search doc for this embedded property.
/** * Return the elastic search doc for this embedded property. */
private String getDocStoreDoc() { return docStoreDoc; }
Determine if and how the associated bean is included in the doc store document.
/** * Determine if and how the associated bean is included in the doc store document. */
@Override public void docStoreInclude(boolean includeByDefault, DocStructure docStructure) { String embeddedDoc = getDocStoreDoc(); if (embeddedDoc == null) { // not annotated so use include by default // which is *ToOne included and *ToMany excluded if (includeByDefault) { docStoreIncludeByDefault(docStructure.doc()); } } else { // explicitly annotated to be included if (embeddedDoc.isEmpty()) { embeddedDoc = "*"; } // add in a nested way PathProperties embDoc = PathProperties.parse(embeddedDoc); docStructure.addNested(name, embDoc); } }
Include the property in the document store by default.
/** * Include the property in the document store by default. */
void docStoreIncludeByDefault(PathProperties pathProps) { pathProps.addToPath(null, name); } @Override public void docStoreMapping(DocMappingBuilder mapping, String prefix) { if (mapping.includesPath(prefix, name)) { String fullName = SplitName.add(prefix, name); DocPropertyType type = isMany() ? DocPropertyType.LIST : DocPropertyType.OBJECT; DocPropertyMapping nested = new DocPropertyMapping(name, type); mapping.push(nested); targetDescriptor.docStoreMapping(mapping, fullName); mapping.pop(); if (!nested.getChildren().isEmpty()) { mapping.add(nested); } } }
Return true if this association is updateable.
/** * Return true if this association is updateable. */
public boolean isUpdateable() { TableJoinColumn[] columns = tableJoin.columns(); if (columns.length <= 0) { return true; } for (TableJoinColumn column : columns) { if (column.isUpdateable()) { // at least 1 is updatable return true; } } return false; }
Return true if this association is insertable.
/** * Return true if this association is insertable. */
public boolean isInsertable() { TableJoinColumn[] columns = tableJoin.columns(); if (columns.length <= 0) { return true; } for (TableJoinColumn column : columns) { if (column.isInsertable()) { // at least 1 is insertable return true; } } return false; }
Return the underlying BeanTable for this property.
/** * Return the underlying BeanTable for this property. */
public BeanTable getBeanTable() { return beanTable; }
return the join to use for the bean.
/** * return the join to use for the bean. */
public TableJoin getTableJoin() { return tableJoin; }
Get the persist info.
/** * Get the persist info. */
public BeanCascadeInfo getCascadeInfo() { return cascadeInfo; }
Build the list of imported property. Matches BeanProperty from the target descriptor back to local database columns in the TableJoin.
/** * Build the list of imported property. Matches BeanProperty from the target * descriptor back to local database columns in the TableJoin. */
ImportedId createImportedId(BeanPropertyAssoc<?> owner, BeanDescriptor<?> target, TableJoin join) { BeanProperty idProp = target.getIdProperty(); BeanProperty[] others = target.propertiesBaseScalar(); if (descriptor.isRawSqlBased()) { String dbColumn = owner.getDbColumn(); return new ImportedIdSimple(owner, dbColumn, null, idProp, 0); } TableJoinColumn[] cols = join.columns(); if (idProp == null) { return null; } if (!idProp.isEmbedded()) { // simple single scalar id if (cols.length != 1) { String msg = "No Imported Id column for [" + idProp + "] in table [" + join.getTable() + "]"; logger.error(msg); return null; } else { BeanProperty[] idProps = {idProp}; return createImportedScalar(owner, cols[0], idProps, others); } } else { // embedded id BeanPropertyAssocOne<?> embProp = (BeanPropertyAssocOne<?>) idProp; BeanProperty[] embBaseProps = embProp.getTargetDescriptor().propertiesBaseScalar(); ImportedIdSimple[] scalars = createImportedList(owner, cols, embBaseProps, others); return new ImportedIdEmbedded(owner, embProp, scalars); } } private ImportedIdSimple[] createImportedList(BeanPropertyAssoc<?> owner, TableJoinColumn[] cols, BeanProperty[] props, BeanProperty[] others) { ArrayList<ImportedIdSimple> list = new ArrayList<>(cols.length); for (TableJoinColumn col : cols) { list.add(createImportedScalar(owner, col, props, others)); } return ImportedIdSimple.sort(list); } private ImportedIdSimple createImportedScalar(BeanPropertyAssoc<?> owner, TableJoinColumn col, BeanProperty[] props, BeanProperty[] others) { String matchColumn = col.getForeignDbColumn(); String localColumn = col.getLocalDbColumn(); String localSqlFormula = col.getLocalSqlFormula(); boolean insertable = col.isInsertable(); boolean updateable = col.isUpdateable(); for (int j = 0; j < props.length; j++) { if (props[j].getDbColumn().equalsIgnoreCase(matchColumn)) { return new ImportedIdSimple(owner, localColumn, localSqlFormula, props[j], j, insertable, updateable); } } for (int j = 0; j < others.length; j++) { if (others[j].getDbColumn().equalsIgnoreCase(matchColumn)) { return new ImportedIdSimple(owner, localColumn, localSqlFormula, others[j], j + props.length, insertable, updateable); } } String msg = "Error with the Join on [" + getFullBeanName() + "]. Could not find the local match for [" + matchColumn + "] "//in table["+searchTable+"]?" + " Perhaps an error in a @JoinColumn"; throw new PersistenceException(msg); } private List<Object> flattenParentIds(List<Object> parentIds) { List<Object> bindValues = new ArrayList<>(parentIds.size() * 3); for (Object parentId : parentIds) { flatten(bindValues, parentId); } return bindValues; } private List<Object> flattenParentId(Object parentId) { List<Object> bindValues = new ArrayList<>(); flatten(bindValues, parentId); return bindValues; } private void flatten(List<Object> bindValues, Object parentId) { if (isExportedSimple()) { bindValues.add(parentId); } else { EntityBean parent = (EntityBean) parentId; for (ExportedProperty exportedProperty : exportedProperties) { bindValues.add(exportedProperty.getValue(parent)); } } } void bindParentIds(DefaultSqlUpdate delete, List<Object> parentIds) { if (isExportedSimple()) { delete.setParameter(new MultiValueWrapper(parentIds)); } else { // embedded ids etc List<Object> bindValues = flattenParentIds(parentIds); for (Object bindValue : bindValues) { delete.setParameter(bindValue); } } } void bindParentId(DefaultSqlUpdate sqlUpd, Object parentId) { if (isExportedSimple()) { sqlUpd.setParameter(parentId); return; } EntityBean parent = (EntityBean) parentId; for (ExportedProperty exportedProperty : exportedProperties) { sqlUpd.setParameter(exportedProperty.getValue(parent)); } } void bindParentIdEq(String expr, Object parentId, Query<?> q) { if (isExportedSimple()) { q.where().raw(expr, parentId); } else { // embedded ids etc List<Object> bindValues = flattenParentId(parentId); q.where().raw(expr, bindValues.toArray()); } } void bindParentIdsIn(String expr, List<Object> parentIds, Query<?> q) { if (isExportedSimple()) { q.where().raw(expr, new MultiValueWrapper(parentIds)); } else { // embedded ids etc List<Object> bindValues = flattenParentIds(parentIds); q.where().raw(expr, bindValues.toArray()); } } private boolean isExportedSimple() { return exportedProperties.length == 1; }
Find and return the exported property matching to this property.
/** * Find and return the exported property matching to this property. */
ExportedProperty findMatch(boolean embedded, BeanProperty prop, String matchColumn, TableJoin tableJoin) { String searchTable = tableJoin.getTable(); for (TableJoinColumn column : tableJoin.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); } }