1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.openjpa.jdbc.kernel;
20
21 import java.sql.Connection;
22 import java.sql.SQLException;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.Map;
29
30 import org.apache.openjpa.jdbc.meta.ClassMapping;
31 import org.apache.openjpa.jdbc.schema.Column;
32 import org.apache.openjpa.jdbc.schema.ForeignKey;
33 import org.apache.openjpa.jdbc.schema.Table;
34 import org.apache.openjpa.jdbc.sql.PrimaryRow;
35 import org.apache.openjpa.jdbc.sql.Row;
36 import org.apache.openjpa.jdbc.sql.RowImpl;
37 import org.apache.openjpa.jdbc.sql.RowManager;
38 import org.apache.openjpa.jdbc.sql.RowManagerImpl;
39 import org.apache.openjpa.jdbc.sql.SQLExceptions;
40 import org.apache.openjpa.kernel.OpenJPAStateManager;
41 import org.apache.openjpa.lib.graph.DepthFirstAnalysis;
42 import org.apache.openjpa.lib.graph.Edge;
43 import org.apache.openjpa.lib.graph.Graph;
44 import org.apache.openjpa.lib.util.Localizer;
45 import org.apache.openjpa.util.InternalException;
46 import org.apache.openjpa.util.OpenJPAException;
47 import org.apache.openjpa.util.UserException;
48
49 /**
50 * <p>Standard update manager, capable of foreign key constraint evaluation.</p>
51 *
52 * @since 1.0.0
53 */
54 public class ConstraintUpdateManager
55 extends AbstractUpdateManager {
56
57 private static final Localizer _loc = Localizer.forPackage
58 (ConstraintUpdateManager.class);
59
60 public boolean orderDirty() {
61 return false;
62 }
63
64 protected PreparedStatementManager newPreparedStatementManager
65 (JDBCStore store, Connection conn) {
66 return new PreparedStatementManagerImpl(store, conn);
67 }
68
69 protected RowManager newRowManager() {
70 return new RowManagerImpl(false);
71 }
72
73 protected Collection flush(RowManager rowMgr,
74 PreparedStatementManager psMgr, Collection exceps) {
75 RowManagerImpl rmimpl = (RowManagerImpl) rowMgr;
76
77 // first take care of all secondary table deletes and 'all row' deletes
78 // (which are probably secondary table deletes), since no foreign
79 // keys ever rely on secondary table pks
80 flush(rmimpl.getAllRowDeletes(), psMgr);
81 flush(rmimpl.getSecondaryDeletes(), psMgr);
82
83 // now do any 'all row' updates
84 flush(rmimpl.getAllRowUpdates(), psMgr);
85
86 // analyze foreign keys
87 Collection inserts = rmimpl.getInserts();
88 Collection updates = rmimpl.getUpdates();
89 Collection deletes = rmimpl.getDeletes();
90 Graph[] graphs = new Graph[2]; // insert graph, delete graph
91 analyzeForeignKeys(inserts, updates, deletes, rmimpl, graphs);
92
93 // flush insert graph, if any
94 boolean autoAssign = rmimpl.hasAutoAssignConstraints();
95 try {
96 flushGraph(graphs[0], psMgr, autoAssign);
97 } catch (SQLException se) {
98 exceps = addException(exceps, SQLExceptions.getStore(se, dict));
99 } catch (OpenJPAException ke) {
100 exceps = addException(exceps, ke);
101 }
102
103 // flush the rest of the inserts and updates; inserts before updates
104 // because some update fks might reference pks that have to be inserted
105 flush(inserts, psMgr);
106 flush(updates, psMgr);
107
108 // flush the delete graph, if any
109 try {
110 flushGraph(graphs[1], psMgr, autoAssign);
111 } catch (SQLException se) {
112 exceps = addException(exceps, SQLExceptions.getStore(se, dict));
113 } catch (OpenJPAException ke) {
114 exceps = addException(exceps, ke);
115 }
116
117 // put the remainder of the deletes after updates because some updates
118 // may be nulling fks to rows that are going to be deleted
119 flush(deletes, psMgr);
120
121 // take care of all secondary table inserts and updates last, since
122 // they may rely on previous inserts or updates, but nothing relies
123 // on them
124 flush(rmimpl.getSecondaryUpdates(), psMgr);
125
126 // flush any left over prepared statements
127 psMgr.flush();
128 return exceps;
129 }
130
131 /**
132 * Analyze foreign key dependencies on the given rows
133 * and create an insert and a delete graph to execute. The insert
134 * graph will be flushed before all other rows, and the delete graph will
135 * be flushed after them.
136 */
137 private void analyzeForeignKeys(Collection inserts, Collection updates,
138 Collection deletes, RowManagerImpl rowMgr, Graph[] graphs) {
139 // if there are any deletes, we have to map the insert objects on their
140 // oids so we'll be able to detect delete-then-insert-same-pk cases
141 Map insertMap = null;
142 OpenJPAStateManager sm;
143 if (!deletes.isEmpty() && !inserts.isEmpty()) {
144 insertMap = new HashMap((int) (inserts.size() * 1.33 + 1));
145 for (Iterator itr = inserts.iterator(); itr.hasNext();) {
146 sm = ((Row) itr.next()).getPrimaryKey();
147 if (sm != null && sm.getObjectId() != null)
148 insertMap.put(sm.getObjectId(), sm);
149 }
150 }
151
152 // first construct the graph for deletes; this may expand to include
153 // inserts and updates as well if there are any inserts that rely on
154 // deletes (delete-then-insert-same-pk cases)
155 PrimaryRow row;
156 Row row2;
157 ForeignKey[] fks;
158 OpenJPAStateManager fkVal;
159 boolean ignoreUpdates = true;
160 for (Iterator itr = deletes.iterator(); itr.hasNext();) {
161 row = (PrimaryRow) itr.next();
162 if (!row.isValid())
163 continue;
164
165 row2 = getInsertRow(insertMap, rowMgr, row);
166 if (row2 != null) {
167 ignoreUpdates = false;
168 graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row, null);
169 }
170
171 // now check this row's fks against other deletes
172 fks = row.getTable().getForeignKeys();
173 for (int j = 0; j < fks.length; j++) {
174 // when deleting ref fks they'll just set a where value, so
175 // check both for fk updates (relation fks) and wheres (ref fks)
176 fkVal = row.getForeignKeySet(fks[j]);
177 if (fkVal == null)
178 fkVal = row.getForeignKeyWhere(fks[j]);
179 if (fkVal == null)
180 continue;
181
182 row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
183 Row.ACTION_DELETE, fkVal, false);
184 if (row2 != null && row2.isValid() && row2 != row)
185 graphs[1] = addEdge(graphs[1], (PrimaryRow) row2, row,
186 fks[j]);
187 }
188 }
189
190 if (ignoreUpdates)
191 graphs[0] = analyzeAgainstInserts(inserts, rowMgr, graphs[0]);
192 else {
193 // put inserts *and updates* in the delete graph; they all rely
194 // on each other
195 graphs[1] = analyzeAgainstInserts(updates, rowMgr, graphs[1]);
196 graphs[1] = analyzeAgainstInserts(inserts, rowMgr, graphs[1]);
197 }
198 }
199
200 /**
201 * Check to see if there is an insert for for the same table and primary
202 * key values as the given delete row.
203 */
204 private Row getInsertRow(Map insertMap, RowManagerImpl rowMgr, Row row) {
205 if (insertMap == null)
206 return null;
207
208 OpenJPAStateManager sm = row.getPrimaryKey();
209 if (sm == null)
210 return null;
211
212 // look for a new object whose insert id is the same as this delete one
213 Object oid = sm.getObjectId();
214 OpenJPAStateManager nsm = (OpenJPAStateManager) insertMap.get(oid);
215 if (nsm == null)
216 return null;
217
218 // found new object; get its row
219 row = rowMgr.getRow(row.getTable(), Row.ACTION_INSERT, nsm, false);
220 return (row == null || row.isValid()) ? row : null;
221 }
222
223 /**
224 * Analyze the given rows against the inserts, placing dependencies
225 * in the given graph.
226 */
227 private Graph analyzeAgainstInserts(Collection rows, RowManagerImpl rowMgr,
228 Graph graph) {
229 PrimaryRow row;
230 Row row2;
231 ForeignKey[] fks;
232 Column[] cols;
233 for (Iterator itr = rows.iterator(); itr.hasNext();) {
234 row = (PrimaryRow) itr.next();
235 if (!row.isValid())
236 continue;
237
238 // check this row's fks against inserts; a logical fk to an auto-inc
239 // column is treated just as actual database fk because the result
240 // is the same: the pk row has to be inserted before the fk row
241 fks = row.getTable().getForeignKeys();
242 for (int j = 0; j < fks.length; j++) {
243 if (row.getForeignKeySet(fks[j]) == null)
244 continue;
245
246 // see if this row is dependent on another. if it's only
247 // depenent on itself, see if the fk is logical or deferred, in
248 // which case it must be an auto-inc because otherwise we
249 // wouldn't have recorded it
250 row2 = rowMgr.getRow(fks[j].getPrimaryKeyTable(),
251 Row.ACTION_INSERT, row.getForeignKeySet(fks[j]), false);
252 if (row2 != null && row2.isValid() && (row2 != row
253 || fks[j].isDeferred() || fks[j].isLogical()))
254 graph = addEdge(graph, row, (PrimaryRow) row2, fks[j]);
255 }
256
257 // see if there are any relation id columns dependent on
258 // auto-inc objects
259 cols = row.getTable().getRelationIdColumns();
260 for (int j = 0; j < cols.length; j++) {
261 OpenJPAStateManager sm = row.getRelationIdSet(cols[j]);
262 if (sm == null)
263 continue;
264
265 row2 = rowMgr.getRow(getBaseTable(sm), Row.ACTION_INSERT,
266 sm, false);
267 if (row2 != null && row2.isValid())
268 graph = addEdge(graph, row, (PrimaryRow) row2, cols[j]);
269 }
270 }
271 return graph;
272 }
273
274 /**
275 * Return the base table for the given instance.
276 */
277 private static Table getBaseTable(OpenJPAStateManager sm) {
278 ClassMapping cls = (ClassMapping) sm.getMetaData();
279 while (cls.getJoinablePCSuperclassMapping() != null)
280 cls = cls.getJoinablePCSuperclassMapping();
281 return cls.getTable();
282 }
283
284 /**
285 * Add an edge between the given rows in the given foreign key graph.
286 */
287 private Graph addEdge(Graph graph, PrimaryRow row1, PrimaryRow row2,
288 Object fk) {
289 // delay creation of the graph
290 if (graph == null)
291 graph = new Graph();
292
293 row1.setDependent(true);
294 row2.setDependent(true);
295 graph.addNode(row1);
296 graph.addNode(row2);
297
298 // add an edge from row1 to row2, and set the fk causing the
299 // dependency as the user object so we can retrieve it when resolving
300 // circular constraints
301 Edge edge = new Edge(row1, row2, true);
302 edge.setUserObject(fk);
303 graph.addEdge(edge);
304
305 return graph;
306 }
307
308 /**
309 * Flush the given graph of rows in the proper order.
310 * @param graph The graph of statements to be walked
311 * @param psMgr The prepared statement manager to use to issue the
312 * statements
313 * @param autoAssign Whether any of the rows in the graph have any
314 * auto-assign constraints
315 */
316 protected void flushGraph(Graph graph, PreparedStatementManager psMgr,
317 boolean autoAssign)
318 throws SQLException {
319 if (graph == null)
320 return;
321
322 DepthFirstAnalysis dfa = newDepthFirstAnalysis(graph, autoAssign);
323 Collection insertUpdates = new LinkedList();
324 Collection deleteUpdates = new LinkedList();
325 boolean recalculate;
326
327 // Handle circular constraints:
328 // - if deleted row A has a ciricular fk to deleted row B,
329 // then use an update statement to null A's fk to B before flushing,
330 // and then flush
331 // - if inserted row A has a circular fk to updated/inserted row B,
332 // then null the fk in the B row object, then flush,
333 // and after flushing, use an update to set the fk back to A
334 // Depending on where circular dependencies are broken, the
335 // topological order of the graph nodes has to be re-calculated.
336 recalculate = resolveCycles(graph, dfa.getEdges(Edge.TYPE_BACK),
337 deleteUpdates, insertUpdates);
338 recalculate |= resolveCycles(graph, dfa.getEdges(Edge.TYPE_FORWARD),
339 deleteUpdates, insertUpdates);
340
341 if (recalculate) {
342 dfa = recalculateDepthFirstAnalysis(graph, autoAssign);
343 }
344
345 // flush delete updates to null fks, then all rows in order, then
346 // the insert updates to set circular fk values
347 flush(deleteUpdates, psMgr);
348 Collection nodes = dfa.getSortedNodes();
349 for (Iterator itr = nodes.iterator(); itr.hasNext();)
350 psMgr.flush((RowImpl) itr.next());
351 flush(insertUpdates, psMgr);
352 }
353
354 /**
355 * Break a circular dependency caused by delete operations.
356 * If deleted row A has a ciricular fk to deleted row B, then use an update
357 * statement to null A's fk to B before deleting B, then delete A.
358 * @param edge Edge in the dependency graph corresponding to a foreign key
359 * constraint. This dependency is broken by nullifying the foreign key.
360 * @param deleteUpdates Collection of update statements that are executed
361 * before the delete operations are flushed
362 */
363 private void addDeleteUpdate(Edge edge, Collection deleteUpdates)
364 throws SQLException {
365 PrimaryRow row;
366 RowImpl update;
367 ForeignKey fk;
368
369 // copy where conditions into new update that nulls the fk
370 row = (PrimaryRow) edge.getTo();
371 update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
372 row.copyInto(update, true);
373 if (edge.getUserObject() instanceof ForeignKey) {
374 fk = (ForeignKey) edge.getUserObject();
375 update.setForeignKey(fk, row.getForeignKeyIO(fk), null);
376 } else
377 update.setNull((Column) edge.getUserObject());
378
379 deleteUpdates.add(update);
380 }
381
382 /**
383 * Break a circular dependency caused by insert operations.
384 * If inserted row A has a circular fk to updated/inserted row B,
385 * then null the fk in the B row object, then flush,
386 * and after flushing, use an update to set the fk back to A.
387 * @param row Row to be flushed
388 * @param edge Edge in the dependency graph corresponding to a foreign key
389 * constraint. This dependency is broken by nullifying the foreign key.
390 * @param insertUpdates Collection of update statements that are executed
391 * after the insert/update operations are flushed
392 */
393 private void addInsertUpdate(PrimaryRow row, Edge edge,
394 Collection insertUpdates) throws SQLException {
395 RowImpl update;
396 ForeignKey fk;
397 Column col;
398
399 // copy where conditions into new update that sets the fk
400 update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
401 if (row.getAction() == Row.ACTION_INSERT) {
402 if (row.getPrimaryKey() == null)
403 throw new InternalException(_loc.get("ref-cycle"));
404 update.wherePrimaryKey(row.getPrimaryKey());
405 } else {
406 // Row.ACTION_UPDATE
407 row.copyInto(update, true);
408 }
409 if (edge.getUserObject() instanceof ForeignKey) {
410 fk = (ForeignKey) edge.getUserObject();
411 update.setForeignKey(fk, row.getForeignKeyIO(fk),
412 row.getForeignKeySet(fk));
413 row.clearForeignKey(fk);
414 } else {
415 col = (Column) edge.getUserObject();
416 update.setRelationId(col, row.getRelationIdSet(col),
417 row.getRelationIdCallback(col));
418 row.clearRelationId(col);
419 }
420
421 insertUpdates.add(update);
422 }
423
424 /**
425 * Finds a nullable foreign key by walking the dependency cycle.
426 * Circular dependencies can be broken at this point.
427 * @param cycle Cycle in the dependency graph.
428 * @return Edge corresponding to a nullable foreign key.
429 */
430 private Edge findBreakableLink(List cycle) {
431 Edge breakableLink = null;
432 for (Iterator iter = cycle.iterator(); iter.hasNext(); ) {
433 Edge edge = (Edge) iter.next();
434 Object userObject = edge.getUserObject();
435 if (userObject instanceof ForeignKey) {
436 if (!((ForeignKey) userObject).hasNotNullColumns()) {
437 breakableLink = edge;
438 break;
439 }
440 } else if (userObject instanceof Column) {
441 if (!((Column) userObject).isNotNull()) {
442 breakableLink = edge;
443 break;
444 }
445 }
446 }
447 return breakableLink;
448 }
449
450 /**
451 * Re-calculates the DepthFirstSearch analysis of the graph
452 * after some of the edges have been removed. Ensures
453 * that the dependency graph is cycle free.
454 * @param graph The graph of statements to be walked
455 * @param autoAssign Whether any of the rows in the graph have any
456 * auto-assign constraints
457 */
458 private DepthFirstAnalysis recalculateDepthFirstAnalysis(Graph graph,
459 boolean autoAssign) {
460 DepthFirstAnalysis dfa;
461 // clear previous traversal data
462 graph.clearTraversal();
463 dfa = newDepthFirstAnalysis(graph, autoAssign);
464 // make sure that the graph is non-cyclic now
465 assert (dfa.hasNoCycles()): _loc.get("graph-not-cycle-free");
466 return dfa;
467 }
468
469 /**
470 * Resolve circular dependencies by identifying and breaking
471 * a nullable foreign key.
472 * @param graph Dependency graph.
473 * @param edges Collection of edges. Each edge indicates a possible
474 * circular dependency
475 * @param deleteUpdates Collection of update operations (nullifying
476 * foreign keys) to be filled. These updates will be executed before
477 * the rows in the dependency graph are flushed
478 * @param insertUpdates CCollection of update operations (nullifying
479 * foreign keys) to be filled. These updates will be executed after
480 * the rows in the dependency graph are flushed
481 * @return Depending on where circular dependencies are broken, the
482 * topological order of the graph nodes has to be re-calculated.
483 */
484 private boolean resolveCycles(Graph graph, Collection edges,
485 Collection deleteUpdates, Collection insertUpdates)
486 throws SQLException {
487 boolean recalculate = false;
488 for (Iterator itr = edges.iterator(); itr.hasNext();) {
489 Edge edge = (Edge) itr.next();
490 List cycle = edge.getCycle();
491
492 if (cycle != null) {
493 // find a nullable foreign key
494 Edge breakableLink = findBreakableLink(cycle);
495 if (breakableLink == null) {
496 throw new UserException(_loc.get("no-nullable-fk"));
497 }
498
499 // topologic node order must be re-calculated, if the
500 // breakable link is different from the edge where
501 // the circular dependency was originally detected
502 if (edge != breakableLink) {
503 recalculate = true;
504 }
505
506 if (!breakableLink.isRemovedFromGraph()) {
507
508 // use a primary row update to prevent setting pk and fk values
509 // until after flush, to get latest auto-increment values
510 PrimaryRow row = (PrimaryRow) breakableLink.getFrom();
511 if (row.getAction() == Row.ACTION_DELETE) {
512 addDeleteUpdate(breakableLink, deleteUpdates);
513 } else {
514 addInsertUpdate(row, breakableLink, insertUpdates);
515 }
516 graph.removeEdge(breakableLink);
517 }
518 }
519 }
520 return recalculate;
521 }
522
523 /**
524 * Create a new {@link DepthFirstAnalysis} suitable for the given graph
525 * and auto-assign settings.
526 */
527 protected DepthFirstAnalysis newDepthFirstAnalysis(Graph graph,
528 boolean autoAssign) {
529 return new DepthFirstAnalysis(graph);
530 }
531
532 /**
533 * Flush the given collection of secondary rows.
534 */
535 protected void flush(Collection rows, PreparedStatementManager psMgr) {
536 if (rows.size() == 0)
537 return;
538
539 RowImpl row;
540 for (Iterator itr = rows.iterator(); itr.hasNext(); ) {
541 row = (RowImpl) itr.next();
542 if (row.isValid() && !row.isDependent())
543 psMgr.flush(row);
544 }
545 }
546 }