Core Data级联删除可靠吗?

5
似乎在使用NSFetchedResultsController时存在一个bug,每当prepareForDelete更新模型时,如果删除的原因是级联删除规则,则会出现问题。这似乎意味着隐式删除(通过级联删除)的行为与显式删除非常不同。这真的是一个bug吗?还是你能解释一下我为什么会看到这些奇怪的结果?

设置项目

您可以跳过整个部分,下载xcodeproj

  1. Create a new project with the Master-Detail Application template.

  2. Add a new attribute to the Event entity. (This is important since we want to be able to update an attribute without it causing the NSFetchedResultsController to reorder any of its items. Otherwise it will send the NSFetchedResultsChangeMove event rather than the NSFetchedResultsChangeUpdate event).

  3. Call the attribute hasMovedUp, and make it a Boolean. (Note: it may seem silly to create such an attribute, but this is only an example, and I tried to reduce it to the minimum number of steps needed in order to reproduce this bug.)

  4. Add a new entity, call it EventParent.

  5. Create a relationship to Event, call it child. Make the inverse relationship as well, call it parent. (Note: this is a 1:1 relationship.)

  6. Click on EventParent. Click on its child relationship. Set its Delete Rule to Cascade. The idea is that we will only be deleting parent objects. When the parent is deleted, it will automatically delete its child.

  7. Leave the Event's parent relationship Delete Rule as Nullify.

  8. Create NSManagedObject Subclasses via Xcode for both entities.

  9. In the insertNewObject: method, where the new Event is created, make sure to create a corresponding parent.

  10. In the Event.m file, automatically assign the last event's hasMovedUp to be YES by declaring a prepareForDeletion event:

    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. In the Storyboard, delete the segue to the DetailViewController. We won't be needing it.

  12. Add some log statements in the didChangeObject event in the case of a NSFetchedResultsChangeDelete and NSFetchedResultsChangeUpdate. Have it output indexPath.row.

  13. Finally, make it so that when a cell is tapped, its corresponding parent is deleted. Do this by creating the - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { in the MasterViewController.m file:

    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属性。

重现错误

  1. Run the App

  2. Create 2 records by tapping the plus button twice.

  3. Tap the top record and watch the app crash (Note: 95% of the time it will crash. If it doesn't crash for you, restart the app until it does). Here are some useful NSLogs:

    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. Now uncomment the [context deleteObject:event] line above.

  5. Run the app and notice that it no longer crashes. The logs:

    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
    

日志中有两个不同的地方:

  1. 在更新下一个事件之前,删除被检测到。

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

(注意:即使在我们预计出现错误但实际上没有出现错误的5%时间内,日志事件也会按照完全相同的顺序输出。)


异常

异常会在configureCell:atIndexPath:方法的以下行引发:

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

它引发异常的原因是在不再存在的行上检测到了更新(1)。请注意,当异常未发生时,更新会在正确的行上被检测到(0),因为顶部行已被删除,底部行现在位于索引0。引发的异常为: CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. *** -[_PFBatchFaultingArray objectAtIndex:]: index (19789522) beyond bounds (2) with userInfo (null). * Terminating app due to uncaught exception 'NSRangeException', reason: '* -[_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];

解决方法

更新 6/9/13:

Xcodeproj 已更新,包含多个不同解决方法的 #define 语句(在 Event.h 文件中)。将这三个都保持未定义状态即可重现错误。定义其中任意一个,即可实现相应的解决方法。目前有三种解决方法:A、B 和 C。

A:显式调用 delete

这种解决方法与上面提到的内容重复,但出于完整性考虑,在此也一并列出。

通过不依赖级联删除,而是自己调用 delete,就可以使所有东西正常工作:

    // (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的建议

通过忽略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秒延迟:
如果在prepare for delete后使用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

然而,这意味着您不能在updateLastEventInContext:方法中访问self.timeStamp,例如,因为对象在那一点上已经被删除(这是假设您在调用删除父对象后立即保存上下文)。

对于一个具有可重现示例的出色问题描述,点赞 +1!!! - 我不知道问题的原因,但似乎我在这里找到的解决方法也可以帮助你的情况。想法是,在更新通知的情况下,直接使用 didChangeObject:...anObject 参数。将该解决方法应用于您的示例项目后,它不再崩溃。 - Martin R
我同意Martin的观点。好问题!打开一个radar可能会有帮助。 - Lorenzo B
我向苹果报告了这个错误。@MartinR: 感谢您提供的解决方法!您是否也报告了您版本的错误?(如果没有,能否请您报告一下?这似乎会[在有重复提交时增加优先级](http://blackpixel.com/blog/2012/02/radar-or-gtfo.html)) 这个错误似乎很严重,特别是因为苹果的默认项目模板在这种情况下会崩溃,而且很难追踪是什么原因导致了这种情况。它还会让你想知道这可能会引起什么其他问题... - Senseful
我没有提交错误报告,因为我无法可靠地重现问题。- 我的解决方法对你有帮助吗? - Martin R
@MartinR:这个变通方法似乎有效。谢谢!然而,仍然有点不安,因为日志显示它正在尝试更新第1行,但在那个时候根本不应该存在。此外,代码中可能还有其他地方可能会遇到相关问题,我目前还不知道。 - Senseful
2个回答

0

0

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