package io.ebeaninternal.server.deploy.parse;

import io.ebean.annotation.DbForeignKey;
import io.ebean.annotation.FetchPreference;
import io.ebean.annotation.HistoryExclude;
import io.ebean.annotation.Where;
import io.ebean.bean.BeanCollection.ModifyListenMode;
import io.ebean.config.BeanNotRegisteredException;
import io.ebean.config.NamingConvention;
import io.ebean.config.TableName;
import io.ebean.core.type.ScalarType;
import io.ebean.util.CamelCaseHelper;
import io.ebeaninternal.server.deploy.BeanDescriptorManager;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanTable;
import io.ebeaninternal.server.deploy.PropertyForeignKey;
import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
import io.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
import io.ebeaninternal.server.deploy.meta.DeployOrderColumn;
import io.ebeaninternal.server.deploy.meta.DeployTableJoin;
import io.ebeaninternal.server.deploy.meta.DeployTableJoinColumn;
import io.ebeaninternal.server.query.SqlJoinType;

import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.EnumType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.MapKey;
import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.OrderColumn;
import java.util.Set;

import static io.ebean.util.StringHelper.isNull;

Read the deployment annotation for Assoc Many beans.
/** * Read the deployment annotation for Assoc Many beans. */
class AnnotationAssocManys extends AnnotationParser { private final BeanDescriptorManager factory;
Create with the DeployInfo.
/** * Create with the DeployInfo. */
AnnotationAssocManys(DeployBeanInfo<?> info, ReadAnnotationConfig readConfig, BeanDescriptorManager factory) { super(info, readConfig); this.factory = factory; }
Parse the annotations.
/** * Parse the annotations. */
@Override public void parse() { for (DeployBeanProperty prop : descriptor.propertiesAll()) { if (prop instanceof DeployBeanPropertyAssocMany<?>) { read((DeployBeanPropertyAssocMany<?>) prop); } } } private boolean readOrphanRemoval(OneToMany property) { try { return property.orphanRemoval(); } catch (NoSuchMethodError e) { // Support old JPA API return false; } } private void read(DeployBeanPropertyAssocMany<?> prop) { OneToMany oneToMany = get(prop, OneToMany.class); if (oneToMany != null) { readToOne(oneToMany, prop); if (readOrphanRemoval(oneToMany)) { prop.setOrphanRemoval(); prop.setModifyListenMode(ModifyListenMode.REMOVALS); prop.getCascadeInfo().setDelete(true); } OrderColumn orderColumn = get(prop, OrderColumn.class); if (orderColumn != null) { // need to cascade as we set the order on cascade prop.setOrderColumn(new DeployOrderColumn(orderColumn)); prop.setFetchOrderBy(DeployOrderColumn.LOGICAL_NAME); prop.getCascadeInfo().setType(CascadeType.ALL); prop.setModifyListenMode(ModifyListenMode.ALL); } } ManyToMany manyToMany = get(prop, ManyToMany.class); if (manyToMany != null) { readToMany(manyToMany, prop); } ElementCollection elementCollection = get(prop, ElementCollection.class); if (elementCollection != null) { readElementCollection(prop, elementCollection); } // for ManyToMany typically to disable foreign keys from intersection table DbForeignKey dbForeignKey = get(prop, DbForeignKey.class); if (dbForeignKey != null){ prop.setForeignKey(new PropertyForeignKey(dbForeignKey)); } if (get(prop, HistoryExclude.class) != null) { prop.setExcludedFromHistory(); } OrderBy orderBy = get(prop, OrderBy.class); if (orderBy != null) { prop.setFetchOrderBy(orderBy.value()); } MapKey mapKey = get(prop, MapKey.class); if (mapKey != null) { prop.setMapKey(mapKey.name()); } Where where = prop.getMetaAnnotationWhere(platform); if (where != null) { prop.setExtraWhere(where.clause()); } FetchPreference fetchPreference = get(prop, FetchPreference.class); if (fetchPreference != null) { prop.setFetchPreference(fetchPreference.value()); } // check for manually defined joins BeanTable beanTable = prop.getBeanTable(); Set<JoinColumn> joinColumns = annotationJoinColumns(prop); if (!joinColumns.isEmpty()) { prop.getTableJoin().addJoinColumn(util, true, joinColumns, beanTable); } JoinTable joinTable = get(prop, JoinTable.class); if (joinTable != null) { if (prop.isManyToMany()) { // expected this readJoinTable(joinTable, prop); } else { // OneToMany with @JoinTable prop.setO2mJoinTable(); readJoinTable(joinTable, prop); manyToManyDefaultJoins(prop); } } if (prop.getMappedBy() != null) { // the join is derived by reversing the join information // from the mapped by property. // Refer BeanDescriptorManager.readEntityRelationships() return; } if (prop.isManyToMany()) { manyToManyDefaultJoins(prop); return; } if (!prop.getTableJoin().hasJoinColumns() && beanTable != null) { // use naming convention to define join (based on the bean name for this side of relationship) // A unidirectional OneToMany or OneToMany with no mappedBy property NamingConvention nc = factory.getNamingConvention(); String fkeyPrefix = null; if (nc.isUseForeignKeyPrefix()) { fkeyPrefix = nc.getColumnFromProperty(descriptor.getBeanType(), descriptor.getName()); } // Use the owning bean table to define the join BeanTable owningBeanTable = factory.getBeanTable(descriptor.getBeanType()); owningBeanTable.createJoinColumn(fkeyPrefix, prop.getTableJoin(), false, prop.getSqlFormulaSelect()); } } @SuppressWarnings("unchecked") private void readElementCollection(DeployBeanPropertyAssocMany<?> prop, ElementCollection elementCollection) { prop.setElementCollection(); if (!elementCollection.targetClass().equals(void.class)) { prop.setTargetType(elementCollection.targetClass()); } Column column = prop.getMetaAnnotation(Column.class); if (column != null) { prop.setDbColumn(column.name()); prop.setDbLength(column.length()); prop.setDbScale(column.scale()); } CollectionTable collectionTable = get(prop, CollectionTable.class); String fullTableName = getFullTableName(collectionTable); if (fullTableName == null) { fullTableName = descriptor.getBaseTable()+"_"+ CamelCaseHelper.toUnderscoreFromCamel(prop.getName()); } BeanTable localTable = factory.getBeanTable(descriptor.getBeanType()); if (collectionTable != null) { prop.getTableJoin().addJoinColumn(util, true, collectionTable.joinColumns(), localTable); } if (!prop.getTableJoin().hasJoinColumns()) { BeanProperty localId = localTable.getIdProperty(); if (localId != null) { // add foreign key based on convention String fkColName = namingConvention.getForeignKey(descriptor.getBaseTable(), localId.getName()); prop.getTableJoin().addJoinColumn(new DeployTableJoinColumn(localId.getDbColumn(), fkColName)); } } BeanTable beanTable = factory.createCollectionBeanTable(fullTableName, prop.getTargetType()); prop.setBeanTable(beanTable); Class<?> elementType = prop.getTargetType(); DeployBeanDescriptor<?> elementDescriptor = factory.createDeployDescriptor(elementType); elementDescriptor.setBaseTable(new TableName(fullTableName), readConfig.getAsOfViewSuffix(), readConfig.getVersionsBetweenSuffix()); int sortOrder = 0; if (!prop.getManyType().isMap()) { elementDescriptor.setProperties(new String[]{"value"}); } else { elementDescriptor.setProperties(new String[]{"key", "value"}); String dbKeyColumn = "mkey"; MapKeyColumn mapKeyColumn = get(prop, MapKeyColumn.class); if (mapKeyColumn != null) { dbKeyColumn = mapKeyColumn.name(); } ScalarType<?> keyScalarType = util.getTypeManager().getScalarType(prop.getMapKeyType()); DeployBeanProperty keyProp = new DeployBeanProperty(elementDescriptor, elementType, keyScalarType, null); setElementProperty(keyProp, "key", dbKeyColumn, sortOrder++); elementDescriptor.addBeanProperty(keyProp); if (mapKeyColumn != null) { keyProp.setDbLength(mapKeyColumn.length()); keyProp.setDbScale(mapKeyColumn.scale()); keyProp.setUnique(mapKeyColumn.unique()); } } ScalarType<?> valueScalarType = util.getTypeManager().getScalarType(elementType); if (valueScalarType == null && elementType.isEnum()) { Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>)elementType; valueScalarType = util.getTypeManager().createEnumScalarType(enumClass, EnumType.STRING); } boolean scalar = true; if (valueScalarType == null) { // embedded value type scalar = false; DeployBeanPropertyAssocOne valueProp = new DeployBeanPropertyAssocOne<>(elementDescriptor, elementType); valueProp.setName("value"); valueProp.setEmbedded(); valueProp.setElementProperty(); valueProp.setSortOrder(sortOrder++); elementDescriptor.addBeanProperty(valueProp); } else { // scalar value type DeployBeanProperty valueProp = new DeployBeanProperty(elementDescriptor, elementType, valueScalarType, null); setElementProperty(valueProp, "value", prop.getDbColumn(), sortOrder++); if (column != null) { valueProp.setDbLength(column.length()); valueProp.setDbScale(column.scale()); } Lob lob = get(prop, Lob.class); if (lob != null) { util.setLobType(valueProp); } elementDescriptor.addBeanProperty(valueProp); } elementDescriptor.setName(prop.getFullBeanName()); factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin()); prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType(), scalar)); } private void setElementProperty(DeployBeanProperty elementProp, String name, String dbColumn, int sortOrder) { if (dbColumn == null) { dbColumn = "value"; } elementProp.setName(name); elementProp.setDbColumn(dbColumn); elementProp.setNullable(false); elementProp.setDbInsertable(true); elementProp.setDbUpdateable(true); elementProp.setDbRead(true); elementProp.setSortOrder(sortOrder); elementProp.setElementProperty(); }
Define the joins for a ManyToMany relationship.

This includes joins to the intersection table and from the intersection table to the other side of the ManyToMany.

/** * Define the joins for a ManyToMany relationship. * <p> * This includes joins to the intersection table and from the intersection table * to the other side of the ManyToMany. * </p> */
private void readJoinTable(JoinTable joinTable, DeployBeanPropertyAssocMany<?> prop) { String intTableName = getFullTableName(joinTable); if (intTableName.isEmpty()) { BeanTable localTable = factory.getBeanTable(descriptor.getBeanType()); BeanTable otherTable = factory.getBeanTable(prop.getTargetType()); intTableName = getM2MJoinTableName(localTable, otherTable); } // set the intersection table DeployTableJoin intJoin = new DeployTableJoin(); intJoin.setTable(intTableName); // add the source to intersection join columns intJoin.addJoinColumn(util, true, joinTable.joinColumns(), prop.getBeanTable()); // set the intersection to dest table join columns DeployTableJoin destJoin = prop.getTableJoin(); destJoin.addJoinColumn(util, false, joinTable.inverseJoinColumns(), prop.getBeanTable()); intJoin.setType(SqlJoinType.OUTER); // reverse join from dest back to intersection DeployTableJoin inverseDest = destJoin.createInverse(intTableName); prop.setIntersectionJoin(intJoin); prop.setInverseJoin(inverseDest); }
Return the full table name
/** * Return the full table name */
private String getFullTableName(JoinTable joinTable) { return append(joinTable.catalog(), joinTable.schema(), joinTable.name()); } private String append(String catalog, String schema, String name) { StringBuilder sb = new StringBuilder(); if (!isNull(catalog)) { sb.append(catalog).append("."); } if (!isNull(schema)) { sb.append(schema).append("."); } sb.append(name); return sb.toString(); }
Return the full table name
/** * Return the full table name */
private String getFullTableName(CollectionTable collectionTable) { if (collectionTable == null || collectionTable.name().isEmpty()) { return null; } return append(collectionTable.catalog(), collectionTable.schema(), collectionTable.name()); }
Define intersection table and foreign key columns for ManyToMany.

Some of these (maybe all) have been already defined via @JoinTable and @JoinColumns etc.

/** * Define intersection table and foreign key columns for ManyToMany. * <p> * Some of these (maybe all) have been already defined via @JoinTable * and @JoinColumns etc. * </p> */
private void manyToManyDefaultJoins(DeployBeanPropertyAssocMany<?> prop) { String intTableName = null; DeployTableJoin intJoin = prop.getIntersectionJoin(); if (intJoin == null) { intJoin = new DeployTableJoin(); prop.setIntersectionJoin(intJoin); } else { // intersection table already defined (by @JoinTable) intTableName = intJoin.getTable(); } BeanTable localTable = factory.getBeanTable(descriptor.getBeanType()); BeanTable otherTable = factory.getBeanTable(prop.getTargetType()); final String localTableName = localTable.getUnqualifiedBaseTable(); final String otherTableName = otherTable.getUnqualifiedBaseTable(); if (intTableName == null) { // define intersection table name intTableName = getM2MJoinTableName(localTable, otherTable); intJoin.setTable(intTableName); intJoin.setType(SqlJoinType.OUTER); } DeployTableJoin destJoin = prop.getTableJoin(); if (intJoin.hasJoinColumns() && destJoin.hasJoinColumns()) { // already defined the foreign key columns etc return; } if (!intJoin.hasJoinColumns()) { // define foreign key columns BeanProperty localId = localTable.getIdProperty(); if (localId != null) { // add the source to intersection join columns String fkCol = localTableName + "_" + localId.getDbColumn(); intJoin.addJoinColumn(new DeployTableJoinColumn(localId.getDbColumn(), namingConvention.getColumnFromProperty(null, fkCol))); } } if (!destJoin.hasJoinColumns()) { // define inverse foreign key columns BeanProperty otherId = otherTable.getIdProperty(); if (otherId != null) { // set the intersection to dest table join columns final String fkCol = otherTableName + "_" + otherId.getDbColumn(); destJoin.addJoinColumn(new DeployTableJoinColumn(namingConvention.getColumnFromProperty(null, fkCol), otherId.getDbColumn())); } } // reverse join from dest back to intersection DeployTableJoin inverseDest = destJoin.createInverse(intTableName); prop.setInverseJoin(inverseDest); } private String errorMsgMissingBeanTable(Class<?> type, String from) { return "Error with association to [" + type + "] from [" + from + "]. Is " + type + " registered? See https://ebean.io/docs/trouble-shooting#not-registered"; } private void readToMany(ManyToMany propAnn, DeployBeanPropertyAssocMany<?> manyProp) { manyProp.setMappedBy(propAnn.mappedBy()); manyProp.setFetchType(propAnn.fetch()); setCascadeTypes(propAnn.cascade(), manyProp.getCascadeInfo()); setTargetType(propAnn.targetEntity(), manyProp); setBeanTable(manyProp); manyProp.setManyToMany(); manyProp.setModifyListenMode(ModifyListenMode.ALL); manyProp.getTableJoin().setType(SqlJoinType.OUTER); } private void readToOne(OneToMany propAnn, DeployBeanPropertyAssocMany<?> manyProp) { manyProp.setMappedBy(propAnn.mappedBy()); manyProp.setFetchType(propAnn.fetch()); setCascadeTypes(propAnn.cascade(), manyProp.getCascadeInfo()); setTargetType(propAnn.targetEntity(), manyProp); setBeanTable(manyProp); manyProp.getTableJoin().setType(SqlJoinType.OUTER); } private void setTargetType(Class<?> targetType, DeployBeanPropertyAssocMany<?> prop) { if (!targetType.equals(void.class)) { prop.setTargetType(targetType); } } private void setBeanTable(DeployBeanPropertyAssocMany<?> manyProp) { BeanTable assoc = factory.getBeanTable(manyProp.getTargetType()); if (assoc == null) { throw new BeanNotRegisteredException(errorMsgMissingBeanTable(manyProp.getTargetType(), manyProp.getFullBeanName())); } manyProp.setBeanTable(assoc); } private String getM2MJoinTableName(BeanTable lhsTable, BeanTable rhsTable) { TableName lhs = new TableName(lhsTable.getBaseTable()); TableName rhs = new TableName(rhsTable.getBaseTable()); TableName joinTable = namingConvention.getM2MJoinTableName(lhs, rhs); return joinTable.getQualifiedName(); } }