如何判断一个`NSManagedObject`是否已被删除?

74

我有一个已经被删除的NSManagedObject对象,包含该托管对象的上下文已经保存。我知道如果Core Data在下一次保存操作时要求删除对象,则isDeleted返回YES。但是,由于保存已经发生,所以isDeleted返回NO

有什么好方法可以在包含该对象的上下文已经保存后告诉是否已删除NSManagedObject对象?

(如果你想知道为什么引用已删除托管对象的对象还没有意识到这一点,那是因为删除和上下文保存是由后台线程发起的,该线程使用performSelectorOnMainThread:withObject:waitUntilDone:方法执行删除和保存操作.)

5个回答

98

检查托管对象的上下文似乎有效:

if (managedObject.managedObjectContext == nil) {
    // Assume that the managed object has been deleted.
}

从苹果关于managedObjectContext的文档中得知...

如果接收器已经从其上下文中删除,则此方法可能返回nil。

如果接收器是一个fault,调用此方法不会导致它触发。

这两个都似乎是好事情。

更新: 如果您想测试使用objectWithID:检索的托管对象是否已被删除,请查看Dave Gallagher的答案。他指出,如果使用已删除对象的ID调用objectWithID:,则返回的对象将是一个fault,它的managedObjectContext没有设置为nil。因此,您不能简单地检查其managedObjectContext以测试它是否已被删除。如果可以,请使用existingObjectWithID:error:。如果不行,例如您正在针对Mac OS 10.5或iOS 2.0,则需要执行其他操作来测试删除。有关详细信息,请参见他的答案


还有一个方法isInserted在NSManagedObject上返回一个BOOL,据我理解,它的意义相同。在这种情况下使用它可能会更加简洁。 - de.
无论如何,在大多数情况下,这个managedObjectContext检查已经足够快了! - flypig
1
@de,isInserted只有在对象保存之前是YES,然后变成NO。文档中没有提到这一点,但我的测试证明了这一点。 - phatmann
1
在iOS 7上进行测试并删除一个对象,然后将删除合并到主线程上下文中,并且对于从主线程上下文保存的该对象的任何引用,托管对象上下文不为nil。尝试通过ID或任何其他获取属性获取对象都会返回nil。 - jjxtra

43

更新:在下面的讨论中,基于James Huddleston的想法,提出了更好的答案。

- (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.");
    }
}

旧版/废弃的回答:

我写了一个稍微好一点的方法。 self 是你的 Core Data 类/控制器。

- (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.
    }
}
James Huddleston在他的回答中提到的那样,检查 NSManagedObject 的 -managedObjectContext 是否返回 nil 是一种“相当不错”的方法,可以看出缓存/过时的 NSManagedObject 是否已从持久化存储中删除,但正如苹果在其文档中所述,它并不总是准确的:

如果接收者已从其上下文中删除,则此方法可能返回nil。

什么情况下它将不会返回 nil? 如果您使用已删除的 NSManagedObject 的 -objectID 获取了不同的 NSManagedObject,如下所示:

// 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.");

这是打印输出的结果:

2 - Not deleted.
3 - Not deleted.
4 - Deleted.
5 - Not deleted.
6 - Deleted.

正如您所看到的,-managedObjectContext如果从持久存储中删除了NSManagedObject,则不会始终返回nil。


1
有趣的是,看起来这个方法不能用在没有属性的对象上。另外,为什么不使用existingObjectWithID:error:而不是objectWithID:并且只需检查返回值是否等于nil呢? - James Huddleston
啊,你说得对,-existingObjectWithID:error: 是更好的方法! :) 我写的答案是为了兼容 Mac OS X 10.5+,所以我忽略了那个只有在10.6+才有的方法。是的,我的答案对于没有任何属性的对象将无法工作,尽管在您的数据模型中很少会有空对象。 - Dave
你说得对。对象很难没有属性,包括关系。由于某种原因,我只考虑了属性。嗯...有没有一种快速评估objectWithID:返回的错误而不检查所有属性的方法?(访问每个属性可能会使尚未被删除的对象变得昂贵。)如果有一个单一的方法可以触发故障,您可以在objectWithID:返回的对象上调用该方法,以查看它是否真的存在。我寻找这样的方法,但没有找到任何明显的东西。 - James Huddleston
我猜优化的更好的方法是只查询单个属性。不需要使用for循环,只需运行(void)[managedObjectClone valueForKey:[propertyNames objectAtIndex:0]];一次即可。对于已删除的对象,它将触发一个故障,尝试从持久存储中读取,并立即引发NSObjectInaccessibleException。如果它没有引发NSObjectInaccessibleException,那么这意味着它成功地从持久存储中读取,并且该对象未被删除。如果你在索引0处的“随机”属性可能非常大,比如100MB的二进制NSData,那么针对它进行优化会很棘手... - Dave
2
这将使方法变得更长,但为什么不提前调用“isDeleted”,如果是,则立即返回它呢?目前它可能会说即将被删除的内容不会被删除,这可能是不好的... - Kendall Helmstetter Gelner
在当前的答案#1中,首先检查obj.moc是否存在,唯一的问题是*moc变量可能为nil,因此不会响应任何选择器,也永远不会到达那里。我认为你必须更早地检查moc是否为nil。 - sbonami

30

我担心其他答案中的讨论实际上掩盖了正确答案的简单性。在几乎所有情况下,正确答案都是:

if ([moc existingObjectWithID:object.objectID error:NULL])
{
    // object is valid, go ahead and use it
}
这个答案不适用于以下情况:
  1. 如果你的目标是Mac OS 10.5或更早版本
  2. 如果你的目标是iOS 2.0或更早版本
  3. 如果对象/上下文尚未保存(这种情况下,您可能不关心它是否会抛出NSObjectInaccessibleException,或者您可以使用object.isDeleted

2
我担心这个问题的复杂性甚至还没有完全探索:假设在一个并发环境中,[moc existingObjectWithID:object.objectID error:NULL])] 的结果立即过时。因此,即使我们测试并得到“YES”,另一个上下文可能会删除对象并保存上下文。随后发送给前一个上下文的 save 现在将抛出异常。更糟糕的是,内部 Core Data 可能使用 Blocks 并将它们同步分派到另一个线程,在那里这个异常就会发生,这使得调用站点上的 try 和 catch 块无用。 - CouchDeveloper
2
我不相信那是真的。托管对象上下文会对持久化存储进行快照,并且在其他上下文或存储上的操作不会影响它,直到合并更改或从存储中获取数据。只要合并在执行existingObjectWithID:的代码所在的同一线程(例如主线程)上执行,每个都将按顺序处理,并且对象仅在合并后过时。 - Matt

15

基于我最近在我的iOS应用中实现iCloud并依赖Core Data进行持久化的经验,我意识到最好的方法是观察框架的通知。至少,这比依赖一些可能会告诉你某个托管对象是否被删除的晦涩方法要好。

对于“纯”Core Data应用程序,您应该在主线程上观察NSManagedObjectContextObjectsDidChangeNotification通知。通知的用户信息字典包含插入、删除和更新的托管对象的对象ID集合。

如果您在这些集合中找到了您的托管对象的对象ID,则可以以某种不错的方式更新您的应用程序和UI。

就是这样...有关更多信息,请查看苹果的Core Data编程指南,其中包括Concurrency with Core Data章节。有一个名为“使用通知跟踪其他线程中的更改”的部分,但不要忘记检查前面的“使用线程限制来支持并发性”部分。


这确实是最好的方法,而且它并不困难。 - Charles A.

0

在Swift 3,Xcode 7.3中已验证

您还可以简单地PRINT每个上下文的内存引用并进行检查

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

例如:(书籍和会员是两个不同的对象)

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

如果上下文存在但不同,它会打印出类似这样的内容。
0x7fe758c307d0
0x7fe758c15d70

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接