Nsmanagedobjectcontext's Propagatesdeletesatendofevent Set to False Causes Error on Save

What does -setPropagatesDeletesAtEndOfEvent: actually do?

This controls whether deletes are propagated at the time of a change event i.e. following a didChange... method call (explicit or synthesized.)

To my knowledge this only affects the flags on objects in memory marking them as to be deleted or not. It only affects how the context manages the in-memory objects and not how the context manages on disk deletions. When set to YES, it cause the context to flag the objects as one to be deleted and treat it as removed from the object graph. However, an undo will reverse the flag just as if you had directly deleted the object.

This flag is available because there maybe times when when you need objects to hang around for a while even after a delete or cascade relationship has ended. When NO the context will return the "deleted" object up to the point of the next save.

This is an advanced feature and it is seldom used.

What is the most efficient way to delete a large number (10.000+) objects in Core Data?

EDIT: Just watched 2015 WWDC "What's New in Core Data" (it's always the first video I watch, but I've been very busy this year) and they announced a new API: NSBatchDeleteRequest that should be much more efficient than any previous solution.


Efficient has multiple meanings, and most often means some sort of trade-off. Here, I assume you just want to contain memory while deleting.

Core Data has lots of performance options, beyond the scope of any single SO question.

How memory is managed depends on the settings for your managedObjectContext and fetchRequest. Look at the docs to see all the options. In particular, though, you should keep these things in mind.

Also, keep in mind the performance aspect. This type of operation should be performed on a separate thread.

Also, note that the rest of your object graph will also come into play (because of how CoreData handles deletion of related objects.

Regarding memory consumption, there are two properties on MOC in particular to pay attention to. While there is a lot here, it is by no means close to comprehensive. If you want to actually see what is happening, NSLog your MOC just before and after each save operation. In particular, log registeredObjects and deletedObjects.

  1. The MOC has a list of registered objects. By default, it does not retain registered objects. However, if retainsRegisteredObjects is YES, it will retain all registered objects.

  2. For deletes in particular, setPropagatesDeletesAtEndOfEvent tells the MOC how to handle related objects. If you want them handled with the save, you need to set that value to NO. Otherwise, it will wait until the current event is done

  3. If you have really large object sets, consider using fetchLimit. While faults do not take a lot of memory, they still take some, and many thousands at a time are not insignificant. It means more fetching, but you will limit the amount of memory

  4. Also consider, any time you have large internal loops, you should be using your own autorelease pool.

  5. If this MOC has a parent, saving only moves those changes to the parent. In this case, if you have a parent MOC, you are just making that one grow.

For restricting memory, consider this (not necessarily best for your case -- there are lots of Core Data options -- only you know what is best for your situation, based on all the options you are using elsewhere.

I wrote a category on NSManagedObjectContext that I use for saving when I want to make sure the save goes to the backing store, very similar to this. If you do not use a MOC hierarchy, you don't need it, but... there is really no reason NOT to use a hierarchy (unless you are bound to old iOS).

- (BOOL)cascadeSave:(NSError**)error {
__block BOOL saveResult = YES;
if ([self hasChanges]) {
saveResult = [self save:error];
}
if (saveResult && self.parentContext) {
[self.parentContext performBlockAndWait:^{
saveResult = [self.parentContext cascadeSave:error];
}];
}
return saveResult;
}

I modified your code a little bit...

+ (void)deleteRelatedEntitiesInManagedObjectContext:(NSManagedObjectContext *)context 
{
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
[context setUndoManager:nil];

[fetch setEntity:[NSEntityDescription entityForName:NSStringFromClass(self) inManagedObjectContext:context]];
[fetch setIncludesPropertyValues:NO];
[fetch setFetchLimit:500];

NSError *error = nil;
NSArray *entities = [context executeFetchRequest:fetch error:&error];
while ([entities count] > 0) {
@autoreleasepool {
for (NSManagedObject *item in entities) {
[context deleteObject:item];
}
if (![context cascadeSave:&error]) {
// Handle error appropriately
}
}
entities = [context executeFetchRequest:fetch error:&error];
}
}

Performance considerations on deleting Managed Objects using Cascade rule in Core Data

You can't do much about the time it takes to traverse and remove the objects in the database. However, you can do it in the background so the UI does not get blocked.

For example, something like (code assumes ARC - and was just typed - not compiled)...

- (void)deleteAllLevelsInMOC:(NSManagedObjectContext*)moc
{
// Create a context with a private queue, so access happens on a separate thread.
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
// Insert this context into the current context hierarchy
context.parentContext = context;
// Execute the block on the queue of the context
context.performBlock:^{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"FirstLevel"];
// This will not load any properties - just object id
fetchRequest.includesPropertyValues = NO;
// Iterate over all first-level objects, deleting each one
[[context executeFetchRequest:fetchRequest error:0]
enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[context deleteObject:obj];
}];
// Push the changes out of the context
[context save:0];
}];
}

Note, that you could add a RootLevel object to your data model, and give it a one-to-many relationship to first level objects. Then, (as long as you only have one root object) all you have to do is delete that one root object, and it will delete everything else inside CoreData. If you have lots of FirstLevel objects, that would be the way to go.

Alternatively, you could connect your "new" context directly to the persistent store, make the changes, and have the other context watch for change notifications.

BTW, if you are using UIManagedDocument, you get this kind of backgrounding for free, because there is already a parent context running on its own queue, and all changes to the main context are passed off to the parent to actually do the database work.

EDIT

You can do it manually, but you have to turn off the cascade rule. I have found that I want CoreData to do as much for me as possible. It reduces my margin for error.

If you are already deleting in a background thread, then how are you seeing performance issues? You must be querying during that deletion... which means you are using a MOC that does all the work.

You should take a lesson from UIManagedDocument. you should have a MOC that runs on a private queue. It does all the real work. You have a child MOC that just passes work to that worker queue.

If you are actually querying for objects not in the graph you are deleting, they could get hung up by the serializing properties of the persistent store coordinator. If that is the case, you should consider either a separate coordinator, or just have two stores.

That way, you can be deleting the objects in your graph as long as you want, while still being responsive to requests for the other objects.



Related Topics



Leave a reply



Submit