【问题标题】:How can I tell whether an `NSManagedObject` has been deleted?如何判断 `NSManagedObject` 是否已被删除?
【发布时间】:2011-05-19 10:24:37
【问题描述】:

我有一个 NSManagedObject 已被删除,并且包含该托管对象的上下文已被保存。我知道 isDeleted 返回 YES 如果 Core Data 将要求持久存储在下一次保存操作期间删除对象。然而,由于保存已经发生,isDeleted 返回NO

什么是判断NSManagedObject 是否在保存其包含的上下文之后被删除的好方法?

(如果您想知道为什么引用已删除托管对象的对象还不知道删除,这是因为删除和上下文保存是由后台线程启动的,该线程使用@987654327 执行删除和保存@.)

【问题讨论】:

    标签: core-data ios nsmanagedobject


    【解决方案1】:

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

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

    来自 Apple 在 managedObjectContext 上的文档 ...

    如果 接收方已从其删除 上下文。

    如果接收器有故障,调用 此方法不会触发它。

    这两个似乎都是好事。

    更新:如果您尝试测试专门使用objectWithID: 检索的托管对象是否已被删除,请查看Dave Gallagher's answer。他指出,如果您使用已删除对象的 ID 调用 objectWithID:,则返回的对象将是一个错误,not 将其 managedObjectContext 设置为 nil。因此,您不能简单地检查它的managedObjectContext 来测试它是否已被删除。如果可以,请使用existingObjectWithID:error:。如果不是,例如,您的目标是 Mac OS 10.5 或 iOS 2.0,则需要执行其他操作来测试是否删除。详情请见his answer

    【讨论】:

    • 还有一个方法 isInserted 在 NSManagedObject 上返回一个 BOOL,据我了解,这表示相同。在这种情况下使用它可能会更干净一些。
    • 不管怎样,在大多数情况下,这个 managedObjectContext 检查已经足够快了!
    • @de, isInserted 在对象被保存之前只有 YES,然后它变为 NO。文档没有这么说,但我的测试证明了这一点。
    • 在 iOS 7 上测试并删除一个对象,然后将删除合并到主线程上下文中,并且对于从主线程上下文中为该对象保存的任何引用,托管对象上下文不为零。尝试通过 ID 或任何其他获取属性获取对象返回 nil。
    【解决方案2】:

    更新:改进的答案,基于 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 是您的核心数据类/控制器。

    - (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 是否已从Persistent Store,但并不总是像 Apple 在其文档中所说的那样准确:

    如果接收者已从其删除,则此方法可能返回 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.
    

    如您所见,如果 NSManagedObject 已从 Persistent Store 中删除,-managedObjectContext 不会总是返回 nil。

    【讨论】:

    • 有趣,虽然看起来这不适用于没有属性的对象。另外,为什么不使用existingObjectWithID:error: 而不是objectWithID:,只检查返回值是否等于nil
    • 啊,你是对的,-existingObjectWithID:error: 是更好的方法! :) 我写的答案是为了与 Mac OS X 10.5+ 兼容,所以我忽略了那个方法,它只是 10.6+。是的,我的回答不适用于没有任何属性的对象,尽管您的数据模型中不太可能有空对象。
    • 你是对的。对象不太可能没有属性,包括关系。出于某种原因,我只考虑属性。嗯……有没有办法在不检查所有属性的情况下快速评估objectWithID: 返回的故障? (对于 尚未 被删除的对象,访问每个属性可能会变得很昂贵。)如果只有一个方法会引发错误,您可以只在 objectWithID: 返回的对象上调用该方法看看它是否真的存在。我找了这样的方法,但没有发现任何明显的东西。
    • 我想更好的优化方法是只查询一个属性。而不是 for 循环,只需运行一次 (void)[managedObjectClone valueForKey:[propertyNames objectAtIndex:0]];。对于已删除的对象,它会触发错误,尝试从持久存储中读取,并立即引发NSObjectInaccessibleException。如果它没有引发NSObjectInaccessibleException,则意味着它成功地从持久存储中读取,并且对象没有被删除。如果您在索引 0 处的“随机”属性可能很大,例如 100MB 二进制 NSData,那么对此进行优化会很棘手......
    • 这将使该方法更长,但为什么不预先调用“isDeleted”并立即返回呢?目前它可能会说一些要被删除的东西不会被删除,这可能很糟糕......
    【解决方案3】:

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

    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

    【讨论】:

    • 我担心这个问题的复杂性甚至没有被充分探索:假设 concurrent 环境,[moc existingObjectWithID:object.objectID error:NULL])] 的结果立即过时。因此,即使我们会对此进行测试并得到“是”,另一个上下文也可能会删除该对象并保存该上下文。随后发送到前一个上下文的save 现在将引发异常。更糟糕的是,Core Data 内部可能会使用 Blocks 并将它们同步分派到另一个线程,然后发生此异常,这使得调用站点上的 try 和 catch 块无用。
    • 我不相信这是真的。托管对象上下文获取持久存储的快照,并且在合并更改或从存储中获取数据之前,它不受其他上下文或存储上的操作的影响。只要合并与执行existingObjectWithID:的代码在同一个线程(例如主线程)上执行,那么每个将按顺序处理,并且对象只会在合并后过时。
    【解决方案4】:

    由于我最近在依赖 Core Data 的 iOS 应用程序中实现 iCloud 的经验,我意识到最好的方法是观察框架的通知。至少,比依赖一些可能会或可能不会告诉您是否删除了某些托管对象的晦涩方法要好。

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

    如果您在其中一组中找到您的托管对象的 objectID,那么您可以通过一些不错的方式更新您的应用程序和 UI。

    就是这样...有关更多信息,请有机会阅读 Apple 的核心数据编程指南,与核心数据并发一章。有一节“使用通知跟踪其他线程中的更改”,但不要忘记查看上一节“使用线程限制支持并发”。

    【讨论】:

    • 这确实是最好的方法,当然也不难。
    【解决方案5】:

    在 Swift 3、Xcode 7.3 中验证

    你也可以简单地PRINT每个上下文的内存引用并检查

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

    例如:(Book 和 Member 是 2 个不同的对象)

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

    如果上下文存在但不同,它会打印这样的内容

    0x7fe758c307d0
    0x7fe758c15d70
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-05-02
      • 2022-01-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-04
      相关资源
      最近更新 更多