How can I tell whether an `NSManagedObject` has been deleted?
UPDATE: An improved answer, based on James Huddleston's ideas in the discussion below.
- (BOOL)hasManagedObjectBeenDeleted:(NSManagedObject *)managedObject {
/*
Returns YES if |managedObject| has been deleted from the Persistent Store,
or NO if it has not.
NO will be returned for NSManagedObject's who have been marked for deletion
(e.g. their -isDeleted method returns YES), but have not yet been commited
to the Persistent Store. YES will be returned only after a deleted
NSManagedObject has been committed to the Persistent Store.
Rarely, an exception will be thrown if Mac OS X 10.5 is used AND
|managedObject| has zero properties defined. If all your NSManagedObject's
in the data model have at least one property, this will not be an issue.
Property == Attributes and Relationships
Mac OS X 10.4 and earlier are not supported, and will throw an exception.
*/
NSParameterAssert(managedObject);
NSManagedObjectContext *moc = [self managedObjectContext];
// Check for Mac OS X 10.6+
if ([moc respondsToSelector:@selector(existingObjectWithID:error:)])
{
NSManagedObjectID *objectID = [managedObject objectID];
NSManagedObject *managedObjectClone = [moc existingObjectWithID:objectID error:NULL];
if (!managedObjectClone)
return YES; // Deleted.
else
return NO; // Not deleted.
}
// Check for Mac OS X 10.5
else if ([moc respondsToSelector:@selector(countForFetchRequest:error:)])
{
// 1) Per Apple, "may" be nil if |managedObject| deleted but not always.
if (![managedObject managedObjectContext])
return YES; // Deleted.
// 2) Clone |managedObject|. All Properties will be un-faulted if
// deleted. -objectWithID: always returns an object. Assumed to exist
// in the Persistent Store. If it does not exist in the Persistent
// Store, firing a fault on any of its Properties will throw an
// exception (#3).
NSManagedObjectID *objectID = [managedObject objectID];
NSManagedObject *managedObjectClone = [moc objectWithID:objectID];
// 3) Fire fault for a single Property.
NSEntityDescription *entityDescription = [managedObjectClone entity];
NSDictionary *propertiesByName = [entityDescription propertiesByName];
NSArray *propertyNames = [propertiesByName allKeys];
NSAssert1([propertyNames count] != 0, @"Method cannot detect if |managedObject| has been deleted because it has zero Properties defined: %@", managedObject);
@try
{
// If the property throws an exception, |managedObject| was deleted.
(void)[managedObjectClone valueForKey:[propertyNames objectAtIndex:0]];
return NO; // Not deleted.
}
@catch (NSException *exception)
{
if ([[exception name] isEqualToString:NSObjectInaccessibleException])
return YES; // Deleted.
else
[exception raise]; // Unknown exception thrown.
}
}
// Mac OS X 10.4 or earlier is not supported.
else
{
NSAssert(0, @"Unsupported version of Mac OS X detected.");
}
}
OLD/DEPRECIATED ANSWER:
I wrote a slightly better method. self
is your Core Data class/controller.
- (BOOL)hasManagedObjectBeenDeleted:(NSManagedObject *)managedObject
{
// 1) Per Apple, "may" be nil if |managedObject| was deleted but not always.
if (![managedObject managedObjectContext])
return YES; // Deleted.
// 2) Clone |managedObject|. All Properties will be un-faulted if deleted.
NSManagedObjectID *objectID = [managedObject objectID];
NSManagedObject *managedObjectClone = [[self managedObjectContext] objectWithID:objectID]; // Always returns an object. Assumed to exist in the Persistent Store. If it does not exist in the Persistent Store, firing a fault on any of its Properties will throw an exception.
// 3) Fire faults for Properties. If any throw an exception, it was deleted.
NSEntityDescription *entityDescription = [managedObjectClone entity];
NSDictionary *propertiesByName = [entityDescription propertiesByName];
NSArray *propertyNames = [propertiesByName allKeys];
@try
{
for (id propertyName in propertyNames)
(void)[managedObjectClone valueForKey:propertyName];
return NO; // Not deleted.
}
@catch (NSException *exception)
{
if ([[exception name] isEqualToString:NSObjectInaccessibleException])
return YES; // Deleted.
else
[exception raise]; // Unknown exception thrown. Handle elsewhere.
}
}
As James Huddleston mentioned in his answer, checking to see if NSManagedObject's -managedObjectContext
returns nil
is a "pretty good" way of seeing if a cached/stale NSManagedObject has been deleted from the Persistent Store, but it's not always accurate as Apple states in their docs:
This method may return nil if the receiver has been deleted from its context.
When won't it return nil? If you acquire a different NSManagedObject using the deleted NSManagedObject's -objectID
like so:
// 1) Create a new NSManagedObject, save it to the Persistant Store.
CoreData *coreData = ...;
NSManagedObject *apple = [coreData addManagedObject:@"Apple"];
[apple setValue:@"Mcintosh" forKey:@"name"];
[coreData saveMOCToPersistentStore];
// 2) The `apple` will not be deleted.
NSManagedObjectContext *moc = [apple managedObjectContext];
if (!moc)
NSLog(@"2 - Deleted.");
else
NSLog(@"2 - Not deleted."); // This prints. The `apple` has just been created.
// 3) Mark the `apple` for deletion in the MOC.
[[coreData managedObjectContext] deleteObject:apple];
moc = [apple managedObjectContext];
if (!moc)
NSLog(@"3 - Deleted.");
else
NSLog(@"3 - Not deleted."); // This prints. The `apple` has not been saved to the Persistent Store yet, so it will still have a -managedObjectContext.
// 4) Now tell the MOC to delete the `apple` from the Persistent Store.
[coreData saveMOCToPersistentStore];
moc = [apple managedObjectContext];
if (!moc)
NSLog(@"4 - Deleted."); // This prints. -managedObjectContext returns nil.
else
NSLog(@"4 - Not deleted.");
// 5) What if we do this? Will the new apple have a nil managedObjectContext or not?
NSManagedObjectID *deletedAppleObjectID = [apple objectID];
NSManagedObject *appleClone = [[coreData managedObjectContext] objectWithID:deletedAppleObjectID];
moc = [appleClone managedObjectContext];
if (!moc)
NSLog(@"5 - Deleted.");
else
NSLog(@"5 - Not deleted."); // This prints. -managedObjectContext does not return nil!
// 6) Finally, let's use the method I wrote, -hasManagedObjectBeenDeleted:
BOOL deleted = [coreData hasManagedObjectBeenDeleted:appleClone];
if (deleted)
NSLog(@"6 - Deleted."); // This prints.
else
NSLog(@"6 - Not deleted.");
Here's the printout:
2 - Not deleted.
3 - Not deleted.
4 - Deleted.
5 - Not deleted.
6 - Deleted.
As you can see, -managedObjectContext
won't always return nil if an NSManagedObject has been deleted from the Persistent Store.
Checking the context of the managed object seems to work:
if (managedObject.managedObjectContext == nil) {
// Assume that the managed object has been deleted.
}
From Apple's documentation on managedObjectContext
...
This method may return nil if the receiver has been deleted from its context.
If the receiver is a fault, calling this method does not cause it to fire.
Both of those seem to be good things.
UPDATE: If you're trying to test whether a managed object retrieved specifically using objectWithID:
has been deleted, check out Dave Gallagher's answer. He points out that if you call objectWithID:
using the ID of a deleted object, the object returned will be a fault that does not have its managedObjectContext
set to nil. Consequently, you can't simply check its managedObjectContext
to test whether it has been deleted. Use existingObjectWithID:error:
if you can. If not, e.g., you're targeting Mac OS 10.5 or iOS 2.0, you'll need to do something else to test for deletion. See his answer for details.