blob: 050d01dcc30a110a77eb304d974f276b6b83097e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.jdbc.kernel;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.sql.PrimaryRow;
import org.apache.openjpa.jdbc.sql.Row;
import org.apache.openjpa.jdbc.sql.RowImpl;
import org.apache.openjpa.jdbc.sql.RowManager;
import org.apache.openjpa.jdbc.sql.RowManagerImpl;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.lib.graph.DepthFirstAnalysis;
import org.apache.openjpa.lib.graph.Edge;
import org.apache.openjpa.lib.graph.Graph;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.UserException;
/**
* <p>Standard update manager, capable of foreign key constraint evaluation.</p>
*
* @since 1.0.0
*/
public class ConstraintUpdateManager
extends AbstractUpdateManager {
private static final Localizer _loc = Localizer.forPackage
(ConstraintUpdateManager.class);
@Override
public boolean orderDirty() {
return true;
}
@Override
protected PreparedStatementManager newPreparedStatementManager
(JDBCStore store, Connection conn) {
return new PreparedStatementManagerImpl(store, conn);
}
@Override
protected RowManager newRowManager() {
return new RowManagerImpl(false);
}
@Override
protected Collection flush(RowManager rowMgr,
PreparedStatementManager psMgr, Collection exceps) {
RowManagerImpl rmimpl = (RowManagerImpl) rowMgr;
// first take care of all secondary table deletes and 'all row' deletes
// (which are probably secondary table deletes), since no foreign
// keys ever rely on secondary table pks
flush(rmimpl.getAllRowDeletes(), psMgr);
flush(rmimpl.getSecondaryDeletes(), psMgr);
// now do any 'all row' updates
flush(rmimpl.getAllRowUpdates(), psMgr);
// analyze foreign keys
Collection<PrimaryRow> inserts = rmimpl.getInserts();
Collection<PrimaryRow> updates = rmimpl.getUpdates();
Collection<PrimaryRow> deletes = rmimpl.getDeletes();
Graph[] graphs = new Graph[2]; // insert graph, delete graph
analyzeForeignKeys(inserts, updates, deletes, rmimpl, graphs);
// flush insert graph, if any
boolean autoAssign = rmimpl.hasAutoAssignConstraints();
try {
flushGraph(graphs[0], psMgr, autoAssign);
} catch (SQLException se) {
exceps = addException(exceps, SQLExceptions.getStore(se, dict));
} catch (OpenJPAException ke) {
exceps = addException(exceps, ke);
}
// flush the rest of the inserts and updates; inserts before updates
// because some update fks might reference pks that have to be inserted
flush(inserts, psMgr);
flush(updates, psMgr);
// flush the delete graph, if any
try {
flushGraph(graphs[1], psMgr, autoAssign);
} catch (SQLException se) {
exceps = addException(exceps, SQLExceptions.getStore(se, dict));
} catch (OpenJPAException ke) {
exceps = addException(exceps, ke);
}
// put the remainder of the deletes after updates because some updates
// may be nulling fks to rows that are going to be deleted
flush(deletes, psMgr);
// take care of all secondary table inserts and updates last, since
// they may rely on previous inserts or updates, but nothing relies
// on them
flush(rmimpl.getSecondaryUpdates(), psMgr);
// flush any left over prepared statements
psMgr.flush();
return exceps;
}
/**
* Analyze foreign key dependencies on the given rows
* and create an insert and a delete graph to execute. The insert
* graph will be flushed before all other rows, and the delete graph will
* be flushed after them.
*/
private void analyzeForeignKeys(Collection inserts, Collection updates,
Collection deletes, RowManagerImpl rowMgr, Graph[] graphs) {
// if there are any deletes, we have to map the insert objects on their
// oids so we'll be able to detect delete-then-insert-same-pk cases
Map insertMap = null;
OpenJPAStateManager sm;
if (!deletes.isEmpty() && !inserts.isEmpty()) {
insertMap = new HashMap((int) (inserts.size() * 1.33 + 1));
for (Object insert : inserts) {
sm = ((Row) insert).getPrimaryKey();
if (sm != null && sm.getObjectId() != null)
insertMap.put(sm.getObjectId(), sm);
}
}
// first construct the graph for deletes; this may expand to include
// inserts and updates as well if there are any inserts that rely on
// deletes (delete-then-insert-same-pk cases)
PrimaryRow row;
Row row2;
ForeignKey[] fks;
OpenJPAStateManager fkVal;
boolean ignoreUpdates = true;
for (Object delete : deletes) {
row = (PrimaryRow) delete;
if (!row.isValid())
continue;
row2 = getInsertRow(insertMap, rowMgr, row);
if (row2 != null) {
ignoreUpdates = false;
graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row, null);
}
// now check this row's fks against other deletes
fks = row.getTable().getForeignKeys();
for (ForeignKey fk : fks) {
// when deleting ref fks they'll just set a where value, so
// check both for fk updates (relation fks) and wheres (ref fks)
fkVal = row.getForeignKeySet(fk);
if (fkVal == null)
fkVal = row.getForeignKeyWhere(fk);
if (fkVal == null)
continue;
row2 = rowMgr.getRow(fk.getPrimaryKeyTable(),
Row.ACTION_DELETE, fkVal, false);
if (row2 != null && row2.isValid() && row2 != row)
graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row,
fk);
}
}
if (ignoreUpdates)
graphs[0] = analyzeAgainstInserts(inserts, rowMgr, graphs[0]);
else {
// put inserts *and updates* in the delete graph; they all rely
// on each other
graphs[1] = analyzeAgainstInserts(updates, rowMgr, graphs[1]);
graphs[1] = analyzeAgainstInserts(inserts, rowMgr, graphs[1]);
}
}
/**
* Check to see if there is an insert for for the same table and primary
* key values as the given delete row.
*/
private Row getInsertRow(Map insertMap, RowManagerImpl rowMgr, Row row) {
if (insertMap == null)
return null;
OpenJPAStateManager sm = row.getPrimaryKey();
if (sm == null)
return null;
// look for a new object whose insert id is the same as this delete one
Object oid = sm.getObjectId();
OpenJPAStateManager nsm = (OpenJPAStateManager) insertMap.get(oid);
if (nsm == null)
return null;
// found new object; get its row
row = rowMgr.getRow(row.getTable(), Row.ACTION_INSERT, nsm, false);
return (row == null || row.isValid()) ? row : null;
}
/**
* Analyze the given rows against the inserts, placing dependencies
* in the given graph.
*/
private Graph analyzeAgainstInserts(Collection rows, RowManagerImpl rowMgr,
Graph graph) {
PrimaryRow row;
Row row2;
ForeignKey[] fks;
Column[] cols;
for (Object o : rows) {
row = (PrimaryRow) o;
if (!row.isValid())
continue;
// check this row's fks against inserts; a logical fk to an auto-inc
// column is treated just as actual database fk because the result
// is the same: the pk row has to be inserted before the fk row
fks = row.getTable().getForeignKeys();
for (ForeignKey fk : fks) {
if (row.getForeignKeySet(fk) == null)
continue;
// see if this row is dependent on another. if it's only
// depenent on itself, see if the fk is logical or deferred, in
// which case it must be an auto-inc because otherwise we
// wouldn't have recorded it
row2 = rowMgr.getRow(fk.getPrimaryKeyTable(),
Row.ACTION_INSERT, row.getForeignKeySet(fk), false);
if (row2 != null && row2.isValid() && (row2 != row
|| fk.isDeferred() || fk.isLogical()))
graph = addEdge(graph, row, (PrimaryRow) row2, fk);
}
// see if there are any relation id columns dependent on
// auto-inc objects
cols = row.getTable().getRelationIdColumns();
for (Column col : cols) {
OpenJPAStateManager sm = row.getRelationIdSet(col);
if (sm == null)
continue;
row2 = rowMgr.getRow(getBaseTable(sm), Row.ACTION_INSERT,
sm, false);
if (row2 != null && row2.isValid())
graph = addEdge(graph, row, (PrimaryRow) row2, col);
}
}
return graph;
}
/**
* Return the base table for the given instance.
*/
private static Table getBaseTable(OpenJPAStateManager sm) {
ClassMapping cls = (ClassMapping) sm.getMetaData();
while (cls.getJoinablePCSuperclassMapping() != null)
cls = cls.getJoinablePCSuperclassMapping();
return cls.getTable();
}
/**
* Add an edge between the given rows in the given foreign key graph.
*/
private Graph addEdge(Graph graph, PrimaryRow row1, PrimaryRow row2,
Object fk) {
// delay creation of the graph
if (graph == null)
graph = new Graph();
row1.setDependent(true);
row2.setDependent(true);
graph.addNode(row1);
graph.addNode(row2);
// add an edge from row1 to row2, and set the fk causing the
// dependency as the user object so we can retrieve it when resolving
// circular constraints
Edge edge = new Edge(row1, row2, true);
edge.setUserObject(fk);
graph.addEdge(edge);
return graph;
}
/**
* Flush the given graph of rows in the proper order.
* @param graph The graph of statements to be walked
* @param psMgr The prepared statement manager to use to issue the
* statements
* @param autoAssign Whether any of the rows in the graph have any
* auto-assign constraints
*/
protected void flushGraph(Graph graph, PreparedStatementManager psMgr,
boolean autoAssign)
throws SQLException {
if (graph == null)
return;
DepthFirstAnalysis dfa = newDepthFirstAnalysis(graph, autoAssign);
Collection insertUpdates = new LinkedList();
Collection deleteUpdates = new LinkedList();
boolean recalculate;
// Handle circular constraints:
// - if deleted row A has a ciricular fk to deleted row B,
// then use an update statement to null A's fk to B before flushing,
// and then flush
// - if inserted row A has a circular fk to updated/inserted row B,
// then null the fk in the B row object, then flush,
// and after flushing, use an update to set the fk back to A
// Depending on where circular dependencies are broken, the
// topological order of the graph nodes has to be re-calculated.
recalculate = resolveCycles(graph, dfa.getEdges(Edge.TYPE_BACK),
deleteUpdates, insertUpdates);
recalculate |= resolveCycles(graph, dfa.getEdges(Edge.TYPE_FORWARD),
deleteUpdates, insertUpdates);
if (recalculate) {
dfa = recalculateDepthFirstAnalysis(graph, autoAssign);
}
// flush delete updates to null fks, then all rows in order, then
// the insert updates to set circular fk values
Collection nodes = dfa.getSortedNodes();
flush(deleteUpdates, nodes, psMgr);
flush(insertUpdates, psMgr);
}
protected void flush(Collection deleteUpdates, Collection nodes, PreparedStatementManager psMgr) {
flush(deleteUpdates, psMgr);
for (Object node : nodes) {
psMgr.flush((RowImpl) node);
}
}
/**
* Break a circular dependency caused by delete operations.
* If deleted row A has a ciricular fk to deleted row B, then use an update
* statement to null A's fk to B before deleting B, then delete A.
* @param edge Edge in the dependency graph corresponding to a foreign key
* constraint. This dependency is broken by nullifying the foreign key.
* @param deleteUpdates Collection of update statements that are executed
* before the delete operations are flushed
*/
private void addDeleteUpdate(Edge edge, Collection deleteUpdates)
throws SQLException {
PrimaryRow row;
RowImpl update;
ForeignKey fk;
// copy where conditions into new update that nulls the fk
row = (PrimaryRow) edge.getTo();
update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
row.copyInto(update, true);
if (edge.getUserObject() instanceof ForeignKey) {
fk = (ForeignKey) edge.getUserObject();
update.setForeignKey(fk, row.getForeignKeyIO(fk), null);
} else
update.setNull((Column) edge.getUserObject());
deleteUpdates.add(update);
}
/**
* Break a circular dependency caused by insert operations.
* If inserted row A has a circular fk to updated/inserted row B,
* then null the fk in the B row object, then flush,
* and after flushing, use an update to set the fk back to A.
* @param row Row to be flushed
* @param edge Edge in the dependency graph corresponding to a foreign key
* constraint. This dependency is broken by nullifying the foreign key.
* @param insertUpdates Collection of update statements that are executed
* after the insert/update operations are flushed
*/
private void addInsertUpdate(PrimaryRow row, Edge edge,
Collection insertUpdates) throws SQLException {
RowImpl update;
ForeignKey fk;
Column col;
// copy where conditions into new update that sets the fk
update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
if (row.getAction() == Row.ACTION_INSERT) {
if (row.getPrimaryKey() == null)
throw new InternalException(_loc.get("ref-cycle"));
update.wherePrimaryKey(row.getPrimaryKey());
} else {
// Row.ACTION_UPDATE
row.copyInto(update, true);
}
if (edge.getUserObject() instanceof ForeignKey) {
fk = (ForeignKey) edge.getUserObject();
update.setForeignKey(fk, row.getForeignKeyIO(fk),
row.getForeignKeySet(fk));
row.clearForeignKey(fk);
} else {
col = (Column) edge.getUserObject();
update.setRelationId(col, row.getRelationIdSet(col),
row.getRelationIdCallback(col));
row.clearRelationId(col);
}
insertUpdates.add(update);
}
/**
* Finds a nullable foreign key by walking the dependency cycle.
* Circular dependencies can be broken at this point.
* @param cycle Cycle in the dependency graph.
* @return Edge corresponding to a nullable foreign key.
*/
private Edge findBreakableLink(List cycle) {
Edge breakableLink = null;
for (Object o : cycle) {
Edge edge = (Edge) o;
Object userObject = edge.getUserObject();
if (userObject instanceof ForeignKey) {
if (!((ForeignKey) userObject).hasNotNullColumns()) {
breakableLink = edge;
break;
}
}
else if (userObject instanceof Column) {
if (!((Column) userObject).isNotNull()) {
breakableLink = edge;
break;
}
}
}
return breakableLink;
}
/**
* Re-calculates the DepthFirstSearch analysis of the graph
* after some of the edges have been removed. Ensures
* that the dependency graph is cycle free.
* @param graph The graph of statements to be walked
* @param autoAssign Whether any of the rows in the graph have any
* auto-assign constraints
*/
private DepthFirstAnalysis recalculateDepthFirstAnalysis(Graph graph,
boolean autoAssign) {
DepthFirstAnalysis dfa;
// clear previous traversal data
graph.clearTraversal();
dfa = newDepthFirstAnalysis(graph, autoAssign);
// make sure that the graph is non-cyclic now
assert (dfa.hasNoCycles()): _loc.get("graph-not-cycle-free");
return dfa;
}
/**
* Resolve circular dependencies by identifying and breaking
* a nullable foreign key.
* @param graph Dependency graph.
* @param edges Collection of edges. Each edge indicates a possible
* circular dependency
* @param deleteUpdates Collection of update operations (nullifying
* foreign keys) to be filled. These updates will be executed before
* the rows in the dependency graph are flushed
* @param insertUpdates CCollection of update operations (nullifying
* foreign keys) to be filled. These updates will be executed after
* the rows in the dependency graph are flushed
* @return Depending on where circular dependencies are broken, the
* topological order of the graph nodes has to be re-calculated.
*/
private boolean resolveCycles(Graph graph, Collection edges,
Collection deleteUpdates, Collection insertUpdates)
throws SQLException {
boolean recalculate = false;
for (Object o : edges) {
Edge edge = (Edge) o;
List cycle = edge.getCycle();
if (cycle != null) {
// find a nullable foreign key
Edge breakableLink = findBreakableLink(cycle);
if (breakableLink == null) {
throw new UserException(_loc.get("no-nullable-fk"));
}
// topologic node order must be re-calculated, if the
// breakable link is different from the edge where
// the circular dependency was originally detected
if (edge != breakableLink) {
recalculate = true;
}
if (!breakableLink.isRemovedFromGraph()) {
// use a primary row update to prevent setting pk and fk
// values until after flush, to get latest auto-increment
// values
PrimaryRow row = (PrimaryRow) breakableLink.getFrom();
if (row.getAction() == Row.ACTION_DELETE) {
addDeleteUpdate(breakableLink, deleteUpdates);
}
else {
addInsertUpdate(row, breakableLink, insertUpdates);
}
graph.removeEdge(breakableLink);
}
}
}
return recalculate;
}
/**
* Create a new {@link DepthFirstAnalysis} suitable for the given graph
* and auto-assign settings.
*/
protected DepthFirstAnalysis newDepthFirstAnalysis(Graph graph,
boolean autoAssign) {
return new DepthFirstAnalysis(graph);
}
/**
* Flush the given collection of secondary rows.
*/
protected void flush(Collection rows, PreparedStatementManager psMgr) {
if (rows.size() == 0)
return;
RowImpl row;
for (Object o : rows) {
row = (RowImpl) o;
if (!row.isFlushed() && row.isValid() && !row.isDependent()) {
psMgr.flush(row);
row.setFlushed(true);
}
}
}
}