blob: c0910c139656980f491598aef5dea80bb4a02906 [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
// 09/12/2018 - Will Dazey
// - 391279: Add support for Unidirectional OneToMany mappings with non-nullable values
package org.eclipse.persistence.internal.sessions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.eclipse.persistence.internal.databaseaccess.DatasourceCall;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.helper.DescriptorCompare;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.localization.ToStringLocalization;
import org.eclipse.persistence.internal.queries.DatabaseQueryMechanism;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.queries.DeleteObjectQuery;
import org.eclipse.persistence.queries.InsertObjectQuery;
import org.eclipse.persistence.queries.UpdateObjectQuery;
import org.eclipse.persistence.queries.WriteObjectQuery;
import org.eclipse.persistence.sessions.UnitOfWork.CommitOrderType;
/**
* This class maintains a commit stack and resolves circular references.
*/
public class CommitManager {
/** Order based on mapping foreign key constraints on how to insert objects by class. */
protected List<Class> commitOrder;
/**
* This tracks the commit state for the objects, PENDING, PRE, POST, COMPLETE.
* The key is the object and the value is the state.
*/
protected Map<Object, Integer> commitState;
/** The commit is in progress, but the row has not been written. */
protected static final Integer PRE = 1;
/** The commit is in progress, and the row has been written. */
protected static final Integer POST = 2;
/** The commit is complete for the object. */
protected static final Integer COMPLETE = 3;
/** This object should be ignored. */
protected static final Integer IGNORE = 4;
/** Set of objects that had partial row written to resolve constraints. */
protected Map shallowCommits;
protected AbstractSession session;
/** The commit manager is active while writing a set of objects (UOW), it is not active when writing a single object (DB session). */
protected boolean isActive;
/** Map of modification events used to defer insertion into m-m, dc, join tables. */
protected Map<DatabaseMapping, List<Object[]>> dataModifications;
/**
* Map of deferred calls groups by their table.
* This is used to defer multiple table writes for batching and deadlock avoidance.
*/
protected Map<DatabaseTable, List<Object[]>> deferredCalls;
/** List of orphaned objects pending deletion. */
protected List objectsToDelete;
/** Counter used to keep track of commit depth for non-UOW writes. */
protected int commitDepth;
/**
* Create the commit manager on the session.
* It must be initialized later on after the descriptors have been added.
*/
public CommitManager(AbstractSession session) {
this.session = session;
}
/**
* Add the data query to be performed at the end of the commit.
* This is done to decrease dependencies and avoid deadlock.
*/
public void addDataModificationEvent(DatabaseMapping mapping, Object[] event) {
if (!getDataModifications().containsKey(mapping)) {
this.dataModifications.put(mapping, new ArrayList());
}
this.dataModifications.get(mapping).add(event);
}
/**
* Add the data query to be performed at the end of the commit.
* This is done to decrease dependencies and avoid deadlock.
*/
public void addDeferredCall(DatabaseTable table, DatasourceCall call, DatabaseQueryMechanism mechanism) {
if (!getDeferredCalls().containsKey(table)) {
this.deferredCalls.put(table, new ArrayList());
}
Object[] arguments = new Object[2];
arguments[0] = call;
arguments[1] = mechanism;
this.deferredCalls.get(table).add(arguments);
}
/**
* Deletion are cached until the end.
*/
public void addObjectToDelete(Object objectToDelete) {
getObjectsToDelete().add(objectToDelete);
}
/**
* Commit all of the objects as a single transaction.
* This should commit the object in the correct order to maintain referential integrity.
*/
public void commitAllObjectsWithChangeSet(UnitOfWorkChangeSet uowChangeSet) throws RuntimeException, DatabaseException, OptimisticLockException {
reinitialize();
this.isActive = true;
this.session.beginTransaction();
try {
// PERF: if the number of classes in the project is large this loop can be a perf issue.
// If only one class types changed, then avoid loop.
if ((uowChangeSet.getObjectChanges().size() + uowChangeSet.getNewObjectChangeSets().size()) <= 1) {
Iterator<Class> classes = uowChangeSet.getNewObjectChangeSets().keySet().iterator();
if (classes.hasNext()) {
Class theClass = classes.next();
commitNewObjectsForClassWithChangeSet(uowChangeSet, theClass);
}
classes = uowChangeSet.getObjectChanges().keySet().iterator();
if (classes.hasNext()) {
Class theClass = classes.next();
commitChangedObjectsForClassWithChangeSet(uowChangeSet, theClass);
}
} else {
// The commit order is all of the classes ordered by dependencies, this is done for deadlock avoidance.
List<Class> commitOrder = getCommitOrder();
int size = commitOrder.size();
for (int index = 0; index < size; index++) {
Class theClass = commitOrder.get(index);
commitAllObjectsForClassWithChangeSet(uowChangeSet, theClass);
}
}
if (hasDeferredCalls()) {
// Perform all batched up calls, done to avoid dependencies.
for (List<Object[]> calls: this.deferredCalls.values()) {
for (Object[] argument : calls) {
((DatabaseQueryMechanism)argument[1]).executeDeferredCall((DatasourceCall)argument[0]);
}
}
}
if (hasDataModifications()) {
// Perform all batched up data modifications, done to avoid dependencies.
for (Map.Entry<DatabaseMapping, List<Object[]>> entry: this.dataModifications.entrySet()) {
List<Object[]> events = entry.getValue();
int size = events.size();
DatabaseMapping mapping = entry.getKey();
for (int index = 0; index < size; index++) {
Object[] event = events.get(index);
mapping.performDataModificationEvent(event, getSession());
}
}
}
if (hasObjectsToDelete()) {
// These are orphaned objects, to be deleted from private ownership updates.
// TODO: These should be added to the unit of work deleted so they are deleted in the correct order.
List objects = getObjectsToDelete();
int size = objects.size();
reinitialize();
for (int index = 0; index < size; index++) {
this.session.deleteObject(objects.get(index));
}
}
this.session.commitTransaction();
} catch (RuntimeException exception) {
this.session.rollbackTransaction();
throw exception;
} finally {
reinitialize();
this.isActive = false;
}
}
/**
* Commit all of the objects of the class type in the change set.
* This allows for the order of the classes to be processed optimally.
*/
protected void commitAllObjectsForClassWithChangeSet(UnitOfWorkChangeSet uowChangeSet, Class theClass) {
// Although new objects should be first, there is an issue that new objects get added to non-new after the insert,
// so the object would be written twice.
commitChangedObjectsForClassWithChangeSet(uowChangeSet, theClass);
commitNewObjectsForClassWithChangeSet(uowChangeSet, theClass);
}
/**
* Commit all of the objects of the class type in the change set.
* This allows for the order of the classes to be processed optimally.
*/
protected void commitNewObjectsForClassWithChangeSet(UnitOfWorkChangeSet uowChangeSet, Class theClass) {
Map<ObjectChangeSet, ObjectChangeSet> newObjectChangesList = uowChangeSet.getNewObjectChangeSets().get(theClass);
if (newObjectChangesList != null) { // may be no changes for that class type.
AbstractSession session = getSession();
ClassDescriptor descriptor = session.getDescriptor(theClass);
List<ObjectChangeSet> newChangeSets = new ArrayList(newObjectChangesList.values());
int size = newChangeSets.size();
for (int index = 0; index < size; index++) {
ObjectChangeSet changeSetToWrite = newChangeSets.get(index);
Object objectToWrite = changeSetToWrite.getUnitOfWorkClone();
if (!isProcessedCommit(objectToWrite)) {
// PERF: Get the descriptor query, to avoid extra query creation.
InsertObjectQuery commitQuery = descriptor.getQueryManager().getInsertQuery();
if (commitQuery == null) {
commitQuery = new InsertObjectQuery();
commitQuery.setDescriptor(descriptor);
} else {
// Ensure original query has been prepared.
commitQuery.checkPrepare(session, commitQuery.getTranslationRow());
commitQuery = (InsertObjectQuery)commitQuery.clone();
}
commitQuery.setIsExecutionClone(true);
commitQuery.setObjectChangeSet(changeSetToWrite);
commitQuery.setObject(objectToWrite);
commitQuery.cascadeOnlyDependentParts();
commitQuery.setModifyRow(null);
session.executeQuery(commitQuery);
}
uowChangeSet.putNewObjectInChangesList(changeSetToWrite, session);
}
}
}
/**
* Commit changed of the objects of the class type in the change set.
* This allows for the order of the classes to be processed optimally.
*/
protected void commitChangedObjectsForClassWithChangeSet(UnitOfWorkChangeSet uowChangeSet, Class theClass) {
Map<ObjectChangeSet, ObjectChangeSet> objectChangesList = uowChangeSet.getObjectChanges().get(theClass);
if (objectChangesList != null) {// may be no changes for that class type.
ClassDescriptor descriptor = null;
AbstractSession session = getSession();
Collection<ObjectChangeSet> changes = objectChangesList.values();
CommitOrderType order = ((UnitOfWorkImpl)session).getCommitOrder();
if (order != CommitOrderType.NONE) {
changes = new ArrayList(objectChangesList.values());
if (order == CommitOrderType.CHANGES) {
Collections.sort((List)changes, new ObjectChangeSet.ObjectChangeSetComparator());
} else {
Collections.sort((List)changes);
}
}
for (ObjectChangeSet changeSetToWrite : changes) {
Object objectToWrite = changeSetToWrite.getUnitOfWorkClone();
if (descriptor == null) {
descriptor = session.getDescriptor(objectToWrite);
}
if (!isProcessedCommit(objectToWrite)) {
// Commit and resume on failure can cause a new change set to be in existing, so need to check here.
WriteObjectQuery commitQuery = null;
if (changeSetToWrite.isNew()) {
commitQuery = new InsertObjectQuery();
} else {
commitQuery = new UpdateObjectQuery();
}
commitQuery.setIsExecutionClone(true);
commitQuery.setDescriptor(descriptor);
commitQuery.setObjectChangeSet(changeSetToWrite);
commitQuery.setObject(objectToWrite);
commitQuery.cascadeOnlyDependentParts();
// removed checking session type to set cascade level
// will always be a unitOfWork so we need to cascade dependent parts
session.executeQuery(commitQuery);
}
}
}
}
/**
* delete all of the objects as a single transaction.
* This should delete the object in the correct order to maintain referential integrity.
*/
public void deleteAllObjects(List objects) throws RuntimeException, DatabaseException, OptimisticLockException {
this.isActive = true;
AbstractSession session = getSession();
session.beginTransaction();
try {
// PERF: Optimize single object case.
if (objects.size() == 1) {
deleteAllObjects(objects.get(0).getClass(), objects, session);
} else {
List<Class> commitOrder = getCommitOrder();
for (int orderIndex = commitOrder.size() - 1; orderIndex >= 0; orderIndex--) {
Class theClass = commitOrder.get(orderIndex);
deleteAllObjects(theClass, objects, session);
}
}
session.commitTransaction();
} catch (RuntimeException exception) {
try {
session.rollbackTransaction();
} catch (Exception ignore) {
}
throw exception;
} finally {
reinitialize();
this.isActive = false;
}
}
/**
* Delete all of the objects with the matching class.
*/
public void deleteAllObjects(Class theClass, List objects, AbstractSession session) {
ClassDescriptor descriptor = null;
if (((UnitOfWorkImpl)session).getCommitOrder() != CommitOrderType.NONE) {// bug 331064 - Sort the delete order
objects = sort(theClass, objects);
}
int size = objects.size();
for (int index = 0; index < size; index++) {
Object objectToDelete = objects.get(index);
if (objectToDelete.getClass() == theClass) {
if (descriptor == null) {
descriptor = session.getDescriptor(theClass);
}
// PERF: Get the descriptor query, to avoid extra query creation.
DeleteObjectQuery deleteQuery = descriptor.getQueryManager().getDeleteQuery();
if (deleteQuery == null) {
deleteQuery = new DeleteObjectQuery();
deleteQuery.setDescriptor(descriptor);
} else {
// Ensure original query has been prepared.
deleteQuery.checkPrepare(session, deleteQuery.getTranslationRow());
deleteQuery = (DeleteObjectQuery)deleteQuery.clone();
}
deleteQuery.setIsExecutionClone(true);
deleteQuery.setObject(objectToDelete);
session.executeQuery(deleteQuery);
}
}
}
/**
* Sort the objects based on PK.
*/
// bug 331064 - Sort the delete order based on PKs.
private List sort (Class theClass, List objects) {
ClassDescriptor descriptor = session.getDescriptor(theClass);
org.eclipse.persistence.internal.descriptors.ObjectBuilder objectBuilder = descriptor.getObjectBuilder();
int size = objects.size();
TreeMap sortedObjects = new TreeMap();
for (int index = 0; index < size; index++) {
Object objectToDelete = objects.get(index);
if (objectToDelete.getClass() == theClass) {
sortedObjects.put(objectBuilder.extractPrimaryKeyFromObject(objectToDelete, session), objectToDelete);
}
}
return new ArrayList(sortedObjects.values());
}
/**
* Return the order in which objects should be committed to the database.
* This order is based on ownership in the descriptors and is require for referential integrity.
* The commit order is a vector of vectors,
* where the first vector is all root level classes, the second is classes owned by roots and so on.
*/
public List<Class> getCommitOrder() {
if (this.commitOrder == null) {
this.commitOrder = new ArrayList();
}
return this.commitOrder;
}
/**
* Return the map of states of the objects being committed.
* The states are defined as static Integers (PENDING, PRE, POST, COMPLETE).
*/
protected Map<Object, Integer> getCommitState() {
if (this.commitState == null) {
// 2612538 - the default size of Map (32) is appropriate
this.commitState = new IdentityHashMap();
}
return this.commitState;
}
protected boolean hasDataModifications() {
return ((this.dataModifications != null) && (!this.dataModifications.isEmpty()));
}
/**
* Used to store data queries to be performed at the end of the commit.
* This is done to decrease dependencies and avoid deadlock.
*/
protected Map<DatabaseMapping, List<Object[]>> getDataModifications() {
if (dataModifications == null) {
dataModifications = new LinkedHashMap();
}
return dataModifications;
}
protected boolean hasDeferredCalls() {
return ((this.deferredCalls != null) && (!this.deferredCalls.isEmpty()));
}
/**
* Used to store calls to be performed at the end of the commit.
* This is done for multiple table descriptors to allow batching and avoid deadlock.
*/
protected Map<DatabaseTable, List<Object[]>> getDeferredCalls() {
if (this.deferredCalls == null) {
this.deferredCalls = new LinkedHashMap();
}
return this.deferredCalls;
}
protected boolean hasObjectsToDelete() {
return ((objectsToDelete != null) && (!objectsToDelete.isEmpty()));
}
/**
* Deletion are cached until the end.
*/
public List getObjectsToDelete() {
if (objectsToDelete == null) {
objectsToDelete = new ArrayList();
}
return objectsToDelete;
}
/**
* Return the session that this is managing commits for.
*/
protected AbstractSession getSession() {
return this.session;
}
/**
* Return any objects that have been shallow committed during this commit process.
*/
protected Map getShallowCommits() {
if (this.shallowCommits == null) {
// 2612538 - the default size of Map (32) is appropriate
this.shallowCommits = new IdentityHashMap();
}
return this.shallowCommits;
}
/**
* Reset the commit order from the session's descriptors.
* This uses the constraint dependencies in the descriptor's mappings,
* to decide which descriptors are dependent on which other descriptors.
* Multiple computations of the commit order should produce the same ordering.
* This is done to improve performance on unit of work writes through decreasing the
* stack size, and acts as a deadlock avoidance mechanism.
*/
public void initializeCommitOrder() {
Vector descriptors = Helper.buildVectorFromMapElements(getSession().getDescriptors());
// Must ensure uniqueness, some descriptor my be register twice for interfaces.
descriptors = Helper.addAllUniqueToVector(new Vector(descriptors.size()), descriptors);
Object[] descriptorsArray = new Object[descriptors.size()];
for (int index = 0; index < descriptors.size(); index++) {
descriptorsArray[index] = descriptors.elementAt(index);
}
Arrays.sort(descriptorsArray, new DescriptorCompare());
descriptors = new Vector(descriptors.size());
for (int index = 0; index < descriptorsArray.length; index++) {
descriptors.addElement(descriptorsArray[index]);
}
CommitOrderCalculator calculator = new CommitOrderCalculator(getSession());
calculator.addNodes(descriptors);
calculator.calculateMappingDependencies();
calculator.orderCommits();
descriptors = calculator.getOrderedDescriptors();
calculator = new CommitOrderCalculator(getSession());
calculator.addNodes(descriptors);
calculator.calculateSpecifiedDependencies();
calculator.orderCommits();
setCommitOrder(calculator.getOrderedClasses());
}
/**
* Return if the commit manager is active.
*/
public boolean isActive() {
return isActive;
}
/**
* Return if the object has been processed.
* This should be called by any query that is writing an object,
* if true the query should not write the object.
*/
public boolean isProcessedCommit(Object object) {
return getCommitState().get(object) != null;
}
/**
* Return if the object has been committed.
* This should be called by any query that is writing an object,
* if true the query should not write the object.
*/
public boolean isCommitCompleted(Object object) {
return getCommitState().get(object) == COMPLETE;
}
/**
* Return if the object has been committed.
* This should be called by any query that is writing an object,
* if true the query should not write the object.
*/
public boolean isCommitCompletedInPostOrIgnore(Object object) {
Integer state = getCommitState().get(object);
return (state == COMPLETE) || (state == POST) || (state == IGNORE);
}
/**
* Return if the object is being in progress of being post modify commit.
* This should be called by any query that is writing an object.
*/
public boolean isCommitInPostModify(Object object) {
return getCommitState().get(object) == POST;
}
/**
* Return if the object is being in progress of being pre modify commit.
* This should be called by any query that is writing an object,
* if true the query must force a shallow insert of the object if it is new.
*/
public boolean isCommitInPreModify(Object objectOrChangeSet) {
return getCommitState().get(objectOrChangeSet) == PRE;
}
/**
* Return if the object is shallow committed.
* This is required to resolve bidirectional references.
*/
public boolean isShallowCommitted(Object object) {
if (this.shallowCommits == null) {
return false;
}
return this.shallowCommits.containsKey(object);
}
/**
* Mark the commit of the object as being fully completed.
* This should be called by any query that has finished writing an object.
*/
public void markCommitCompleted(Object object) {
this.commitDepth --;
getCommitState().put(object, COMPLETE);
// If not in a unit of work commit and the commit of this object is done reset the commit manager.
if ((!this.isActive) && (this.commitDepth == 0)) {
reinitialize();
return;
}
}
public void markIgnoreCommit(Object object){
getCommitState().put(object, IGNORE);
}
/**
* Add an object as being in progress of being committed.
* This should be called by any query that is writing an object.
*/
public void markPostModifyCommitInProgress(Object object) {
getCommitState().put(object, POST);
}
/**
* Add an object as being in progress of being committed.
* This should be called by any query that is writing an object.
*/
public void markPreModifyCommitInProgress(Object object) {
this.commitDepth ++;
getCommitState().put(object, PRE);
}
/**
* Mark the object as shallow committed.
* This is required to resolve bidirectional references.
*/
public void markShallowCommit(Object object) {
getShallowCommits().put(object, object); // Use as set.
}
/**
* Reset the commits.
* This must be done before a new commit process is begun.
*/
public void reinitialize() {
this.commitState = null;
this.commitDepth = 0;
this.shallowCommits = null;
this.objectsToDelete = null;
this.dataModifications = null;
this.deferredCalls = null;
}
/**
* Set the order in which objects should be committed to the database.
* This order is based on ownership in the descriptors and is require for referential integrity.
* The commit order is a vector of vectors,
* where the first vector is all root level classes, the second is classes owned by roots and so on.
*/
public void setCommitOrder(List commitOrder) {
this.commitOrder = commitOrder;
}
/**
* Used to store data queries to be performed at the end of the commit.
* This is done to decrease dependencies and avoid deadlock.
*/
protected void setDataModifications(Map<DatabaseMapping, List<Object[]>> dataModifications) {
this.dataModifications = dataModifications;
}
/**
* Set if the commit manager is active.
*/
public void setIsActive(boolean isActive) {
this.isActive = isActive;
}
/**
* Deletion are cached until the end.
*/
protected void setObjectsToDelete(List objectsToDelete) {
this.objectsToDelete = objectsToDelete;
}
/**
* Set the session that this is managing commits for.
*/
protected void setSession(AbstractSession session) {
this.session = session;
}
/**
* Set any objects that have been shallow committed during this commit process.
*/
protected void setShallowCommits(Map shallowCommits) {
this.shallowCommits = shallowCommits;
}
/**
* Print the in progress depth.
*/
@Override
public String toString() {
Object[] args = {this.commitDepth};
return Helper.getShortClassName(getClass()) + ToStringLocalization.buildMessage("commit_depth", args);
}
}