5

I'm trying to efficiently batch delete a lot of NSManagedObjects (without using an NSBatchDeleteRequest). I have been following the general procedure in this answer (adapted to Swift), by batching an operation which requests objects, deletes, saves and then resets the context. My fetch request sets includesPropertyValues to false.

However, when this runs, at the point where each object is deleted from the context, the fault is fired. Adding logging as follows:

// Fetch one object without property values
let f = NSFetchRequest<NSManagedObject>(entityName: "Entity")
f.includesPropertyValues = false
f.fetchLimit = 1

// Get the result from the fetch. This will be a fault
let firstEntity = try! context.fetch(f).first!

// Delete the object, watch whether the object is a fault before and after
print("pre-delete object is fault: \(firstEntity.isFault)")
context.delete(firstEntity)
print("post-delete object is fault: \(firstEntity.isFault)")

yields the output:

pre-delete object is fault: true

post-delete object is fault: false

This occurs even when there are no overrides of any CoreData methods (willSave(), prepareForDeletion(), validateForUpdate(), etc). I can't figure out what else could be causing these faults to fire.


Update: I've created a simple example in a Swift playground. This has a single entity with a single attribute, and no relationships. The playground deletes the managed object on the main thread, from the viewContext of an NSPersistentContainer, a demonstrates that the object property isFault changes from true to false.

Community
  • 1
  • 1
Andrew Bennet
  • 2,600
  • 1
  • 21
  • 55
  • Maybe unrelated, but there is (or was) an issue with relationshipKeyPathsForPrefetching not working with child contexts - see [this question](https://stackoverflow.com/q/34018021/3985749). Might be worth testing using a parent context. – pbasdf Mar 01 '18 at 23:46
  • Does the entity for the object you're deleting have relationships of any kind? – Tom Harrington Mar 02 '18 at 16:08
  • @pbasdf Thanks - I will check whether the same happens on a main-queue context when I get time – Andrew Bennet Mar 02 '18 at 16:16
  • @TomHarrington Yes, the object being deleted (a `List`) has a many-to-many relationship to `Book` entities, with a delete rule of `nullify`. (`Book` objects also have relationships to another entity, but I didn't think that would matter). I put in the `relationshipKeyPathsForPrefetching` so that the `books` relationship would be prefetched. I thought - based on [this answer](https://stackoverflow.com/questions/12113961/core-data-delete-all-objects-of-an-entity-type-ie-clear-a-table/12116402#12116402) - that that would be enough, and no faults would need to be fired upon delete... – Andrew Bennet Mar 02 '18 at 16:20
  • I'm only using this method since `NSBatchDeleteRequest` (the obvious alternative) requires me to trigger an update to my table views: `NSFetchedResultsControllerDelegate` is not notified of changes. But perhaps it would be easier just to implement a batch delete notification which triggers a tableView reload... – Andrew Bennet Mar 02 '18 at 16:22
  • @pbasdf : I see the same behaviour with an entity with no relationships (so no prefetching), and on the main UI context - see the added example. – Andrew Bennet Mar 03 '18 at 14:29
  • @TomHarrington : I've added an example showing the behaviour occurring when there are no relationships in the model... – Andrew Bennet Mar 03 '18 at 14:29

2 Answers2

4

I think an authoritative answer would require a look at the Core Data source code. Since that's not likely to be forthcoming, here are some reasons I can think of that this might be necessary.

  • For entities that have relationships, it's probably necessary to examine the relationship to handle delete rules and maintain data integrity. For example if the delete rule is "cascade", it's necessary to fire the fault to figure out what related instances should be deleted. If it's "nullify", fire the fault to figure out which related instances need to have their relationship value set to nil.
  • In addition to the above, entities with relationships need to have validation checks performed on related instances. For example if you delete an object with a relationship that uses the "nullify" delete rule, and the inverse relationship is not optional, you would fail the validation check on the inverse relationship. Checking this likely triggers firing the fault.
  • Binary attributes can have data automatically stored in external files (the "allows external storage" option). In order to clean up the external file, it's probably necessary to fire the fault, in order to know which file to delete.

I think all of these could probably be optimized away. For example, don't fire faults if the entity has no relationships and has no attributes that use external storage. However, this is looking from the outside without access to source code. There might be other reasons that require firing the fault. That seems likely. Or it could be that nobody has attempted this optimization, for whatever reason. That seems less likely but is possible.

BTW I forked your playground code to get a version that doesn't rely on an external data model file, but instead builds the model in code.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • After reading around to answer this question, I find this to be the most thorough explanation. Thanks for the fork as well. – Lukas Mar 11 '18 at 20:22
  • Thanks for the answer, and also thanks for the fork - setting up the entity and model in code is v useful! – Andrew Bennet Mar 12 '18 at 09:06
1

Tom Harrington has explained it best. CoreData's internal implementation apparently requires to fire fault when marking an object to be removed from the persistent store, just like it would if you were accessing a property of the object. As explained in this answer, "An NSManagedObject is always dynamically rendered. Hence, if it is deleted, Core Data faults out the data".

This seems to be the normal behaviour at least for the moment being, not really an issue.

Lukas
  • 3,423
  • 2
  • 14
  • 26