Hello WOrriors,
An object marked for deletion using editingContext.deleteObject() should stay in the "deleted" objects bucket. Yet, under "special" circumstances it will jump to the "updated" objects bucket in the middle of an editingContext.saveChanges() at which point you are hosed.
This long standing issue can now be understood, and worked around. Most of us have little experience with it but those of us who do have lost years of our lives trying to find a solution. Chuck, Lenny, and a handful of others have discussed this issue at length and even offered plausible solutions. I have learned from them and offer yet one more way to snuff out this evil nasty bug.
At the surface, the object that makes a jump from the "deleted" -> "updated" bucket is always a "parent" object that has a cascade delete relationship. Faulting in the related objects to delete them during a saveChanges() confuses EOF.
Past solutions have dwelt on either pre-faulting all relationships (DeletePrefetcher.java) or on processing notifications in a special queue after saveChanges() is done. These solutions do help but are not complete. They provided some relief for me but they did not completely prevent my issue for all circumstances.
Let me back up a second. I should say that with nearly all of the WO apps I’ve had my fingers in I have never had problems with cascade deletes other than ordering of SQL statements. You either need to use deferred constraints or handle adaptor operation ordering yourself because EOF will not always do inserts/deletes in the appropriate order. This whole issue of a "deleted" eo jumping to the "updated" side only bites me in one project only.
Upon further reflection I realized that the project with the issue had a defaultFetchTimestampLag of merely 2 seconds. All the other projects were the default of 1 hour. I would postulate that these other projects have the exact same problem I’d just need to wait an hour plus one minute to trip the "jumping bucket" phenomena.
Now for the new solution to this vexing problem:
Tweak a subclass of EOEditingContext to have the ability to know when any EC is in the middle of "saveChanges()". Create an "EOFDelegate" and implement a special method to handle faulting.
public class DeleteSafeEOEditingContext extends EOEditingContext {
protected static NSMutableArray saveQueue = new NSMutableArray();
protected void enteringSaveChanges() {
synchronized (saveQueue) {
saveQueue.addObject("save");
}
}
protected void leavingSaveChanges() {
synchronized (saveQueue) {
if (saveQueue.count() > 0) {
saveQueue.removeObjectAtIndex(0);
}
}
}
public static boolean anEditingContextIsInTheMiddleOfSaveChanges() {
boolean inTheMiddle = false;
synchronized (saveQueue) {
inTheMiddle = saveQueue.count() > 0;
}
return inTheMiddle;
}
public void saveChanges() {
enteringSaveChanges();
try {
super.saveChanges();
} finally {
leavingSaveChanges();
}
}
} package com.webobjects.eoaccess;
import com.webobjects.foundation.*;
import com.webobjects.eocontrol.*;
//
// NOTE: We need to know the correct fetchStamp In the Database Channel but
// it is a closed API. There are a few ways to get it:
//
// 1) Completely rewrite EODatabaseChannel including apple package description then
// load it before Apple's frameworks to trick the JVM into using yours.
// 2) Use java "reflection" to get the private methods and mark them with
// "setAccessible(true)"
// 3) Create another class in the same package so you can treat anything
// "protected" as a "friend"
//
// We have taken approach #3 here.
//
public class EODatabaseChannelInspector {
public static long currentFetchTimeStamp(EODatabaseChannel dbChannel) {
return dbChannel._currentEditCtxTimestamp;
}
} public class EOFDelegate extends Object {
public NSDictionary databaseContextShouldUpdateCurrentSnapshot(
EODatabaseContext eodatabasecontext,
NSDictionary currentSnapshot,
NSDictionary newSnapshot,
EOGlobalID eoglobalid,
EODatabaseChannel eodatabasechannel) {
//System.out.println("\n\n\n\n\n=/=/= " + "Entering custom databaseContextShouldUpdateCurrentSnapshot()");
EODatabase database = eodatabasecontext.database();
long lag = com.webobjects.eoaccess.EODatabaseChannelInspector.currentFetchTimeStamp(eodatabasechannel);
NSDictionary snapshotToReturn = database.snapshotForGlobalID(eoglobalid, lag);
if (snapshotToReturn != null) {
if ( ! snapshotToReturn.isEqualToDictionary(newSnapshot)) {
snapshotToReturn = newSnapshot;
}
} else { // Note: snapshotToReturn == null
if (newSnapshot != null) {
snapshotToReturn = newSnapshot;
}
}
// Do not return a "new memory allocated" object that has the same value,
// it will confuse WO. If they have the same values, return the old object.
// WO does a "memory" equality instead of "value" equality.
if (snapshotToReturn != null && currentSnapshot != null &&
snapshotToReturn.isEqualToDictionary(currentSnapshot)) {
snapshotToReturn = currentSnapshot;
}
if (DeleteSafeEOEditingContext.anEditingContextIsInTheMiddleOfSaveChanges()) {
//Note: (Aaron) this works around what many consider a long standing
//EOF bug ("deleted" -> "updated" EO) by preventing the object store
//from being refreshed with a new snapshot for an EO during
//an ec.saveChanges().
//
//System.out.println("\n\n\n\n\n=/=/= " + "anEditingContextIsInTheMiddleOfSaveChanges for snapshot = " + currentSnapshot);
snapshotToReturn = currentSnapshot;
}
return snapshotToReturn;
}
} public Application() {
super();
EODatabaseContext.setDefaultDelegate(new EOFDelegate());
...
That’s it. That’s the solution.
Special care must be taken when implementing "databaseContextShouldUpdateCurrentSnapshot()" because it operates at a very low level. You could shoot yourself in the foot here if you are not careful. Returning "null" is generally not a good idea, contrary to what Apple’s documentation states. You need to return a good NSDictionary. If nothing has changed return the exact same NSDictionary that is passed via the "currentSnapshot" parameter. Passing an NSDictionary with the same values is not good enough, it has to be the same memory address (the same pointer).
I’d like to hear from any of you if you have had further experience with this issue or other insight. I’ve been testing this today and it appears to finally solve this issue in its entirety. Comments and criticism is welcome. I hope at the very least this posting will help another poor soul faced with this dilemma.