71

I have an NSManagedObject that has been deleted, and the context containing that managed object has been saved. I understand that isDeleted returns YES if Core Data will ask the persistent store to delete the object during the next save operation. However, since the save has already happened, isDeleted returns NO.

What is a good way to tell whether an NSManagedObject has been deleted after its containing context has been saved?

(In case you're wondering why the object referring to the deleted managed object isn't already aware of the deletion, it's because the deletion and context save was initiated by a background thread which performed the deletion and save using performSelectorOnMainThread:withObject:waitUntilDone:.)

James Huddleston
  • 8,222
  • 4
  • 32
  • 39

5 Answers5

94

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.

Community
  • 1
  • 1
James Huddleston
  • 8,222
  • 4
  • 32
  • 39
  • There is also a method `isInserted` returning a BOOL on NSManagedObject which, to my understanding, signifies the same. It's probably a bit cleaner to use it for this case. – de. Aug 24 '13 at 22:43
  • Anyway, in most cases ,this managedObjectContext check is enough and fast! – flypig Dec 18 '13 at 06:21
  • 1
    @de, `isInserted` is only YES until the object is saved, and then it becomes NO. The documentation does not say this, but my testing proves it. – phatmann Apr 11 '14 at 12:52
  • 1
    Testing on iOS 7 and deleting an object that then has the deletion merged into a main thread context and the managed object context is not nil for any references saved for that object from the main thread context. Attempting to fetch the object by ID or any other fetch properties returns nil. – jjxtra May 02 '14 at 22:06
43

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.

Dave
  • 11,848
  • 11
  • 59
  • 67
  • 1
    Interesting, though it looks like this won't work on objects that don't have properties. Also, why not use `existingObjectWithID:error:` instead of `objectWithID:` and just check whether the return value equals `nil`? – James Huddleston Oct 26 '11 at 15:07
  • Ah, you're correct, `-existingObjectWithID:error:` is a better way! :) I wrote the answer to be compatible with Mac OS X 10.5+, so I ignored that method, which is 10.6+ only. And yes, my answer will not work for an object without any properties, though it's unlikely to have empty objects in your data model. – Dave Oct 26 '11 at 16:04
  • You're right. It's unlikely for objects to have no properties, which includes relationships. For some reason, I was thinking of attributes alone. Hmm... is there a way to quickly evaluate the fault returned by `objectWithID:` without checking all properties? (Accessing every property could get expensive for objects which *haven't* been deleted.) If there were a single method that would fire the fault, you could just call that method on the object returned by `objectWithID:` to see whether it really exists or not. I looked for such a method, but didn't find anything obvious. – James Huddleston Oct 26 '11 at 17:02
  • I guess a better way to optimize would be to only query a single Property. Instead of the for-loop, just run `(void)[managedObjectClone valueForKey:[propertyNames objectAtIndex:0]];` once. For a deleted object, it'll fire a fault, try to read from the Persistent Store, and raise `NSObjectInaccessibleException` immediately. If it does not raise `NSObjectInaccessibleException`, that means it read from the Persistent Store successfully, and the object was not deleted. If your "random" Property at index 0 might be huge, like a 100MB binary NSData, optimizing for that would be tricky... – Dave Oct 26 '11 at 17:18
  • 2
    This is going to make the method even longer, but why not give "isDeleted" a call up front as well and return that right away if it is? Currently it could say something about to be deleted is not going to be, which could be bad... – Kendall Helmstetter Gelner Apr 05 '12 at 00:54
  • In the current answer #1 checks first if the obj.moc exists, the only issue being that the *moc variable would be nil, thus not responding to any selectors and never getting that far. I'm of the opinion that you must check much earlier if the moc is nil. – sbonami Jan 31 '13 at 19:13
29

I fear the discussion in the other answers is actually hiding the simplicity of the correct answer. In pretty much all cases, the correct answer is:

if ([moc existingObjectWithID:object.objectID error:NULL])
{
    // object is valid, go ahead and use it
}

The only cases this answer doesn't apply in is:

  1. If you are targetting Mac OS 10.5 or earlier
  2. If you are targetting iOS 2.0 or earlier
  3. If the object/context has not been saved yet (in which case you either don't care because it won't throw a NSObjectInaccessibleException, or you can use object.isDeleted)
JosephH
  • 36,107
  • 19
  • 126
  • 149
  • 2
    I fear the complexity of this issue is even not fully explored: Assuming a _concurrent_ environment, the result of `[moc existingObjectWithID:object.objectID error:NULL])]` is immediately stale. So, even we would test this and get a "YES", another context may delete the object and save the context. The subsequent `save` sent to the former context will now throw an exception. Worse, internally Core Data may use Blocks and synchronously dispatch them to another thread where this exception then occurs, which makes try and catch blocks on the call-site useless. – CouchDeveloper Mar 20 '14 at 16:01
  • 2
    I don't believe that to be true. The managed object context takes a snapshot of the persistent store and it is not affected by operations on other contexts or the store until it merges changes or fetches data from the store. As long as the merge is performed on the same thread (e.g. the main thread) as the code performing `existingObjectWithID:` then each will be processed in sequence, and the object will only be stale after the merge. – Matt May 13 '15 at 02:09
14

Due to my recent experience implementing iCloud in my iOS app that relies on Core Data for persistence, I realized that the best way is observing the framework's notifications. At least, better than relying on some obscure methods that may, or may not tell you if some managed object was deleted.

For 'pure' Core Data apps you should observe NSManagedObjectContextObjectsDidChangeNotification on the main thread. The notification's user info dictionary contains sets with the managed objects' objectIDs that were inserted, deleted and updated.

If you find your managed object's objectID in one of these sets, then you can update your application and UI in some nice way.

That's it... for more information, give a chance to Apple's Core Data Programming Guide, Concurrency with Core Data chapter. There's a section "Track Changes in Other Threads Using Notifications", but don't forget to check the previous one "Use Thread Confinement to Support Concurrency".

rmartinsjr
  • 499
  • 4
  • 6
0

Verified in Swift 3, Xcode 7.3

You can also simply PRINT the memory references of each context and check

(a) if the context exists,
(b) if the contexts of 2 objects are different

eg:( Book and Member being 2 different objects)

 print(book.managedObjectContext)
 print(member.managedObjectContext)

It would print something like this if the contexts exist but are different

0x7fe758c307d0
0x7fe758c15d70
Naishta
  • 9,905
  • 4
  • 60
  • 49