核心数据级联删除不可靠?

时间:2013-07-09 05:45:25

标签: core-data nsfetchedresultscontroller cascade

prepareForDelete 在删除原因为级联删除规则时更新模型时, NSFetchedResultsController 似乎存在错误。

似乎暗示隐式删除(通过级联删除)的行为与显式删除的行为完全不同。

这真的是一个错误,还是你可以解释为什么我看到这些奇怪的结果?


设置项目

您可以跳过整个部分,而不是download the xcodeproj

  1. 使用主从应用程序模板创建新项目。

  2. 事件实体添加新属性。 (这很重要,因为我们希望能够更新属性而不会导致NSFetchedResultsController重新排序其任何项目。否则它将发送NSFetchedResultsChangeMove事件而不是NSFetchedResultsChangeUpdate事件。

  3. 调用属性hasMovedUp,并将其设为Boolean。 (注意:创建这样的属性可能看起来很愚蠢,但这只是一个例子,我试图将其减少到重现此错误所需的最小步骤数。)

  4. 添加新实体,将其命名为EventParent

  5. 活动建立关系,将其称为child。也建立反向关系,称之为parent。 (注意:这是1:1的关系。)

  6. 单击EventParent。单击其子关系。将删除规则设置为级联。我们的想法是,我们只会删除父对象。删除父项后,它将自动删除其子项。

  7. 将事件的父母关系删除规则保留为 Nullify

  8. 通过Xcode为两个实体创建NSManagedObject子类。

  9. 在创建新事件的insertNewObject:方法中,请务必创建相应的父级。

  10. Event.m文件中,通过声明hasMovedUp事件自动将最后一个事件的YES指定为prepareForDeletion

    NSLog(@"Prepare for deletion");
    
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:NO];
    [fetchRequest setSortDescriptors:@[sortDescriptor]];
    NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
    NSAssert(results, nil);
    Event *lastEvent = results.lastObject;
    NSLog(@"Updating event: %@", lastEvent.timeStamp);
    lastEvent.hasMovedUp = @YES;
    
    [super prepareForDeletion];
    
  11. 在Storyboard中,将segue删除到DetailViewController。我们不需要它。

  12. 如果是didChangeObjectNSFetchedResultsChangeDelete,请在NSFetchedResultsChangeUpdate事件中添加一些日志语句。输出indexPath.row

  13. 最后,使其在点击单元格时删除其对应的父级。通过在- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {文件中创建MasterViewController.m来执行此操作:

    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
    Event *event = [self.fetchedResultsController objectAtIndexPath:indexPath];
    EventParent *parent = event.parent;
    
    NSLog(@"Deleting event: %@", event.timeStamp);
    [context deleteObject:parent];
    //[context deleteObject:event]; // comment and uncomment this line to reproduce or fix the error, respectively.
    
  14. 到目前为止的设置摘要:

    • 我们不打算多接触NSFetchedResultsController。我们将允许它观察和展示活动。
    • 每当我们删除EventParent时,我们都希望删除相应的Event。
    • 要添加其他扭曲,我们希望每当删除事件时都会更新hasMovedUp属性。

    重现错误

    1. 运行App

    2. 点击加号按钮两次创建2条记录。

    3. 点击顶部记录并观看应用程序崩溃(注意:95%的时间会崩溃。如果它没有崩溃,请重新启动应用程序直至崩溃一样)。以下是一些有用的NSLog:

      2013-07-09 13:38:26.984 ReproNFC_PFD_bug[9518:11603] Deleting event: 2013-07-09 20:28:30 +0000
      2013-07-09 13:38:26.986 ReproNFC_PFD_bug[9518:11603] Prepare for deletion
      2013-07-09 13:38:26.987 ReproNFC_PFD_bug[9518:11603] Updating event: 2013-07-09 02:48:49 +0000
      2013-07-09 13:38:26.989 ReproNFC_PFD_bug[9518:11603] Delete detected on row: 0
      2013-07-09 13:38:26.990 ReproNFC_PFD_bug[9518:11603] Update detected on row: 1
      
    4. 现在取消注释上面的[context deleteObject:event]行。

    5. 运行应用程序并注意它不再崩溃。日志:

      2013-07-09 13:20:19.917 ReproNFC_PFD_bug[8997:11603] Deleting event: 2013-07-09 20:20:03 +0000
      2013-07-09 13:20:19.919 ReproNFC_PFD_bug[8997:11603] Prepare for deletion
      2013-07-09 13:20:19.921 ReproNFC_PFD_bug[8997:11603] Delete detected on row: 0
      2013-07-09 13:20:19.924 ReproNFC_PFD_bug[8997:11603] Updating event: 2013-07-09 02:48:49 +0000
      2013-07-09 13:20:19.925 ReproNFC_PFD_bug[8997:11603] Update detected on row: 0
      
    6. 日志中有两件事情不同:

      1. 在我们更新下一个活动之前检测到删除。

      2. 更新发生在第0行(正确的行)而不是第1行(不正确的行)。请继续阅读,了解为什么0是正确的数字。

      3. (注意:即使在我们预期错误发生的5%时间内也没有发生,但日志事件的输出顺序相同。)


        例外

        configureCell:atIndexPath:中的以下行引发了异常:

        NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
        

        导致异常的原因是因为在不再存在的行上检测到更新(1)。请注意,如果未发生异常,则会在正确的行(0)上检测到更新,因为顶行将被删除,而底行现在位于索引0处。

        提出的例外是:

          

        CoreData:错误:严重的应用程序错误。在Core Data更改处理期间捕获到异常。这通常是NSManagedObjectContextObjectsDidChangeNotification的观察者中的错误。 *** - [_ PFBatchFaultingArray objectAtIndex:]:索引(19789522)超出边界(2)与userInfo(null)

             

             

        *由于未捕获的异常终止应用' NSRangeException',原因:' * - [_ PFBatchFaultingArray objectAtIndex:]:索引(19789522)超出边界(2)& #39;


        的意义

        这似乎表明依赖级联删除规则与自己明确删除对象不同。

        换句话说......

        此:

         [context deleteObject:parent]; 
         // parent will auto-delete the corresponding Event via a cascade rule
        

        ...与此不一样:

         [context deleteObject:parent];
         [context deleteObject:event];
        

        的变通方法

        2013年6月9日更新:

        Xcodeproj已更新为包含可用的不同变通方法的多个#define语句(在 Event.h 文件中)。保留所有3未定义以重现该错误。定义其中任何一个以查看实现的特定解决方法。到目前为止,有三种解决方法:A,B和C.

        答:明确要求删除

        此解决方案与上面已经提到的内容重复,但为了完整起见,它包括在内。

        不依赖于级联删除,而是自己调用删除,一切都会正常工作:

            // (CUSTOMIZATION_POINT A)
            [context deleteObject:parent]; // A1: this line should always run
        #ifdef Workaround_A
            [context deleteObject:event]; // A2: this line will fix the bug
        #endif
        

        日志:

        2013-07-09 13:20:19.917 ReproNFC_PFD_bug[8997:11603] Deleting event: 2013-07-09 20:20:03 +0000
        2013-07-09 13:20:19.919 ReproNFC_PFD_bug[8997:11603] Prepare for deletion
        2013-07-09 13:20:19.921 ReproNFC_PFD_bug[8997:11603] Delete detected on row: 0
        2013-07-09 13:20:19.924 ReproNFC_PFD_bug[8997:11603] Updating event: 2013-07-09 02:48:49 +0000
        2013-07-09 13:20:19.925 ReproNFC_PFD_bug[8997:11603] Update detected on row: 0
        

        B:使用@ MartinR' s recommendation

        忽略indexPath参数,并且仅使用anObject方法中的didChangeObject:参数,您可以规避问题:

                case NSFetchedResultsChangeUpdate:
                    NSLog(@"Update detected on row: %d", indexPath.row);
                    // (CUSTOMIZATION_POINT B)
        #ifndef Workaround_B
                    [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; // B1: causes bug
        #else
                    [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject]; // B2: doesn't cause bug
        #endif
                    break;
        

        但是,日志仍会显示无序的信息:

        2013-07-09 13:24:43.662 ReproNFC_PFD_bug[9101:11603] Deleting event: 2013-07-09 20:24:42 +0000
        2013-07-09 13:24:43.663 ReproNFC_PFD_bug[9101:11603] Prepare for deletion
        2013-07-09 13:24:43.666 ReproNFC_PFD_bug[9101:11603] Updating event: 2013-07-09 02:48:49 +0000
        2013-07-09 13:24:43.667 ReproNFC_PFD_bug[9101:11603] Delete detected on row: 0
        2013-07-09 13:24:43.667 ReproNFC_PFD_bug[9101:11603] Update detected on row: 1
        

        这让我相信这个解决方案可能会在我的代码的其他部分引起相关问题。

        C:在prepareForDelete中使用0秒延迟:

        如果在准备删除后零秒延迟后更新对象,这将规避错误:

        - (void)updateLastEventInContext:(NSManagedObjectContext *)context {
            // warning: do not call self.<anything> in this method when it is called with a delay, since the object would have already been deleted
            NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
            NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp" ascending:NO];
            [fetchRequest setSortDescriptors:@[sortDescriptor]];
            NSArray *results = [context executeFetchRequest:fetchRequest error:nil];
            NSAssert(results, nil);
            Event *lastEvent = results.lastObject;
            NSLog(@"Updating event: %@", lastEvent.timeStamp);
            lastEvent.hasMovedUp = @YES;
        }
        
        - (void)prepareForDeletion {
            NSLog(@"Prepare for deletion");
        
            // (CUSTOMIZATION_POINT C)
        #ifndef Workaround_C
            [self updateLastEventInContext:self.managedObjectContext]; // C1: causes the bug
        #else
            [self performSelector:@selector(updateLastEventInContext:) withObject:self.managedObjectContext afterDelay:0]; // C2: doesn't cause the bug
        #endif
        
            [super prepareForDeletion];
        }
        

        此外,日志顺序似乎是正确的,因此您可以继续在NSFetchedResultsController上调用indexPath(即,您不需要使用变通方法B):

        2013-07-09 13:27:38.308 ReproNFC_PFD_bug[9196:11603] Deleting event: 2013-07-09 20:27:37 +0000
        2013-07-09 13:27:38.309 ReproNFC_PFD_bug[9196:11603] Prepare for deletion
        2013-07-09 13:27:38.310 ReproNFC_PFD_bug[9196:11603] Delete detected on row: 0
        2013-07-09 13:27:38.319 ReproNFC_PFD_bug[9196:11603] Updating event: 2013-07-09 02:48:49 +0000
        2013-07-09 13:27:38.320 ReproNFC_PFD_bug[9196:11603] Update detected on row: 0
        

        但是,这意味着您无法访问self.timeStamp,例如,在updateLastEventInContext:方法中,因为该对象已经在此时被删除(假设您在此之后立即保存了上下文)调用删除父对象。)

2 个答案:

答案 0 :(得分:0)

这为我可靠地修复了你的项目错误:

http://oleb.net/blog/2013/02/nsfetchedresultscontroller-documentation-bug/

答案 1 :(得分:0)

我认为此次崩溃可能与此有关:NSManagedObjectContextObjectsDidChangeNotification not always called instantly

因此,您可以在删除父级后尝试调用processPendingChanges以确保它级联到所有子级,然后获取控制器将接收已删除的正确事件。