You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openjpa.apache.org by de...@apache.org on 2009/12/11 21:06:19 UTC
svn commit: r889793 - in /openjpa/branches/1.1.x:
openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/
openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/
openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/
openjpa-persiste...
Author: dezzio
Date: Fri Dec 11 20:06:18 2009
New Revision: 889793
URL: http://svn.apache.org/viewvc?rev=889793&view=rev
Log:
OpenJPA-817: Applied Ravi Palacherla's patch to 1.1.x branch. Also made ConstraintUpdateManager's EOL native.
Added:
openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java (with props)
openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java (with props)
openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java (with props)
openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java (with props)
Modified:
openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java (contents, props changed)
openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java
openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java
Modified: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java?rev=889793&r1=889792&r2=889793&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java (original)
+++ openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java Fri Dec 11 20:06:18 2009
@@ -1,546 +1,546 @@
-/*
- * 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.Iterator;
-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);
-
- public boolean orderDirty() {
- return false;
- }
-
- protected PreparedStatementManager newPreparedStatementManager
- (JDBCStore store, Connection conn) {
- return new PreparedStatementManagerImpl(store, conn);
- }
-
- protected RowManager newRowManager() {
- return new RowManagerImpl(false);
- }
-
- 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 inserts = rmimpl.getInserts();
- Collection updates = rmimpl.getUpdates();
- Collection 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 (Iterator itr = inserts.iterator(); itr.hasNext();) {
- sm = ((Row) itr.next()).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 (Iterator itr = deletes.iterator(); itr.hasNext();) {
- row = (PrimaryRow) itr.next();
- 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 (int j = 0; j < fks.length; j++) {
- // 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(fks[j]);
- if (fkVal == null)
- fkVal = row.getForeignKeyWhere(fks[j]);
- if (fkVal == null)
- continue;
-
- row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
- Row.ACTION_DELETE, fkVal, false);
- if (row2 != null && row2.isValid() && row2 != row)
- graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row,
- fks[j]);
- }
- }
-
- 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 (Iterator itr = rows.iterator(); itr.hasNext();) {
- row = (PrimaryRow) itr.next();
- 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 (int j = 0; j < fks.length; j++) {
- if (row.getForeignKeySet(fks[j]) == 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(fks[j].getPrimaryKeyTable(),
- Row.ACTION_INSERT, row.getForeignKeySet(fks[j]), false);
- if (row2 != null && row2.isValid() && (row2 != row
- || fks[j].isDeferred() || fks[j].isLogical()))
- graph = addEdge(graph, row, (PrimaryRow) row2, fks[j]);
- }
-
- // see if there are any relation id columns dependent on
- // auto-inc objects
- cols = row.getTable().getRelationIdColumns();
- for (int j = 0; j < cols.length; j++) {
- OpenJPAStateManager sm = row.getRelationIdSet(cols[j]);
- 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, cols[j]);
- }
- }
- 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
- flush(deleteUpdates, psMgr);
- Collection nodes = dfa.getSortedNodes();
- for (Iterator itr = nodes.iterator(); itr.hasNext();)
- psMgr.flush((RowImpl) itr.next());
- flush(insertUpdates, psMgr);
- }
-
- /**
- * 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 (Iterator iter = cycle.iterator(); iter.hasNext(); ) {
- Edge edge = (Edge) iter.next();
- 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 (Iterator itr = edges.iterator(); itr.hasNext();) {
- Edge edge = (Edge) itr.next();
- 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 (Iterator itr = rows.iterator(); itr.hasNext(); ) {
- row = (RowImpl) itr.next();
- if (row.isValid() && !row.isDependent())
- psMgr.flush(row);
- }
- }
+/*
+ * 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.Iterator;
+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);
+
+ public boolean orderDirty() {
+ return true;
+ }
+
+ protected PreparedStatementManager newPreparedStatementManager
+ (JDBCStore store, Connection conn) {
+ return new PreparedStatementManagerImpl(store, conn);
+ }
+
+ protected RowManager newRowManager() {
+ return new RowManagerImpl(false);
+ }
+
+ 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 inserts = rmimpl.getInserts();
+ Collection updates = rmimpl.getUpdates();
+ Collection 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 (Iterator itr = inserts.iterator(); itr.hasNext();) {
+ sm = ((Row) itr.next()).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 (Iterator itr = deletes.iterator(); itr.hasNext();) {
+ row = (PrimaryRow) itr.next();
+ 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 (int j = 0; j < fks.length; j++) {
+ // 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(fks[j]);
+ if (fkVal == null)
+ fkVal = row.getForeignKeyWhere(fks[j]);
+ if (fkVal == null)
+ continue;
+
+ row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
+ Row.ACTION_DELETE, fkVal, false);
+ if (row2 != null && row2.isValid() && row2 != row)
+ graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row,
+ fks[j]);
+ }
+ }
+
+ 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 (Iterator itr = rows.iterator(); itr.hasNext();) {
+ row = (PrimaryRow) itr.next();
+ 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 (int j = 0; j < fks.length; j++) {
+ if (row.getForeignKeySet(fks[j]) == 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(fks[j].getPrimaryKeyTable(),
+ Row.ACTION_INSERT, row.getForeignKeySet(fks[j]), false);
+ if (row2 != null && row2.isValid() && (row2 != row
+ || fks[j].isDeferred() || fks[j].isLogical()))
+ graph = addEdge(graph, row, (PrimaryRow) row2, fks[j]);
+ }
+
+ // see if there are any relation id columns dependent on
+ // auto-inc objects
+ cols = row.getTable().getRelationIdColumns();
+ for (int j = 0; j < cols.length; j++) {
+ OpenJPAStateManager sm = row.getRelationIdSet(cols[j]);
+ 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, cols[j]);
+ }
+ }
+ 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
+ flush(deleteUpdates, psMgr);
+ Collection nodes = dfa.getSortedNodes();
+ for (Iterator itr = nodes.iterator(); itr.hasNext();)
+ psMgr.flush((RowImpl) itr.next());
+ flush(insertUpdates, psMgr);
+ }
+
+ /**
+ * 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 (Iterator iter = cycle.iterator(); iter.hasNext(); ) {
+ Edge edge = (Edge) iter.next();
+ 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 (Iterator itr = edges.iterator(); itr.hasNext();) {
+ Edge edge = (Edge) itr.next();
+ 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 (Iterator itr = rows.iterator(); itr.hasNext(); ) {
+ row = (RowImpl) itr.next();
+ if (row.isValid() && !row.isDependent())
+ psMgr.flush(row);
+ }
+ }
}
\ No newline at end of file
Propchange: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ConstraintUpdateManager.java
------------------------------------------------------------------------------
svn:eol-style = native
Modified: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java?rev=889793&r1=889792&r2=889793&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java (original)
+++ openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/RowManagerImpl.java Fri Dec 11 20:06:18 2009
@@ -22,7 +22,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -39,13 +39,13 @@
public class RowManagerImpl
implements RowManager {
- private Map _inserts = null;
- private Map _updates = null;
- private Map _deletes = null;
- private Collection _secondaryUpdates = null;
- private Collection _secondaryDeletes = null;
- private Collection _allRowUpdates = null;
- private Collection _allRowDeletes = null;
+ private Map<Key, PrimaryRow> _inserts = null;
+ private Map<Key, PrimaryRow> _updates = null;
+ private Map<Key, PrimaryRow> _deletes = null;
+ private Collection<SecondaryRow> _secondaryUpdates = null;
+ private Collection<SecondaryRow> _secondaryDeletes = null;
+ private Collection<Row> _allRowUpdates = null;
+ private Collection<Row> _allRowDeletes = null;
// we maintain a list of the order of all primary rows if the user
// wants to be able to fetch them in order
@@ -66,7 +66,7 @@
* @param order whether to keep track of the order in which rows are added
*/
public RowManagerImpl(boolean order) {
- _primaryOrder = (order) ? new ArrayList() : null;
+ _primaryOrder = (order) ? new ArrayList<PrimaryRow>() : null;
}
/**
@@ -80,61 +80,98 @@
* Return the ordered primary rows. Only available if ordering requested
* on construction.
*/
- public List getOrdered() {
- return (_primaryOrder == null) ? Collections.EMPTY_LIST : _primaryOrder;
+ public List<PrimaryRow> getOrdered() {
+ if(_primaryOrder == null ) {
+ return Collections.emptyList();
+ }
+ else {
+ return _primaryOrder;
+ }
}
/**
* Return all inserted primary rows.
*/
- public Collection getInserts() {
- return (_inserts == null) ? Collections.EMPTY_LIST : _inserts.values();
+ public Collection<PrimaryRow> getInserts() {
+ if(_inserts == null ) {
+ return Collections.emptyList();
+ }
+ else {
+ return _inserts.values();
+ }
}
/**
* Return all updated primary rows.
*/
- public Collection getUpdates() {
- return (_updates == null) ? Collections.EMPTY_LIST : _updates.values();
+ public Collection<PrimaryRow> getUpdates() {
+ if(_updates == null ){
+ return Collections.emptyList();
+ }
+ else {
+ return _updates.values();
+ }
}
/**
* Return all deleted primary rows.
*/
- public Collection getDeletes() {
- return (_deletes == null) ? Collections.EMPTY_LIST : _deletes.values();
+ public Collection<PrimaryRow> getDeletes() {
+ if(_deletes == null) {
+ return Collections.emptyList();
+ }
+ else {
+ return _deletes.values();
+ }
}
/**
* Return all inserted and updated secondary rows.
*/
- public Collection getSecondaryUpdates() {
- return (_secondaryUpdates == null) ? Collections.EMPTY_LIST
- : _secondaryUpdates;
+ public Collection<SecondaryRow> getSecondaryUpdates() {
+ if(_secondaryUpdates == null) {
+ return Collections.emptyList();
+ }
+ else {
+ return _secondaryUpdates;
+ }
}
/**
* Return all deleted secondary rows.
*/
- public Collection getSecondaryDeletes() {
- return (_secondaryDeletes == null) ? Collections.EMPTY_LIST
- : _secondaryDeletes;
+ public Collection<SecondaryRow> getSecondaryDeletes() {
+ if(_secondaryDeletes == null) {
+ return Collections.emptyList();
+ }
+ else {
+ return _secondaryDeletes;
+ }
}
/**
* Return any 'all row' updates.
*/
- public Collection getAllRowUpdates() {
- return (_allRowUpdates == null) ? Collections.EMPTY_LIST
- : _allRowUpdates;
+ public Collection<Row> getAllRowUpdates() {
+ if(_allRowUpdates == null) {
+ return Collections.emptyList();
+ }
+ else {
+ return _allRowUpdates;
+ }
}
/**
* Return any 'all row' deletes.
*/
- public Collection getAllRowDeletes() {
- return (_allRowDeletes == null) ? Collections.EMPTY_LIST
- : _allRowDeletes;
+ public Collection<Row> getAllRowDeletes() {
+ if(_allRowDeletes == null) {
+ return Collections.emptyList();
+ }
+ else {
+ return _allRowDeletes;
+ }
+
}
public Row getSecondaryRow(Table table, int action) {
@@ -149,12 +186,12 @@
SecondaryRow srow = (SecondaryRow) row;
if (srow.getAction() == Row.ACTION_DELETE) {
if (_secondaryDeletes == null)
- _secondaryDeletes = new ArrayList();
- _secondaryDeletes.add(srow.clone());
+ _secondaryDeletes = new ArrayList<SecondaryRow>();
+ _secondaryDeletes.add((SecondaryRow) srow.clone());
} else {
if (_secondaryUpdates == null)
- _secondaryUpdates = new ArrayList();
- _secondaryUpdates.add(srow.clone());
+ _secondaryUpdates = new ArrayList<SecondaryRow>();
+ _secondaryUpdates.add((SecondaryRow) srow.clone());
}
}
@@ -169,12 +206,12 @@
switch (row.getAction()) {
case Row.ACTION_UPDATE:
if (_allRowUpdates == null)
- _allRowUpdates = new ArrayList();
+ _allRowUpdates = new ArrayList<Row>();
_allRowUpdates.add(row);
break;
case Row.ACTION_DELETE:
if (_allRowDeletes == null)
- _allRowDeletes = new ArrayList();
+ _allRowDeletes = new ArrayList<Row>();
_allRowDeletes.add(row);
break;
default:
@@ -192,25 +229,25 @@
&& _row != null && _row.getAction() == action)
return _row;
- Map map;
+ Map<Key, PrimaryRow> map;
if (action == Row.ACTION_DELETE) {
if (_deletes == null && create)
- _deletes = new HashMap();
+ _deletes = new LinkedHashMap<Key, PrimaryRow>();
map = _deletes;
} else if (action == Row.ACTION_INSERT) {
if (_inserts == null && create)
- _inserts = new HashMap();
+ _inserts = new LinkedHashMap<Key, PrimaryRow>();
map = _inserts;
} else {
if (_updates == null && create)
- _updates = new HashMap();
+ _updates = new LinkedHashMap<Key, PrimaryRow>();
map = _updates;
}
if (map == null)
return null;
_key = new Key(table, sm);
- _row = (PrimaryRow) map.get(_key);
+ _row = map.get(_key);
if (_row == null && create) {
_row = new PrimaryRow(table, action, sm);
Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,83 @@
+/*
+ * 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.util.Collection;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.Version;
+
+@Entity
+// Try not to collide with other Employee entities
+@Table(name = "PER_JDBC_KERN_EMP")
+public class Employee {
+
+ @Id
+ private int id;
+
+ @Version
+ private int version;
+
+ private String firstName;
+ private String lastName;
+
+ @OneToMany(mappedBy = "employee", cascade = { CascadeType.MERGE,
+ CascadeType.PERSIST })
+ private Collection<Task> tasks;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public Collection<Task> getTasks() {
+ return tasks;
+ }
+
+ public void setTasks(Collection<Task> tasks) {
+ this.tasks = tasks;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+}
Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Employee.java
------------------------------------------------------------------------------
svn:eol-style = native
Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,60 @@
+/*
+ * 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 javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.Version;
+
+@Entity
+@Table(name = "PER_JDBC_KERN_STORY") // try not to collide
+public class Story {
+ @Id
+ private int id;
+
+ @Version
+ private int version;
+
+ @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
+ private Task task;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public Task getTask() {
+ return task;
+ }
+
+ public void setTask(Task task) {
+ this.task = task;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+}
Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Story.java
------------------------------------------------------------------------------
svn:eol-style = native
Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,75 @@
+/*
+ * 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.util.Collection;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.Version;
+
+@Entity
+@Table(name = "PER_JDBC_KERN_TASK")
+// try not to collide
+public class Task {
+ @Id
+ private int id;
+
+ @Version
+ private int version;
+
+ @OneToMany(mappedBy = "task", cascade = { CascadeType.MERGE,
+ CascadeType.PERSIST })
+ private Collection<Story> stories;
+
+ @ManyToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST })
+ private Employee employee;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public Collection<Story> getStories() {
+ return stories;
+ }
+
+ public void setStories(Collection<Story> stories) {
+ this.stories = stories;
+ }
+
+ public Employee getEmployee() {
+ return employee;
+ }
+
+ public void setEmployee(Employee employee) {
+ this.employee = employee;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+}
Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/Task.java
------------------------------------------------------------------------------
svn:eol-style = native
Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java?rev=889793&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java Fri Dec 11 20:06:18 2009
@@ -0,0 +1,116 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collection;
+
+import javax.persistence.EntityManager;
+
+import org.apache.openjpa.persistence.test.SQLListenerTestCase;
+
+/**
+ * Test that insert order is preserved when using the ConstraintUpdateManager
+ * for entities which are not annotated with ForeignKey constraints.
+ */
+public class TestInsertOrder extends SQLListenerTestCase {
+ private String empTableName;
+ private String taskTableName;
+ private String storyTableName;
+
+ public void setUp() {
+ setUp(Employee.class, Task.class, Story.class);
+ empTableName = getMapping(Employee.class).getTable().getFullName();
+ taskTableName = getMapping(Task.class).getTable().getFullName();
+ storyTableName = getMapping(Story.class).getTable().getFullName();
+ }
+
+ /**
+ * <P>Persist an Employee entity and allow the cascade to insert the
+ * children. The inserts should be executed in this order, Employee, Task,
+ * Story.
+ * </P>
+ *
+ * <P>
+ * Originally this test would pass in some scenarios. I believe the order
+ * relied on the hashcode of the underlying entities.
+ * </P>
+ */
+ public void testCascadePersist() {
+ Employee e = newTree(10);
+ EntityManager em = emf.createEntityManager();
+ em.getTransaction().begin();
+ em.persist(e);
+ em.getTransaction().commit();
+ em.close();
+
+ assertAllSQLInOrder("INSERT INTO " + empTableName + ".*", "INSERT INTO "
+ + taskTableName + ".*", "INSERT INTO " + storyTableName + ".*");
+ }
+
+ /**
+ * Merge an Employee entity and allow the cascade to insert the children.
+ * The inserts should be executed in this order, Employee, Task, Story.
+ */
+ public void testCascadeMerge() {
+ Employee e = newTree(11);
+ EntityManager em = emf.createEntityManager();
+ em.getTransaction().begin();
+ em.merge(e);
+ em.getTransaction().commit();
+ em.close();
+
+ assertAllSQLInOrder("INSERT INTO " + empTableName + ".*", "INSERT INTO "
+ + taskTableName + ".*", "INSERT INTO " + storyTableName + ".*");
+ }
+
+
+ /**
+ * Helper to create a tree of entities
+ *
+ * @param id
+ * ID for the entities.
+ * @return an unmanaged Employee instance with the appropriate relationships
+ * set.
+ */
+ private Employee newTree(int id) {
+ Employee e = new Employee();
+ e.setId(id);
+
+ Task t = new Task();
+ t.setId(id);
+
+ Story s = new Story();
+ s.setId(id);
+
+ Collection<Task> tasks = new ArrayList<Task>();
+ tasks.add(t);
+
+ Collection<Story> stories = new ArrayList<Story>();
+ stories.add(s);
+
+ e.setTasks(tasks);
+ t.setEmployee(e);
+
+ t.setStories(stories);
+ s.setTask(t);
+
+ return e;
+ }
+}
Propchange: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/kernel/TestInsertOrder.java
------------------------------------------------------------------------------
svn:eol-style = native
Modified: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java?rev=889793&r1=889792&r2=889793&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java (original)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/test/SQLListenerTestCase.java Fri Dec 11 20:06:18 2009
@@ -20,7 +20,6 @@
import java.util.List;
import java.util.ArrayList;
-import java.util.Map;
import org.apache.openjpa.lib.jdbc.AbstractJDBCListener;
import org.apache.openjpa.lib.jdbc.JDBCEvent;
@@ -34,6 +33,7 @@
public abstract class SQLListenerTestCase
extends SingleEMFTestCase {
+ private static String _nl = System.getProperty("line.separator");
protected List<String> sql = new ArrayList<String>();
protected int sqlCount;
@@ -95,6 +95,47 @@
}
/**
+ * Confirm the list of expected SQL expressions have been executed in the
+ * order specified. I.e. additional SQL statements can be executed in
+ * between expected SQLs.
+ *
+ * @param expected
+ * SQL expressions. E.g., ("SELECT FOO .*", "UPDATE .*")
+ */
+ public void assertAllSQLInOrder(String... expected) {
+ assertSQLInOrder(false, expected);
+ }
+
+ private void assertSQLInOrder(boolean exact, String... expected) {
+ boolean match = false;
+ int sqlSize = sql.size();
+ if (expected.length <= sqlSize) {
+ int hits = 0;
+ for (String executedSQL : sql) {
+ if (executedSQL.matches(expected[hits])) {
+ if (++hits == expected.length)
+ break;
+ }
+ }
+ match = hits == (exact ? sqlSize : expected.length);
+ }
+
+ if (!match) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Did not find SQL in expected order : ").append(_nl);
+ for (String s : expected) {
+ sb.append(s).append(_nl);
+ }
+
+ sb.append("SQL Statements issued : ");
+ for (String s : sql) {
+ sb.append(s).append(_nl);
+ }
+ fail(sb.toString());
+ }
+ }
+
+ /**
* Gets the number of SQL issued since last reset.
*/
public int getSQLCount() {
@@ -122,4 +163,28 @@
}
}
}
+
+ public void assertSQLOrder(String... expected) {
+ int hits = 0;
+
+ for (String executedSQL : sql) {
+ if (executedSQL.matches(expected[hits])) {
+ hits++;
+ }
+ }
+
+ if (hits != expected.length) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Did not find SQL in expected order : ").append(_nl);
+ for (String s : expected) {
+ sb.append(s).append(_nl);
+ }
+
+ sb.append("SQL Statements issued : ");
+ for (String s : sql) {
+ sb.append(s).append(_nl);
+ }
+ fail(sb.toString());
+ }
+ }
}