| /* |
| * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0, |
| * or the Eclipse Distribution License v. 1.0 which is available at |
| * http://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause |
| */ |
| |
| // Contributors: |
| // Oracle - initial API and implementation from Oracle TopLink |
| // 07/19/2011-2.2.1 Guy Pelletier |
| // - 338812: ManyToMany mapping in aggregate object violate integrity constraint on deletion |
| package org.eclipse.persistence.mappings; |
| |
| import java.util.*; |
| |
| import org.eclipse.persistence.descriptors.ClassDescriptor; |
| import org.eclipse.persistence.exceptions.*; |
| import org.eclipse.persistence.expressions.*; |
| import org.eclipse.persistence.history.*; |
| import org.eclipse.persistence.internal.expressions.*; |
| import org.eclipse.persistence.internal.helper.*; |
| import org.eclipse.persistence.internal.identitymaps.CacheKey; |
| import org.eclipse.persistence.internal.queries.*; |
| import org.eclipse.persistence.internal.sessions.*; |
| import org.eclipse.persistence.mappings.foundation.MapComponentMapping; |
| import org.eclipse.persistence.sessions.DatabaseRecord; |
| import org.eclipse.persistence.queries.*; |
| |
| /** |
| * <p><b>Purpose</b>: Many to many mappings are used to represent the relationships |
| * between a collection of source objects and a collection of target objects. |
| * The mapping requires the creation of an intermediate table for managing the |
| * associations between the source and target records. |
| * |
| * @author Sati |
| * @since TOPLink/Java 1.0 |
| */ |
| public class ManyToManyMapping extends CollectionMapping implements RelationalMapping, MapComponentMapping { |
| |
| /** Used for data modification events. */ |
| protected static final String PostInsert = "postInsert"; |
| protected static final String ObjectRemoved = "objectRemoved"; |
| protected static final String ObjectAdded = "objectAdded"; |
| |
| /** Mechanism holds relationTable and all fields and queries associated with it. */ |
| protected RelationTableMechanism mechanism; |
| protected HistoryPolicy historyPolicy; |
| |
| /** |
| * 266912: Since: EclipseLink 2.0 for the Metamodel API |
| * For 1:1 and m:m mappings - track the original externally defined mapping if different |
| * Note: This field will provide differentiation for the following |
| * external to internal representations for mapping types<br> |
| * - A OneToManyMapping will be represented by a ManyToManyMapping if unidirectional<br> |
| * - A ManyToOneMapping will be represented by a OneToOneMapping (without a FK constraint)<br> |
| */ |
| protected boolean isDefinedAsOneToManyMapping = false; |
| |
| /** |
| * PUBLIC: |
| * Default constructor. |
| */ |
| public ManyToManyMapping() { |
| this.mechanism = new RelationTableMechanism(); |
| this.isListOrderFieldSupported = true; |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| @Override |
| public boolean isOwned(){ |
| return !isReadOnly; |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| @Override |
| public boolean isRelationalMapping() { |
| return true; |
| } |
| |
| /** |
| * PUBLIC: |
| * Add the fields in the intermediate table that corresponds to the primary |
| * key in the source table. This method is used if the keys are composite. |
| */ |
| public void addSourceRelationKeyField(DatabaseField sourceRelationKeyField, DatabaseField sourcePrimaryKeyField) { |
| this.mechanism.addSourceRelationKeyField(sourceRelationKeyField, sourcePrimaryKeyField); |
| } |
| |
| /** |
| * PUBLIC: |
| * Add the fields in the intermediate table that corresponds to the primary |
| * key in the source table. This method is used if the keys are composite. |
| */ |
| public void addSourceRelationKeyFieldName(String sourceRelationKeyFieldName, String sourcePrimaryKeyFieldName) { |
| this.mechanism.addSourceRelationKeyFieldName(sourceRelationKeyFieldName, sourcePrimaryKeyFieldName); |
| } |
| |
| /** |
| * PUBLIC: |
| * Add the fields in the intermediate table that corresponds to the primary |
| * key in the target table. This method is used if the keys are composite. |
| */ |
| public void addTargetRelationKeyField(DatabaseField targetRelationKeyField, DatabaseField targetPrimaryKeyField) { |
| this.mechanism.addTargetRelationKeyField(targetRelationKeyField, targetPrimaryKeyField); |
| } |
| |
| /** |
| * PUBLIC: |
| * Add the fields in the intermediate table that corresponds to the primary |
| * key in the target table. This method is used if the keys are composite. |
| */ |
| public void addTargetRelationKeyFieldName(String targetRelationKeyFieldName, String targetPrimaryKeyFieldName) { |
| this.mechanism.addTargetRelationKeyFieldName(targetRelationKeyFieldName, targetPrimaryKeyFieldName); |
| } |
| |
| /** |
| * INTERNAL: |
| * This method is used to store the FK fields that can be cached that correspond to noncacheable mappings |
| * the FK field values will be used to re-issue the query when cloning the shared cache entity |
| */ |
| @Override |
| public void collectQueryParameters(Set<DatabaseField> cacheFields){ |
| this.mechanism.collectQueryParameters(cacheFields); |
| } |
| |
| /** |
| * INTERNAL: |
| * The mapping clones itself to create deep copy. |
| */ |
| @Override |
| public Object clone() { |
| ManyToManyMapping clone = (ManyToManyMapping)super.clone(); |
| clone.mechanism = (RelationTableMechanism)this.mechanism.clone(); |
| |
| return clone; |
| } |
| |
| /** |
| * INTERNAL: |
| * Delete join tables before the start of the deletion process to avoid constraint errors. |
| */ |
| @Override |
| public void earlyPreDelete(DeleteObjectQuery query, Object object) { |
| AbstractSession querySession = query.getSession(); |
| if (!this.isCascadeOnDeleteSetOnDatabase) { |
| prepareTranslationRow(query.getTranslationRow(), query.getObject(), query.getDescriptor(), querySession); |
| querySession.executeQuery(this.deleteAllQuery, query.getTranslationRow()); |
| } |
| |
| if ((this.historyPolicy != null) && this.historyPolicy.shouldHandleWrites()) { |
| if (this.isCascadeOnDeleteSetOnDatabase) { |
| prepareTranslationRow(query.getTranslationRow(), query.getObject(), query.getDescriptor(), querySession); |
| } |
| this.historyPolicy.mappingLogicalDelete(this.deleteAllQuery, query.getTranslationRow(), querySession); |
| } |
| } |
| |
| /** |
| * INTERNAL |
| * Called when a DatabaseMapping is used to map the key in a collection. Returns the key. |
| */ |
| @Override |
| public Object createMapComponentFromRow(AbstractRecord dbRow, ObjectBuildingQuery query, CacheKey parentCacheKey, AbstractSession session, boolean isTargetProtected){ |
| return session.executeQuery(getSelectionQuery(), dbRow); |
| } |
| |
| /** |
| * INTERNAL: |
| * Adds locking clause to the target query to extend pessimistic lock scope. |
| */ |
| @Override |
| protected void extendPessimisticLockScopeInTargetQuery(ObjectLevelReadQuery targetQuery, ObjectBuildingQuery sourceQuery) { |
| this.mechanism.setRelationTableLockingClause(targetQuery, sourceQuery); |
| } |
| |
| /** |
| * INTERNAL: |
| * Called only if both |
| * shouldExtendPessimisticLockScope and shouldExtendPessimisticLockScopeInSourceQuery are true. |
| * Adds fields to be locked to the where clause of the source query. |
| * Note that the sourceQuery must be ObjectLevelReadQuery so that it has ExpressionBuilder. |
| * |
| * This method must be implemented in subclasses that allow |
| * setting shouldExtendPessimisticLockScopeInSourceQuery to true. |
| */ |
| @Override |
| public void extendPessimisticLockScopeInSourceQuery(ObjectLevelReadQuery sourceQuery) { |
| Expression exp = sourceQuery.getSelectionCriteria(); |
| exp = this.mechanism.joinRelationTableField(exp, sourceQuery.getExpressionBuilder()); |
| sourceQuery.setSelectionCriteria(exp); |
| } |
| |
| /** |
| * INTERNAL: |
| * Extract the source primary key value from the relation row. |
| * Used for batch reading, most following same order and fields as in the mapping. |
| */ |
| @Override |
| protected Object extractKeyFromTargetRow(AbstractRecord row, AbstractSession session) { |
| return this.mechanism.extractKeyFromTargetRow(row, session); |
| } |
| |
| /** |
| * INTERNAL: |
| * Extract the primary key value from the source row. |
| * Used for batch reading, most following same order and fields as in the mapping. |
| */ |
| @Override |
| protected Object extractBatchKeyFromRow(AbstractRecord row, AbstractSession session) { |
| return this.mechanism.extractBatchKeyFromRow(row, session); |
| } |
| |
| /** |
| * INTERNAL: |
| * Return the selection criteria used to IN batch fetching. |
| */ |
| @Override |
| protected Expression buildBatchCriteria(ExpressionBuilder builder, ObjectLevelReadQuery query) { |
| return this.mechanism.buildBatchCriteria(builder, query); |
| } |
| |
| /** |
| * INTERNAL: |
| * Add additional fields and check for history. |
| */ |
| @Override |
| protected void postPrepareNestedBatchQuery(ReadQuery batchQuery, ObjectLevelReadQuery query) { |
| super.postPrepareNestedBatchQuery(batchQuery, query); |
| ReadAllQuery mappingBatchQuery = (ReadAllQuery)batchQuery; |
| this.mechanism.postPrepareNestedBatchQuery(batchQuery, query); |
| if (this.historyPolicy != null) { |
| ExpressionBuilder builder = mappingBatchQuery.getExpressionBuilder(); |
| Expression twisted = batchQuery.getSelectionCriteria(); |
| if (query.getSession().getAsOfClause() != null) { |
| builder.asOf(query.getSession().getAsOfClause()); |
| } else if (builder.getAsOfClause() == null) { |
| builder.asOf(AsOfClause.NO_CLAUSE); |
| } |
| twisted = twisted.and(this.historyPolicy.additionalHistoryExpression(builder, builder)); |
| mappingBatchQuery.setSelectionCriteria(twisted); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Return the base expression to use for adding fields to the query. |
| * Normally this is the query's builder, but may be the join table for m-m. |
| */ |
| @Override |
| protected Expression getAdditionalFieldsBaseExpression(ReadQuery query) { |
| return ((ReadAllQuery)query).getExpressionBuilder().getTable(getRelationTable()); |
| } |
| |
| |
| protected DataModifyQuery getDeleteQuery() { |
| return this.mechanism.getDeleteQuery(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Should be overridden by subclass that allows setting |
| * extendPessimisticLockScope to DEDICATED_QUERY. |
| */ |
| @Override |
| protected ReadQuery getExtendPessimisticLockScopeDedicatedQuery(AbstractSession session, short lockMode) { |
| if(this.mechanism != null) { |
| return this.mechanism.getLockRelationTableQueryClone(session, lockMode); |
| } else { |
| return super.getExtendPessimisticLockScopeDedicatedQuery(session, lockMode); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Return source key fields for translation by an AggregateObjectMapping |
| */ |
| @Override |
| public Collection getFieldsForTranslationInAggregate() { |
| return getRelationTableMechanism().getSourceKeyFields(); |
| } |
| |
| protected DataModifyQuery getInsertQuery() { |
| return this.mechanism.getInsertQuery(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Returns the join criteria stored in the mapping selection query. This criteria |
| * is used to read reference objects across the tables from the database. |
| */ |
| @Override |
| public Expression getJoinCriteria(ObjectExpression context, Expression base) { |
| if (getHistoryPolicy() != null) { |
| Expression result = super.getJoinCriteria(context, base); |
| Expression historyCriteria = getHistoryPolicy().additionalHistoryExpression(context, base); |
| if (result != null) { |
| return result.and(historyCriteria); |
| } else if (historyCriteria != null) { |
| return historyCriteria; |
| } else { |
| return null; |
| } |
| } else { |
| return super.getJoinCriteria(context, base); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Allows history tracking on the m-m join table. |
| */ |
| public HistoryPolicy getHistoryPolicy() { |
| return historyPolicy; |
| } |
| |
| /** |
| * PUBLIC: |
| * Returns RelationTableMechanism that may be owned by the mapping. |
| * Note that all RelationTableMechanism methods are accessible |
| * through the mapping directly. |
| * The only reason this method is provided |
| * is to allow a uniform approach to RelationTableMechanism |
| * in both ManyToManyMapping and OneToOneMapping |
| * that uses RelationTableMechanism. |
| */ |
| public RelationTableMechanism getRelationTableMechanism() { |
| return this.mechanism; |
| } |
| |
| /** |
| * INTERNAL: |
| * Return the relation table associated with the mapping. |
| */ |
| public DatabaseTable getRelationTable() { |
| return this.mechanism.getRelationTable(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the relation table name associated with the mapping. |
| */ |
| public String getRelationTableName() { |
| return this.mechanism.getRelationTableName(); |
| } |
| |
| //CR#2407 This method is added to include table qualifier. |
| |
| /** |
| * PUBLIC: |
| * Return the relation table qualified name associated with the mapping. |
| */ |
| public String getRelationTableQualifiedName() { |
| return this.mechanism.getRelationTableQualifiedName(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the source key field names associated with the mapping. |
| * These are in-order with the sourceRelationKeyFieldNames. |
| */ |
| public Vector getSourceKeyFieldNames() { |
| return this.mechanism.getSourceKeyFieldNames(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Return all the source key fields associated with the mapping. |
| */ |
| public Vector<DatabaseField> getSourceKeyFields() { |
| return this.mechanism.getSourceKeyFields(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the source relation key field names associated with the mapping. |
| * These are in-order with the sourceKeyFieldNames. |
| */ |
| public Vector getSourceRelationKeyFieldNames() { |
| return this.mechanism.getSourceRelationKeyFieldNames(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Return all the source relation key fields associated with the mapping. |
| */ |
| public Vector<DatabaseField> getSourceRelationKeyFields() { |
| return this.mechanism.getSourceRelationKeyFields(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the target key field names associated with the mapping. |
| * These are in-order with the targetRelationKeyFieldNames. |
| */ |
| public Vector getTargetKeyFieldNames() { |
| return this.mechanism.getTargetKeyFieldNames(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Return all the target keys associated with the mapping. |
| */ |
| public Vector<DatabaseField> getTargetKeyFields() { |
| return this.mechanism.getTargetKeyFields(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the target relation key field names associated with the mapping. |
| * These are in-order with the targetKeyFieldNames. |
| */ |
| public Vector getTargetRelationKeyFieldNames() { |
| return this.mechanism.getTargetRelationKeyFieldNames(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Return all the target relation key fields associated with the mapping. |
| */ |
| public Vector<DatabaseField> getTargetRelationKeyFields() { |
| return this.mechanism.getTargetRelationKeyFields(); |
| } |
| |
| protected boolean hasCustomDeleteQuery() { |
| return this.mechanism.hasCustomDeleteQuery(); |
| } |
| |
| protected boolean hasCustomInsertQuery() { |
| return this.mechanism.hasCustomInsertQuery(); |
| } |
| |
| /** |
| * INTERNAL: |
| * The join table is a dependency if not read-only. |
| */ |
| @Override |
| public boolean hasDependency() { |
| return this.isPrivateOwned || (!this.isReadOnly); |
| } |
| |
| /** |
| * INTERNAL: |
| * Initialize mappings |
| */ |
| @Override |
| public void initialize(AbstractSession session) throws DescriptorException { |
| if (session.hasBroker()) { |
| if (getReferenceClass() == null) { |
| throw DescriptorException.referenceClassNotSpecified(this); |
| } |
| // substitute session that owns the mapping for the session that owns reference descriptor. |
| session = session.getBroker().getSessionForClass(getReferenceClass()); |
| } |
| |
| super.initialize(session); |
| getDescriptor().addPreDeleteMapping(this); |
| |
| if(this.mechanism != null) { |
| this.mechanism.initialize(session, this); |
| } else { |
| throw DescriptorException.noRelationTableMechanism(this); |
| } |
| |
| if (shouldInitializeSelectionCriteria()) { |
| if (shouldForceInitializationOfSelectionCriteria()) { |
| initializeSelectionCriteriaAndAddFieldsToQuery(null); |
| } else { |
| initializeSelectionCriteriaAndAddFieldsToQuery(getSelectionCriteria()); |
| } |
| } |
| if (!getSelectionQuery().hasSessionName()) { |
| getSelectionQuery().setSessionName(session.getName()); |
| } |
| |
| initializeDeleteAllQuery(session); |
| if (getHistoryPolicy() != null) { |
| getHistoryPolicy().initialize(session); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Verifies listOrderField's table: it must be relation table. |
| * Precondition: listOrderField != null. |
| */ |
| @Override |
| protected void buildListOrderField() { |
| if(this.listOrderField.hasTableName()) { |
| if(!getRelationTable().equals(this.listOrderField.getTable())) { |
| throw DescriptorException.listOrderFieldTableIsWrong(this.getDescriptor(), this, this.listOrderField.getTable(), getRelationTable()); |
| } |
| } else { |
| listOrderField.setTable(getRelationTable()); |
| } |
| this.listOrderField = getDescriptor().buildField(this.listOrderField, getRelationTable()); |
| } |
| |
| /** |
| * INTERNAL: |
| * Indicates whether getListOrderFieldExpression method should create field expression on table expression. |
| */ |
| @Override |
| public boolean shouldUseListOrderFieldTableExpression() { |
| return true; |
| } |
| |
| /** |
| * INTERNAL: |
| * Initialize changeOrderTargetQuery. |
| */ |
| @Override |
| protected void initializeChangeOrderTargetQuery(AbstractSession session) { |
| boolean hasChangeOrderTargetQuery = changeOrderTargetQuery != null; |
| if(!hasChangeOrderTargetQuery) { |
| changeOrderTargetQuery = new DataModifyQuery(); |
| } |
| |
| changeOrderTargetQuery = new DataModifyQuery(); |
| if (!changeOrderTargetQuery.hasSessionName()) { |
| changeOrderTargetQuery.setSessionName(session.getName()); |
| } |
| if (hasChangeOrderTargetQuery) { |
| return; |
| } |
| |
| // Build where clause expression. |
| Expression whereClause = null; |
| Expression builder = new ExpressionBuilder(); |
| |
| List<DatabaseField> sourceRelationKeyFields = getSourceRelationKeyFields(); |
| int size = sourceRelationKeyFields.size(); |
| for (int index = 0; index < size; index++) { |
| DatabaseField sourceRelationKeyField = sourceRelationKeyFields.get(index); |
| Expression expression = builder.getField(sourceRelationKeyField).equal(builder.getParameter(sourceRelationKeyField)); |
| whereClause = expression.and(whereClause); |
| } |
| |
| List<DatabaseField> targetRelationKeyFields = getTargetRelationKeyFields(); |
| size = targetRelationKeyFields.size(); |
| for (int index = 0; index < size; index++) { |
| DatabaseField targetRelationKeyField = targetRelationKeyFields.get(index); |
| Expression expression = builder.getField(targetRelationKeyField).equal(builder.getParameter(targetRelationKeyField)); |
| whereClause = expression.and(whereClause); |
| } |
| |
| AbstractRecord modifyRow = new DatabaseRecord(); |
| modifyRow.add(listOrderField, null); |
| |
| SQLUpdateStatement statement = new SQLUpdateStatement(); |
| statement.setTable(listOrderField.getTable()); |
| statement.setWhereClause(whereClause); |
| statement.setModifyRow(modifyRow); |
| changeOrderTargetQuery.setSQLStatement(statement); |
| } |
| |
| /** |
| * Initialize delete all query. This query is used to all relevant rows from the |
| * relation table. |
| */ |
| protected void initializeDeleteAllQuery(AbstractSession session) { |
| if (!getDeleteAllQuery().hasSessionName()) { |
| getDeleteAllQuery().setSessionName(session.getName()); |
| } |
| getDeleteAllQuery().setName(getAttributeName()); |
| if (getDeleteAllQuery().getPartitioningPolicy() == null) { |
| getDeleteAllQuery().setPartitioningPolicy(getPartitioningPolicy()); |
| } |
| if (hasCustomDeleteAllQuery()) { |
| return; |
| } |
| |
| Expression expression = null; |
| Expression subExpression; |
| Expression builder = new ExpressionBuilder(); |
| SQLDeleteStatement statement = new SQLDeleteStatement(); |
| |
| // Construct an expression to delete from the relation table. |
| for (int index = 0; index < getSourceRelationKeyFields().size(); index++) { |
| DatabaseField sourceRelationKey = getSourceRelationKeyFields().elementAt(index); |
| DatabaseField sourceKey = getSourceKeyFields().elementAt(index); |
| |
| subExpression = builder.getField(sourceRelationKey).equal(builder.getParameter(sourceKey)); |
| expression = subExpression.and(expression); |
| } |
| |
| // All the entries are deleted in one shot. |
| statement.setWhereClause(expression); |
| statement.setTable(getRelationTable()); |
| getDeleteAllQuery().setSQLStatement(statement); |
| } |
| |
| /** |
| * INTERNAL: |
| * Initializes listOrderField's table. |
| * Precondition: listOrderField != null. |
| */ |
| @Override |
| protected void initializeListOrderFieldTable(AbstractSession session) { |
| this.mechanism.initializeRelationTable(session, this); |
| } |
| |
| /** |
| * INTERNAL: |
| * Selection criteria is created to read target records from the table. |
| */ |
| protected void initializeSelectionCriteriaAndAddFieldsToQuery(Expression startCriteria) { |
| setSelectionCriteria(this.mechanism.buildSelectionCriteriaAndAddFieldsToQuery(this, startCriteria)); |
| } |
| |
| /** |
| * INTERNAL: |
| * An object was added to the collection during an update, insert it. |
| */ |
| protected void insertAddedObjectEntry(ObjectLevelModifyQuery query, Object objectAdded, Map extraData) throws DatabaseException, OptimisticLockException { |
| //cr 3819 added the line below to fix the translationtable to ensure that it |
| // contains the required values |
| prepareTranslationRow(query.getTranslationRow(), query.getObject(), query.getDescriptor(), query.getSession()); |
| AbstractRecord databaseRow = this.mechanism.buildRelationTableSourceAndTargetRow(query.getTranslationRow(), containerPolicy.unwrapIteratorResult(objectAdded), query.getSession(), this); |
| ContainerPolicy.copyMapDataToRow(getContainerPolicy().getKeyMappingDataForWriteQuery(objectAdded, query.getSession()), databaseRow); |
| |
| if(listOrderField != null && extraData != null) { |
| databaseRow.put(listOrderField, extraData.get(listOrderField)); |
| } |
| |
| query.getExecutionSession().executeQuery(this.mechanism.getInsertQuery(), databaseRow); |
| if ((getHistoryPolicy() != null) && getHistoryPolicy().shouldHandleWrites()) { |
| getHistoryPolicy().mappingLogicalInsert(this.mechanism.getInsertQuery(), databaseRow, query.getSession()); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Insert into relation table. This follows following steps. |
| * <p>- Extract primary key and its value from the source object. |
| * <p>- Extract target key and its value from the target object. |
| * <p>- Construct a insert statement with above fields and values for relation table. |
| * <p>- execute the statement. |
| * <p>- Repeat above three statements until all the target objects are done. |
| */ |
| public void insertIntoRelationTable(WriteObjectQuery query) throws DatabaseException { |
| if (isReadOnly()) { |
| return; |
| } |
| |
| ContainerPolicy cp = getContainerPolicy(); |
| Object objects = getRealCollectionAttributeValueFromObject(query.getObject(), query.getSession()); |
| if (cp.isEmpty(objects)) { |
| return; |
| } |
| |
| prepareTranslationRow(query.getTranslationRow(), query.getObject(), query.getDescriptor(), query.getSession()); |
| AbstractRecord databaseRow = this.mechanism.buildRelationTableSourceRow(query.getTranslationRow()); |
| |
| int orderIndex = 0; |
| // Extract target field and its value. Construct insert statement and execute it |
| for (Object iter = cp.iteratorFor(objects); cp.hasNext(iter);) { |
| Object wrappedObject = cp.nextEntry(iter, query.getSession()); |
| Object object = cp.unwrapIteratorResult(wrappedObject); |
| databaseRow = this.mechanism.addRelationTableTargetRow(object, query.getExecutionSession(), databaseRow, this); |
| |
| ContainerPolicy.copyMapDataToRow(cp.getKeyMappingDataForWriteQuery(wrappedObject, query.getSession()), databaseRow); |
| |
| if(listOrderField != null) { |
| databaseRow.put(listOrderField, orderIndex++); |
| } |
| |
| query.getExecutionSession().executeQuery(this.mechanism.getInsertQuery(), databaseRow); |
| if ((getHistoryPolicy() != null) && getHistoryPolicy().shouldHandleWrites()) { |
| getHistoryPolicy().mappingLogicalInsert(this.mechanism.getInsertQuery(), databaseRow, query.getSession()); |
| } |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Write the target objects if the cascade policy requires them to be written first. |
| * They must be written within a unit of work to ensure that they exist. |
| */ |
| public void insertTargetObjects(WriteObjectQuery query) throws DatabaseException, OptimisticLockException { |
| if (!shouldObjectModifyCascadeToParts(query)) { |
| return; |
| } |
| |
| // Only cascade dependents writes in uow. |
| if (query.shouldCascadeOnlyDependentParts()) { |
| return; |
| } |
| |
| Object objects = getRealCollectionAttributeValueFromObject(query.getObject(), query.getSession()); |
| |
| ContainerPolicy cp = getContainerPolicy(); |
| if (cp.isEmpty(objects)) { |
| return; |
| } |
| |
| // Write each of the target objects |
| for (Object objectsIterator = cp.iteratorFor(objects); cp.hasNext(objectsIterator);) { |
| Object wrappedObject = cp.next(objectsIterator, query.getSession()); |
| Object object = cp.unwrapIteratorResult(wrappedObject); |
| if (isPrivateOwned()) { |
| // no need to set changeset as insert is a straight copy anyway |
| InsertObjectQuery insertQuery = new InsertObjectQuery(); |
| insertQuery.setIsExecutionClone(true); |
| insertQuery.setObject(object); |
| insertQuery.setCascadePolicy(query.getCascadePolicy()); |
| query.getSession().executeQuery(insertQuery); |
| } else { |
| ObjectChangeSet changeSet = null; |
| UnitOfWorkChangeSet uowChangeSet = null; |
| if (query.getSession().isUnitOfWork() && (((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet() != null)) { |
| uowChangeSet = (UnitOfWorkChangeSet)((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet(); |
| changeSet = (ObjectChangeSet)uowChangeSet.getObjectChangeSetForClone(object); |
| } |
| WriteObjectQuery writeQuery = new WriteObjectQuery(); |
| writeQuery.setIsExecutionClone(true); |
| writeQuery.setObject(object); |
| writeQuery.setObjectChangeSet(changeSet); |
| writeQuery.setCascadePolicy(query.getCascadePolicy()); |
| query.getSession().executeQuery(writeQuery); |
| } |
| cp.propogatePostInsert(query, wrappedObject); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Return whether this mapping was originally defined as a OneToMany. |
| */ |
| public boolean isDefinedAsOneToManyMapping() { |
| return isDefinedAsOneToManyMapping; |
| } |
| |
| /** |
| * INTERNAL: |
| * Return if this mapping support joining. |
| */ |
| @Override |
| public boolean isJoiningSupported() { |
| return true; |
| } |
| |
| @Override |
| public boolean isManyToManyMapping() { |
| return true; |
| } |
| |
| /** |
| * INTERNAL: |
| * Ensure the container policy is post initialized |
| */ |
| @Override |
| public void postInitialize(AbstractSession session) { |
| super.postInitialize(session); |
| this.mustDeleteReferenceObjectsOneByOne = true; |
| } |
| |
| /** |
| * INTERNAL: |
| * An object was added to the collection during an update, insert it if private. |
| */ |
| @Override |
| protected void objectAddedDuringUpdate(ObjectLevelModifyQuery query, Object objectAdded, ObjectChangeSet changeSet, Map extraData) throws DatabaseException, OptimisticLockException { |
| // First insert/update object. |
| super.objectAddedDuringUpdate(query, objectAdded, changeSet, extraData); |
| |
| // In the uow data queries are cached until the end of the commit. |
| if (query.shouldCascadeOnlyDependentParts()) { |
| // Hey I might actually want to use an inner class here... ok array for now. |
| Object[] event = new Object[4]; |
| event[0] = ObjectAdded; |
| event[1] = query; |
| event[2] = objectAdded; |
| event[3] = extraData; |
| query.getSession().getCommitManager().addDataModificationEvent(this, event); |
| } else { |
| insertAddedObjectEntry(query, objectAdded, extraData); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * An object was removed to the collection during an update, delete it if private. |
| */ |
| @Override |
| protected void objectRemovedDuringUpdate(ObjectLevelModifyQuery query, Object objectDeleted, Map extraData) throws DatabaseException, OptimisticLockException { |
| Object unwrappedObjectDeleted = getContainerPolicy().unwrapIteratorResult(objectDeleted); |
| AbstractRecord databaseRow = this.mechanism.buildRelationTableSourceAndTargetRow(query.getTranslationRow(), unwrappedObjectDeleted, query.getSession(), this); |
| |
| // In the uow data queries are cached until the end of the commit. |
| if (query.shouldCascadeOnlyDependentParts()) { |
| // Hey I might actually want to use an inner class here... ok array for now. |
| Object[] event = new Object[3]; |
| event[0] = ObjectRemoved; |
| event[1] = this.mechanism.getDeleteQuery(); |
| event[2] = databaseRow; |
| query.getSession().getCommitManager().addDataModificationEvent(this, event); |
| } else { |
| query.getSession().executeQuery(this.mechanism.getDeleteQuery(), databaseRow); |
| if ((getHistoryPolicy() != null) && getHistoryPolicy().shouldHandleWrites()) { |
| getHistoryPolicy().mappingLogicalDelete(this.mechanism.getDeleteQuery(), databaseRow, query.getSession()); |
| } |
| } |
| |
| // Delete object after join entry is delete if private. |
| super.objectRemovedDuringUpdate(query, objectDeleted, extraData); |
| } |
| |
| @Override |
| protected void objectOrderChangedDuringUpdate(WriteObjectQuery query, Object orderChangedObject, int orderIndex) { |
| prepareTranslationRow(query.getTranslationRow(), query.getObject(), query.getDescriptor(), query.getSession()); |
| AbstractRecord databaseRow = this.mechanism.buildRelationTableSourceAndTargetRow(query.getTranslationRow(), orderChangedObject, query.getSession(), this); |
| databaseRow.put(listOrderField, orderIndex); |
| |
| query.getSession().executeQuery(changeOrderTargetQuery, databaseRow); |
| } |
| |
| /** |
| * INTERNAL: |
| * Perform the commit event. |
| * This is used in the uow to delay data modifications. |
| */ |
| @Override |
| public void performDataModificationEvent(Object[] event, AbstractSession session) throws DatabaseException, DescriptorException { |
| // Hey I might actually want to use an inner class here... ok array for now. |
| if (event[0] == PostInsert) { |
| insertIntoRelationTable((WriteObjectQuery)event[1]); |
| } else if (event[0] == ObjectRemoved) { |
| session.executeQuery((DataModifyQuery)event[1], (AbstractRecord)event[2]); |
| if ((getHistoryPolicy() != null) && getHistoryPolicy().shouldHandleWrites()) { |
| getHistoryPolicy().mappingLogicalDelete((DataModifyQuery)event[1], (AbstractRecord)event[2], session); |
| } |
| } else if (event[0] == ObjectAdded) { |
| insertAddedObjectEntry((WriteObjectQuery)event[1], event[2], (Map)event[3]); |
| } else { |
| throw DescriptorException.invalidDataModificationEventCode(event[0], this); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Insert into relation table. This follows following steps. |
| * <p>- Extract primary key and its value from the source object. |
| * <p>- Extract target key and its value from the target object. |
| * <p>- Construct a insert statement with above fields and values for relation table. |
| * <p>- execute the statement. |
| * <p>- Repeat above three statements until all the target objects are done. |
| */ |
| @Override |
| public void postInsert(WriteObjectQuery query) throws DatabaseException { |
| insertTargetObjects(query); |
| // Batch data modification in the uow |
| if (query.shouldCascadeOnlyDependentParts()) { |
| // Hey I might actually want to use an inner class here... ok array for now. |
| Object[] event = new Object[2]; |
| event[0] = PostInsert; |
| event[1] = query; |
| query.getSession().getCommitManager().addDataModificationEvent(this, event); |
| } else { |
| insertIntoRelationTable(query); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Update the relation table with the entries related to this mapping. |
| * Delete entries removed, insert entries added. |
| * If private also insert/delete/update target objects. |
| */ |
| @Override |
| public void postUpdate(WriteObjectQuery query) throws DatabaseException { |
| if (this.isReadOnly) { |
| return; |
| } |
| |
| // If objects are not instantiated that means they are not changed. |
| if (!isAttributeValueInstantiatedOrChanged(query.getObject())) { |
| return; |
| } |
| if (query.getObjectChangeSet() != null) { |
| // UnitOfWork |
| writeChanges(query.getObjectChangeSet(), query); |
| } else { |
| // OLD COMMIT |
| compareObjectsAndWrite(query); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Delete entries related to this mapping from the relation table. |
| */ |
| @Override |
| public void preDelete(DeleteObjectQuery query) throws DatabaseException { |
| AbstractSession session = query.getSession(); |
| Object objectsIterator = null; |
| ContainerPolicy containerPolicy = getContainerPolicy(); |
| |
| if (this.isReadOnly) { |
| return; |
| } |
| Object objects = null; |
| |
| boolean cascade = shouldObjectModifyCascadeToParts(query); |
| if (containerPolicy.propagatesEventsToCollection() || cascade) { |
| // if processed during UnitOfWork commit process the private owned delete will occur during change calculation |
| objects = getRealCollectionAttributeValueFromObject(query.getObject(), session); |
| //this must be done up here because the select must be done before the entry in the relation table is deleted. |
| // TODO: Hmm given the below code, the rows are already deleted, so this code is broken. |
| // Assuming it was a cascade remove, it will have been instantiated, so may be ok? |
| objectsIterator = containerPolicy.iteratorFor(objects); |
| } |
| |
| // This has already been done in a unit of work. |
| if (!session.isUnitOfWork()) { |
| earlyPreDelete(query, query.getObject()); |
| } |
| |
| // If privately owned delete the objects, this does not handle removed objects (i.e. verify delete, not req in uow). |
| // Does not try to optimize delete all like 1-m, (rarely used and hard to do). |
| if (containerPolicy.propagatesEventsToCollection() || cascade) { |
| if (objects != null) { |
| //objectsIterator will not be null because cascade check will still return true. |
| while (containerPolicy.hasNext(objectsIterator)) { |
| Object wrappedObject = containerPolicy.nextEntry(objectsIterator, session); |
| Object object = containerPolicy.unwrapIteratorResult(wrappedObject); |
| if (cascade){ |
| // PERF: Avoid query execution if already deleted. |
| if (!session.getCommitManager().isCommitCompletedInPostOrIgnore(object)) { |
| DeleteObjectQuery deleteQuery = new DeleteObjectQuery(); |
| deleteQuery.setIsExecutionClone(true); |
| deleteQuery.setObject(object); |
| deleteQuery.setCascadePolicy(query.getCascadePolicy()); |
| session.executeQuery(deleteQuery); |
| } |
| } |
| containerPolicy.propogatePreDelete(query, wrappedObject); |
| } |
| } |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * The translation row may require additional fields than the primary key if the mapping in not on the primary key. |
| */ |
| @Override |
| protected void prepareTranslationRow(AbstractRecord translationRow, Object object, ClassDescriptor descriptor, AbstractSession session) { |
| // Make sure that each source key field is in the translation row. |
| for (Enumeration<DatabaseField> sourceFieldsEnum = getSourceKeyFields().elements(); |
| sourceFieldsEnum.hasMoreElements();) { |
| DatabaseField sourceKey = sourceFieldsEnum.nextElement(); |
| if (!translationRow.containsKey(sourceKey)) { |
| Object value = descriptor.getObjectBuilder().extractValueFromObjectForField(object, sourceKey, session); |
| translationRow.put(sourceKey, value); |
| } |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * The default delete query for mapping can be overridden by specifying the new query. |
| * This query must delete the row from the M-M join table. |
| */ |
| public void setCustomDeleteQuery(DataModifyQuery query) { |
| this.mechanism.setCustomDeleteQuery(query); |
| } |
| |
| /** |
| * PUBLIC: |
| * The default insert query for mapping can be overridden by specifying the new query. |
| * This query must insert the row into the M-M join table. |
| */ |
| public void setCustomInsertQuery(DataModifyQuery query) { |
| this.mechanism.setCustomInsertQuery(query); |
| } |
| |
| protected void setDeleteQuery(DataModifyQuery deleteQuery) { |
| this.mechanism.setDeleteQuery(deleteQuery); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the receiver's delete SQL string. This allows the user to override the SQL |
| * generated by TOPLink, with there own SQL or procedure call. The arguments are |
| * translated from the fields of the source row, through replacing the field names |
| * marked by '#' with the values for those fields. |
| * This is used to delete a single entry from the M-M join table. |
| * Example, 'delete from PROJ_EMP where PROJ_ID = #PROJ_ID AND EMP_ID = #EMP_ID'. |
| */ |
| public void setDeleteSQLString(String sqlString) { |
| this.mechanism.setDeleteSQLString(sqlString); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set whether this mapping was originally defined as a OneToMany |
| */ |
| public void setDefinedAsOneToManyMapping(boolean isDefinedAsOneToManyMapping) { |
| this.isDefinedAsOneToManyMapping = isDefinedAsOneToManyMapping; |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the receiver's delete Call. This allows the user to override the SQL |
| * generated by TOPLink, with there own SQL or procedure call. The arguments are |
| * translated from the fields of the source row. |
| * This is used to delete a single entry from the M-M join table. |
| * Example, 'new SQLCall("delete from PROJ_EMP where PROJ_ID = #PROJ_ID AND EMP_ID = #EMP_ID")'. |
| */ |
| public void setDeleteCall(Call call) { |
| this.mechanism.setDeleteCall(call); |
| } |
| |
| protected void setInsertQuery(DataModifyQuery insertQuery) { |
| this.mechanism.setInsertQuery(insertQuery); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the receiver's insert SQL string. This allows the user to override the SQL |
| * generated by TOPLink, with there own SQL or procedure call. The arguments are |
| * translated from the fields of the source row, through replacing the field names |
| * marked by '#' with the values for those fields. |
| * This is used to insert an entry into the M-M join table. |
| * Example, 'insert into PROJ_EMP (EMP_ID, PROJ_ID) values (#EMP_ID, #PROJ_ID)'. |
| */ |
| public void setInsertSQLString(String sqlString) { |
| this.mechanism.setInsertSQLString(sqlString); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the receiver's insert Call. This allows the user to override the SQL |
| * generated by TOPLink, with there own SQL or procedure call. The arguments are |
| * translated from the fields of the source row. |
| * This is used to insert an entry into the M-M join table. |
| * Example, 'new SQLCall("insert into PROJ_EMP (EMP_ID, PROJ_ID) values (#EMP_ID, #PROJ_ID)")'. |
| */ |
| public void setInsertCall(Call call) { |
| this.mechanism.setInsertCall(call); |
| } |
| |
| /** |
| * PUBLIC: |
| * Allows to set RelationTableMechanism to be owned by the mapping. |
| * It's not necessary to explicitly set the mechanism: |
| * one is created by mapping's constructor. |
| * The only reason this method is provided |
| * is to allow a uniform approach to RelationTableMechanism |
| * in both ManyToManyMapping and OneToOneMapping |
| * that uses RelationTableMechanism. |
| * ManyToManyMapping must have RelationTableMechanism, |
| * never set it to null. |
| */ |
| void setRelationTableMechanism(RelationTableMechanism mechanism) { |
| this.mechanism = mechanism; |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the relational table. |
| * This is the join table that store both the source and target primary keys. |
| */ |
| public void setRelationTable(DatabaseTable relationTable) { |
| this.mechanism.setRelationTable(relationTable); |
| } |
| |
| /** |
| * PUBLIC: |
| * Enable history tracking on the m-m join table. |
| */ |
| public void setHistoryPolicy(HistoryPolicy policy) { |
| this.historyPolicy = policy; |
| if (policy != null) { |
| policy.setMapping(this); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the name of the relational table. |
| * This is the join table that store both the source and target primary keys. |
| */ |
| public void setRelationTableName(String tableName) { |
| this.mechanism.setRelationTableName(tableName); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the name of the session to execute the mapping's queries under. |
| * This can be used by the session broker to override the default session |
| * to be used for the target class. |
| */ |
| @Override |
| public void setSessionName(String name) { |
| super.setSessionName(name); |
| this.mechanism.setSessionName(name); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the source key field names associated with the mapping. |
| * These must be in-order with the sourceRelationKeyFieldNames. |
| */ |
| public void setSourceKeyFieldNames(Vector fieldNames) { |
| this.mechanism.setSourceKeyFieldNames(fieldNames); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set the source fields. |
| */ |
| public void setSourceKeyFields(Vector<DatabaseField> sourceKeyFields) { |
| this.mechanism.setSourceKeyFields(sourceKeyFields); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the source key field in the relation table. |
| * This is the name of the foreign key in the relation table to the source's primary key field. |
| * This method is used if the source primary key is a singleton only. |
| */ |
| public void setSourceRelationKeyFieldName(String sourceRelationKeyFieldName) { |
| this.mechanism.setSourceRelationKeyFieldName(sourceRelationKeyFieldName); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the source relation key field names associated with the mapping. |
| * These must be in-order with the sourceKeyFieldNames. |
| */ |
| public void setSourceRelationKeyFieldNames(Vector fieldNames) { |
| this.mechanism.setSourceRelationKeyFieldNames(fieldNames); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set the source fields. |
| */ |
| public void setSourceRelationKeyFields(Vector<DatabaseField> sourceRelationKeyFields) { |
| this.mechanism.setSourceRelationKeyFields(sourceRelationKeyFields); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set the target key field names associated with the mapping. |
| * These must be in-order with the targetRelationKeyFieldNames. |
| */ |
| public void setTargetKeyFieldNames(Vector fieldNames) { |
| this.mechanism.setTargetKeyFieldNames(fieldNames); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set the target fields. |
| */ |
| public void setTargetKeyFields(Vector<DatabaseField> targetKeyFields) { |
| this.mechanism.setTargetKeyFields(targetKeyFields); |
| } |
| |
| /** |
| * PUBLIC: |
| * Set the target key field in the relation table. |
| * This is the name of the foreign key in the relation table to the target's primary key field. |
| * This method is used if the target's primary key is a singleton only. |
| */ |
| public void setTargetRelationKeyFieldName(String targetRelationKeyFieldName) { |
| this.mechanism.setTargetRelationKeyFieldName(targetRelationKeyFieldName); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set the target relation key field names associated with the mapping. |
| * These must be in-order with the targetKeyFieldNames. |
| */ |
| public void setTargetRelationKeyFieldNames(Vector fieldNames) { |
| this.mechanism.setTargetRelationKeyFieldNames(fieldNames); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set the target fields. |
| */ |
| public void setTargetRelationKeyFields(Vector<DatabaseField> targetRelationKeyFields) { |
| this.mechanism.setTargetRelationKeyFields(targetRelationKeyFields); |
| } |
| |
| /** |
| * INTERNAL: |
| * Append the temporal selection to the query selection criteria. |
| */ |
| @Override |
| protected ReadQuery prepareHistoricalQuery(ReadQuery targetQuery, ObjectBuildingQuery sourceQuery, AbstractSession executionSession) { |
| if (getHistoryPolicy() != null) { |
| if (targetQuery == getSelectionQuery()) { |
| targetQuery = (ObjectLevelReadQuery)targetQuery.clone(); |
| targetQuery.setIsExecutionClone(true); |
| } |
| if (targetQuery.getSelectionCriteria() == getSelectionQuery().getSelectionCriteria()) { |
| targetQuery.setSelectionCriteria((Expression)targetQuery.getSelectionCriteria().clone()); |
| } |
| if (sourceQuery.getSession().getAsOfClause() != null) { |
| ((ObjectLevelReadQuery)targetQuery).setAsOfClause(sourceQuery.getSession().getAsOfClause()); |
| } else if (((ObjectLevelReadQuery)targetQuery).getAsOfClause() == null) { |
| ((ObjectLevelReadQuery)targetQuery).setAsOfClause(AsOfClause.NO_CLAUSE); |
| } |
| Expression temporalExpression = (this).getHistoryPolicy().additionalHistoryExpression(targetQuery.getSelectionCriteria().getBuilder(), targetQuery.getSelectionCriteria().getBuilder()); |
| targetQuery.setSelectionCriteria(targetQuery.getSelectionCriteria().and(temporalExpression)); |
| } |
| return targetQuery; |
| } |
| } |