blob: a4e86828627963637591c1010feb6686bee78956 [file] [log] [blame]
/*
* 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 sourceFieldsEnum = getSourceKeyFields().elements();
sourceFieldsEnum.hasMoreElements();) {
DatabaseField sourceKey = (DatabaseField)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;
}
}