Note that there are some explanatory texts on larger screens.

plurals
  1. POCore Data Cascade Deletes not reliable?
    primarykey
    data
    text
    <p>There seems to be a bug with <strong>NSFetchedResultsController</strong> whenever a <strong>prepareForDelete</strong> updates the model when the cause of the deletion is a <strong>cascade delete</strong> rule.</p> <p>It seems to imply that an implicit delete (via cascade delete) behaves very differently than an explicit delete.</p> <p>Is this really a bug, or can you explain why I am seeing these strange results?</p> <hr> <h1>Setting up the Project</h1> <p>You can skip this entire section and <a href="https://github.com/senseful/nfrc-prepareForDelete-CascadeDeleteBug" rel="nofollow noreferrer">download the xcodeproj</a> instead.</p> <ol> <li><p>Create a new project with the <strong>Master-Detail Application</strong> template.</p></li> <li><p>Add a new attribute to the <strong>Event</strong> 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 <code>NSFetchedResultsChangeMove</code> event rather than the <code>NSFetchedResultsChangeUpdate</code> event).</p></li> <li><p>Call the attribute <code>hasMovedUp</code>, and make it a <code>Boolean</code>. (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.)</p></li> <li><p>Add a new entity, call it <code>EventParent</code>.</p></li> <li><p>Create a relationship to <strong>Event</strong>, call it <code>child</code>. Make the inverse relationship as well, call it <code>parent</code>. (Note: this is a 1:1 relationship.)</p></li> <li><p>Click on EventParent. Click on its child relationship. Set its <strong>Delete Rule</strong> to <strong>Cascade</strong>. The idea is that we will only be deleting parent objects. When the parent is deleted, it will automatically delete its child.</p></li> <li><p>Leave the Event's parent relationship Delete Rule as <strong>Nullify</strong>.</p></li> <li><p>Create NSManagedObject Subclasses via Xcode for both entities.</p></li> <li><p>In the <code>insertNewObject:</code> method, where the new Event is created, make sure to create a corresponding parent.</p></li> <li><p>In the <code>Event.m</code> file, automatically assign the last event's <code>hasMovedUp</code> to be <code>YES</code> by declaring a <code>prepareForDeletion</code> event:</p> <pre><code>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]; </code></pre></li> <li><p>In the Storyboard, delete the segue to the DetailViewController. We won't be needing it.</p></li> <li><p>Add some log statements in the <code>didChangeObject</code> event in the case of a <code>NSFetchedResultsChangeDelete</code> and <code>NSFetchedResultsChangeUpdate</code>. Have it output <code>indexPath.row</code>.</p></li> <li><p>Finally, make it so that when a cell is tapped, its corresponding parent is deleted. Do this by creating the <code>- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {</code> in the <code>MasterViewController.m</code> file:</p> <pre><code>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. </code></pre></li> </ol> <p><strong>Summary of the setup so far:</strong></p> <ul> <li>We are not going to touch the NSFetchedResultsController much. We will allow it to observe and show Events.</li> <li>Whenever we delete an EventParent, we want its corresponding Event to be deleted.</li> <li>To add another twist, we want the <code>hasMovedUp</code> property to be updated whenever an Event is deleted.</li> </ul> <hr> <h1>Reproducing the bug</h1> <ol> <li><p>Run the App</p></li> <li><p>Create 2 records by tapping the plus button twice.</p></li> <li><p>Tap the <strong>top</strong> 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:</p> <pre><code>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 </code></pre></li> <li><p>Now uncomment the <code>[context deleteObject:event]</code> line above.</p></li> <li><p>Run the app and notice that it no longer crashes. The logs:</p> <pre><code>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 </code></pre></li> </ol> <p>Two things are different in the logs:</p> <ol> <li><p>The deletion is detected before we update the next Event.</p></li> <li><p>The update takes place on row 0 (the correct row) rather than row 1 (the incorrect row). Read on for an explanation of why 0 is the correct number.</p></li> </ol> <p>(Note: even during that 5% of the time when we expect the error to occur but it doesn't, the log events are outputed in the same exact order.)</p> <hr> <h1>The Exception</h1> <p>The exception is raised on the following line in <code>configureCell:atIndexPath:</code>:</p> <pre><code>NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath]; </code></pre> <p>The reason it causes an exception is because the update is detected on a row that no longer exists (1). Notice that when the exception does not occur, the update is detected on the correct row (0), since the top row would have been deleted, and the bottom row is now at index 0.</p> <p>The exception raised is:</p> <blockquote> <p>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)</p> <p>.</p> <p><strong>* Terminating app due to uncaught exception 'NSRangeException', reason: '*</strong> -[_PFBatchFaultingArray objectAtIndex:]: index (19789522) beyond bounds (2)'</p> </blockquote> <hr> <h1>Implications</h1> <p>This seems to suggest that relying on the cascade delete rule is not the same as explicitly deleting the object yourself.</p> <p>In other words...</p> <p>This:</p> <pre><code> [context deleteObject:parent]; // parent will auto-delete the corresponding Event via a cascade rule </code></pre> <p>… is not the same as this:</p> <pre><code> [context deleteObject:parent]; [context deleteObject:event]; </code></pre> <hr> <h1>Workarounds</h1> <h3>Update 6/9/13:</h3> <p>The <a href="https://github.com/senseful/nfrc-prepareForDelete-CascadeDeleteBug" rel="nofollow noreferrer">Xcodeproj</a> was updated to include several <code>#define</code> statements for the different workarounds available (in the <strong>Event.h</strong> file). Leave all 3 undefined to reproduce the bug. Define any 1 of these to see a particular workaround implemented. So far there are three workarounds: A, B, and C.</p> <p><strong>A: Explicitly calling delete</strong></p> <p>This solution is a duplicate of what has already been mentioned above, but it is included for completeness sake.</p> <p>By not relying on the Cascade Delete, and instead calling the delete yourself, everything will work fine:</p> <pre><code> // (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 </code></pre> <p>Logs:</p> <pre><code>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 </code></pre> <p><strong>B: Using @MartinR's <a href="https://stackoverflow.com/questions/11432556/nsrangeexception-exception-in-nsfetchedresultschangeupdate-event-of-nsfetchedres">recommendation</a>:</strong> </p> <p>By ignoring the <code>indexPath</code> parameter, and only using the <code>anObject</code> parameter in the <code>didChangeObject:</code> method, you can circumvent the problem:</p> <pre><code> 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; </code></pre> <p>However, the logs still display things out of order:</p> <pre><code>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 </code></pre> <p>Which leads me to believe that this solution could cause related problems in other parts of my code.</p> <p><strong>C: Using a 0-second delay in prepareForDelete:</strong></p> <p>If you update the object after a zero-second delay in the prepare for delete, this will circumvent the bug:</p> <pre><code>- (void)updateLastEventInContext:(NSManagedObjectContext *)context { // warning: do not call self.&lt;anything&gt; 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]; } </code></pre> <p>Additionally, the log order seems to be correct, so you can resume calling indexPath on the NSFetchedResultsController (I.e. you don't need to use workaround B):</p> <pre><code>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 </code></pre> <p>However, this means you cannot access <code>self.timeStamp</code>, for example, in the <code>updateLastEventInContext:</code> method, since the object will already have been deleted at that point (this is assuming you save the context immediately after the call to delete the parent object).</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload