blob: 5f3294ef26427d2695ab0c0373dde171daf031bc [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:
// ailitchev - Uni-directional OneToMany
// 07/19/2011-2.2.1 Guy Pelletier
// - 338812: ManyToMany mapping in aggregate object violate integrity constraint on deletion
// 09/12/2018 - Will Dazey
// - 391279: Add support for Unidirectional OneToMany mappings with non-nullable values
package org.eclipse.persistence.mappings;
import java.util.Iterator;
import java.util.Vector;
import org.eclipse.persistence.config.SystemProperties;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.eclipse.persistence.internal.descriptors.CascadeLockingPolicy;
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.ChangeRecord;
import org.eclipse.persistence.internal.sessions.CollectionChangeRecord;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.queries.DeleteObjectQuery;
import org.eclipse.persistence.queries.ObjectLevelModifyQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadAllQuery;
import org.eclipse.persistence.queries.ReadQuery;
import org.eclipse.persistence.sessions.DatabaseRecord;
/**
* <p><b>Purpose</b>: UnidirectionalOneToManyMapping doesn't have 1:1 back reference mapping.
*
* @author Andrei Ilitchev
* @since Eclipselink 1.1
*/
public class UnidirectionalOneToManyMapping extends OneToManyMapping {
/**
* Indicates whether target's optimistic locking value should be incremented on
* target being added to / removed from a source.
**/
protected boolean shouldIncrementTargetLockValueOnAddOrRemoveTarget;
/**
* Indicates whether target's optimistic locking value should be incremented on
* the source deletion.
* Note that if the flag is set to true then the indirection will be triggered on
* source delete - in order to verify all targets' versions.
**/
protected boolean shouldIncrementTargetLockValueOnDeleteSource;
/**
* PUBLIC:
* Default constructor.
*/
public UnidirectionalOneToManyMapping() {
super();
this.shouldIncrementTargetLockValueOnAddOrRemoveTarget = true;
this.shouldIncrementTargetLockValueOnDeleteSource = true;
}
/**
* INTERNAL:
* Build a row containing the keys for use in the query that updates the row for the
* target object during an insert or update
*/
@Override
protected AbstractRecord buildKeyRowForTargetUpdate(ObjectLevelModifyQuery query){
AbstractRecord keyRow = new DatabaseRecord();
// Extract primary key and value from the source.
int size = sourceKeyFields.size();
for (int index = 0; index < size; index++) {
DatabaseField sourceKey = sourceKeyFields.get(index);
DatabaseField targetForeignKey = targetForeignKeyFields.get(index);
Object sourceKeyValue = query.getTranslationRow().get(sourceKey);
keyRow.put(targetForeignKey, sourceKeyValue);
}
return keyRow;
}
/**
* INTERNAL:
* This method is used to create a change record from comparing two collections
* @return org.eclipse.persistence.internal.sessions.ChangeRecord
*/
@Override
public ChangeRecord compareForChange(Object clone, Object backUp, ObjectChangeSet owner, AbstractSession uow) {
ChangeRecord record = super.compareForChange(clone, backUp, owner, uow);
if(record != null && getReferenceDescriptor().getOptimisticLockingPolicy() != null) {
postCalculateChanges(record, (UnitOfWorkImpl)uow);
}
return record;
}
/**
* INTERNAL:
* Extract the source primary key value from the target row.
* Used for batch reading, most following same order and fields as in the mapping.
*/
protected Vector extractSourceKeyFromRow(AbstractRecord row, AbstractSession session) {
int size = sourceKeyFields.size();
Vector key = new Vector(size);
ConversionManager conversionManager = session.getDatasourcePlatform().getConversionManager();
for (int index = 0; index < size; index++) {
DatabaseField targetField = targetForeignKeyFields.get(index);
DatabaseField sourceField = sourceKeyFields.get(index);
Object value = row.get(targetField);
// Must ensure the classification gets a cache hit.
try {
value = conversionManager.convertObject(value, sourceField.getType());
} catch (ConversionException e) {
throw ConversionException.couldNotBeConverted(this, getDescriptor(), e);
}
key.addElement(value);
}
return key;
}
/**
* INTERNAL:
*/
@Override
public boolean isOwned(){
return true;
}
/**
* INTERNAL:
*/
@Override
public boolean isUnidirectionalOneToManyMapping() {
return true;
}
/**
* INTERNAL:
* Initialize the mapping.
*/
@Override
public void initialize(AbstractSession session) throws DescriptorException {
super.initialize(session);
if (getReferenceDescriptor().getOptimisticLockingPolicy() != null) {
if (this.shouldIncrementTargetLockValueOnAddOrRemoveTarget) {
this.descriptor.addMappingsPostCalculateChanges(this);
}
if (this.shouldIncrementTargetLockValueOnDeleteSource && !this.isPrivateOwned) {
this.descriptor.addMappingsPostCalculateChangesOnDeleted(this);
}
}
}
/**
* Initialize the type of the target foreign key, as it will be null as it is not mapped in the target.
*/
@Override
public void postInitialize(AbstractSession session) {
super.postInitialize(session);
Iterator<DatabaseField> targetForeignKeys = getTargetForeignKeyFields().iterator();
Iterator<DatabaseField> sourceKeys = getSourceKeyFields().iterator();
while (targetForeignKeys.hasNext()) {
DatabaseField targetForeignKey = targetForeignKeys.next();
DatabaseField sourcePrimaryKey = sourceKeys.next();
if (targetForeignKey.getType() == null) {
DatabaseMapping mapping = getDescriptor().getObjectBuilder().getMappingForField(sourcePrimaryKey);
// If we have a mapping, set the type, otherwise at this point
// there is not much more we can do. This case will likely hit
// when we have a UnidirectionalOneToManyMapping on an aggregate
// outside of JPA. Within JPA, in most cases, the metadata
// processing should set the type on the targetForeignKey for us.
// Bug 278263 has been entered to revisit this code.
if (mapping != null) {
targetForeignKey.setType(mapping.getFieldClassification(sourcePrimaryKey));
}
}
}
}
/**
* INTERNAL:
*/
@Override
protected AbstractRecord createModifyRowForAddTargetQuery() {
AbstractRecord modifyRow = super.createModifyRowForAddTargetQuery();
int size = targetForeignKeyFields.size();
for (int index = 0; index < size; index++) {
DatabaseField targetForeignKey = targetForeignKeyFields.get(index);
modifyRow.put(targetForeignKey, null);
}
return modifyRow;
}
/**
* INTERNAL:
* Delete the reference objects.
*/
@Override
public void preDelete(DeleteObjectQuery query) throws DatabaseException, OptimisticLockException {
if (shouldObjectModifyCascadeToParts(query)) {
super.preDelete(query);
} else {
updateTargetRowPreDeleteSource(query);
}
}
/**
* Prepare a cascade locking policy.
*/
@Override
public void prepareCascadeLockingPolicy() {
CascadeLockingPolicy policy = new CascadeLockingPolicy(getDescriptor(), getReferenceDescriptor());
policy.setQueryKeyFields(getSourceKeysToTargetForeignKeys());
policy.setShouldHandleUnmappedFields(true);
getReferenceDescriptor().addCascadeLockingPolicy(policy);
}
/**
* INTERNAL:
* Overridden by mappings that require additional processing of the change record after the record has been calculated.
*/
@Override
public void postCalculateChanges(org.eclipse.persistence.sessions.changesets.ChangeRecord changeRecord, UnitOfWorkImpl uow) {
// targets are added to and/or removed to/from the source.
CollectionChangeRecord collectionChangeRecord = (CollectionChangeRecord)changeRecord;
Iterator it = collectionChangeRecord.getAddObjectList().values().iterator();
while(it.hasNext()) {
ObjectChangeSet change = (ObjectChangeSet)it.next();
if(!change.hasChanges()) {
change.setShouldModifyVersionField(Boolean.TRUE);
((org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet)change.getUOWChangeSet()).addObjectChangeSet(change, uow, false);
}
}
// in the mapping is privately owned then the target will be deleted - no need to modify target version.
it = collectionChangeRecord.getRemoveObjectList().values().iterator();
while(it.hasNext()) {
ObjectChangeSet change = (ObjectChangeSet)it.next();
if (!isPrivateOwned()){
if(!change.hasChanges()) {
change.setShouldModifyVersionField(Boolean.TRUE);
((org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet)change.getUOWChangeSet()).addObjectChangeSet(change, uow, false);
}
}else{
containerPolicy.postCalculateChanges(change, referenceDescriptor, this, uow);
}
}
}
/**
* INTERNAL:
* Overridden by mappings that require objects to be deleted contribute to change set creation.
*/
@Override
public void postCalculateChangesOnDeleted(Object deletedObject, UnitOfWorkChangeSet uowChangeSet, UnitOfWorkImpl uow) {
// the source is deleted:
// trigger the indirection - we have to get optimistic lock exception
// in case another thread has updated one of the targets:
// triggered indirection caches the target with the old version,
// then the version update waits until the other thread (which is locking the version field) commits,
// then the version update is executed and it throws optimistic lock exception.
Object col = getRealCollectionAttributeValueFromObject(deletedObject, uow);
if (col != null) {
Object iterator = this.containerPolicy.iteratorFor(col);
while (this.containerPolicy.hasNext(iterator)) {
Object target = this.containerPolicy.next(iterator, uow);
ObjectChangeSet change = this.referenceDescriptor.getObjectBuilder().createObjectChangeSet(target, uowChangeSet, uow);
if (!change.hasChanges()) {
change.setShouldModifyVersionField(Boolean.TRUE);
((org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet)change.getUOWChangeSet()).addObjectChangeSet(change, uow, false);
}
}
}
}
/**
* INTERNAL:
* Add additional fields
*/
@Override
protected void postPrepareNestedBatchQuery(ReadQuery batchQuery, ObjectLevelReadQuery query) {
super.postPrepareNestedBatchQuery(batchQuery, query);
ReadAllQuery mappingBatchQuery = (ReadAllQuery)batchQuery;
int size = this.targetForeignKeyFields.size();
for (int i=0; i < size; i++) {
mappingBatchQuery.addAdditionalField(this.targetForeignKeyFields.get(i));
}
}
/**
* 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.
int size = sourceKeyFields.size();
for(int i=0; i < size; i++) {
DatabaseField sourceKey = sourceKeyFields.get(i);
if (!translationRow.containsKey(sourceKey)) {
Object value = descriptor.getObjectBuilder().extractValueFromObjectForField(object, sourceKey, session);
translationRow.put(sourceKey, value);
}
}
}
/**
* INTERNAL:
* Overridden by mappings that require additional processing of the change record after the record has been calculated.
*/
@Override
public void recordPrivateOwnedRemovals(Object object, UnitOfWorkImpl uow) {
//need private owned check for this mapping as this method is called for any mapping
// that also registers a postCalculateChanges() method. Most mappings only register the
// postCalculateChanges if they are privately owned. This Mapping is a special case an
// always registers a postCalculateChanges mapping when the target has OPT locking.
if (isPrivateOwned){
super.recordPrivateOwnedRemovals(object, uow);
}
}
/**
* INTERNAL:
* UnidirectionalOneToManyMapping performs some events after INSERT/UPDATE to maintain the keys
*/
@Override
public boolean requiresDataModificationEvents(){
return true;
}
/**
* PUBLIC:
* Set value that indicates whether target's optimistic locking value should be incremented on
* target being added to / removed from a source (default value is true).
**/
public void setShouldIncrementTargetLockValueOnAddOrRemoveTarget(boolean shouldIncrementTargetLockValueOnAddOrRemoveTarget) {
this.shouldIncrementTargetLockValueOnAddOrRemoveTarget = shouldIncrementTargetLockValueOnAddOrRemoveTarget;
}
/**
* PUBLIC:
* Set value that indicates whether target's optimistic locking value should be incremented on
* the source deletion (default value is true).
**/
public void setShouldIncrementTargetLockValueOnDeleteSource(boolean shouldIncrementTargetLockValueOnDeleteSource) {
this.shouldIncrementTargetLockValueOnDeleteSource = shouldIncrementTargetLockValueOnDeleteSource;
}
/**
* PUBLIC:
* Indicates whether target's optimistic locking value should be incremented on
* target being added to / removed from a source (default value is true).
**/
public boolean shouldIncrementTargetLockValueOnAddOrRemoveTarget() {
return shouldIncrementTargetLockValueOnAddOrRemoveTarget;
}
/**
* PUBLIC:
* Indicates whether target's optimistic locking value should be incremented on
* the source deletion (default value is true).
**/
public boolean shouldIncrementTargetLockValueOnDeleteSource() {
return shouldIncrementTargetLockValueOnDeleteSource;
}
/**
* INTERNAL
* Target foreign key of the removed object should be modified (set to null).
*/
@Override
protected boolean shouldRemoveTargetQueryModifyTargetForeignKey() {
return true;
}
@Override
public boolean shouldDeferInsert() {
if (shouldDeferInserts == null) {
String property = PrivilegedAccessHelper.getSystemProperty(SystemProperties.ONETOMANY_DEFER_INSERTS);
shouldDeferInserts = true;
if (property != null) {
shouldDeferInserts = "true".equalsIgnoreCase(property);
} else {
for (DatabaseField f : targetForeignKeyFields) {
if (!f.isNullable()) {
shouldDeferInserts = false;
break;
}
}
}
}
return shouldDeferInserts;
}
}