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

Posted

技术标签:

【中文标题】核心数据级联删除不可靠?【英文标题】:Core Data Cascade Deletes not reliable? 【发布时间】:2013-07-09 05:45:25 【问题描述】:

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

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

这真的是一个错误吗,或者您能解释一下为什么我会看到这些奇怪的结果吗?


设置项目

您可以跳过这整个部分并改用download the xcodeproj。

    使用 Master-Detail Application 模板创建一个新项目。

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

    李>

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

    添加一个新实体,命名为EventParent

    创建与事件的关系,将其命名为child。建立反比关系,称之为parent。 (注意:这是一对一的关系。)

    单击 EventParent。单击其子关系。将其 Delete Rule 设置为 Cascade。这个想法是我们只会删除父对象。当父级被删除时,它会自动删除它的子级。

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

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

    在创建新事件的insertNewObject:方法中,确保创建相应的父级。

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

    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];
    

    在 Storyboard 中,删除 DetailViewController 的 segue。我们不需要它。

    NSFetchedResultsChangeDeleteNSFetchedResultsChangeUpdate 的情况下,在didChangeObject 事件中添加一些日志语句。让它输出indexPath.row

    最后,使当一个单元格被点击时,其对应的父级被删除。通过在MasterViewController.m 文件中创建- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 来做到这一点:

    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.
    

到目前为止的设置摘要:

我们不会过多地接触 NSFetchedResultsController。我们将允许它观察和显示事件。 每当我们删除 EventParent 时,我们都希望删除其对应的 Event。 为了增加另一个变化,我们希望在删除事件时更新 hasMovedUp 属性。

重现错误

    运行应用程序

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

    点击顶部记录并观察应用崩溃(注意: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
    

    现在取消注释上面的[context deleteObject:event] 行。

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

    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
    

日志中有两点不同:

    在我们更新下一个事件之前检测到删除。

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

(注意:即使在 5% 的时间内,我们预计错误会发生但实际上并没有发生,日志事件也会以相同的确切顺序输出。)


例外

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

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

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

引发的异常是:

CoreData:错误:严重的应用程序错误。在核心数据更改处理期间捕获到异常。这通常是 NSManagedObjectContextObjectsDidChangeNotification 观察者中的一个错误。 *** -[_PFBatchFaultingArray objectAtIndex:]: index (19789522) 越界 (2) with userInfo (null)

.

* 由于未捕获的异常“NSRangeException”而终止应用程序,原因:“* -[_PFBatchFaultingArray objectAtIndex:]: index (19789522) beyond bounds (2)”


含义

这似乎暗示了依赖级联删除规则与自己显式删除对象是不一样的。

换句话说……

这个:

 [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。

A:显式调用删除

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

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

    // (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 的recommendation:

通过忽略indexPath参数,只在didChangeObject:方法中使用anObject参数,可以规避问题:

        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: 方法中,因为此时对象已经被删除(假设您在调用 delete 后立即保存上下文父对象)。

【问题讨论】:

+1 以获得如此出色的问题描述和可重复的示例!!!! - 我不知道问题的原因,但似乎我在这里找到的解决方法:***.com/questions/11432556/… 对您的情况也有帮助。这个想法是,在更新通知的情况下,您直接使用didChangeObject:...anObject 参数。将该解决方法应用于您的示例项目后,它不再崩溃。 我同意马丁的观点。好问题!!打开雷达可能会有所帮助。 我向 Apple 提交了该错误。 @MartinR:感谢您的解决方法!您是否也提交了您的错误版本? (如果没有,你能这样做吗?似乎increase the priority when there are duplicates filed)这似乎是一个令人讨厌的错误,特别是因为Apple的默认项目模板在这种情况下崩溃,并且很难追查导致它的原因发生。这也让你想知道这可能会导致哪些其他类型的问题...... 我没有提交错误,因为我无法可靠地重现问题。 - 我的解决方法对您的情况有帮助吗? @MartinR:解决方法似乎确实有效。谢谢!但是,这仍然有点令人不安,因为日志显示它正在尝试更新第 1 行,而此时该行甚至不应该存在。似乎代码中的其他地方可能会遇到我目前不知道的相关问题。 【参考方案1】:

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

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

【讨论】:

【参考方案2】:

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

因此,您可以在删除父级后尝试调用 processPendingChanges 以确保它级联到所有子级,然后 Fetch Controller 将收到两者都已被删除的正确事件。

【讨论】:

以上是关于核心数据级联删除不可靠?的主要内容,如果未能解决你的问题,请参考以下文章

sql级联更新和级联删除不起作用

实体框架核心级联删除错误

数据库中啥是“级联更新关联字段”和“级联删除关联字段”

删除父子记录而不使用删除级联

PythonDjango数据模型级联删除级联更新ER图导出等

SQL 级联更新,级联删除的概念