Why are Core Data NSManagedObject faults fired upon deletion?
Asked Answered
B

2

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.

Bronwen answered 1/3, 2018 at 22:36 Comment(7)
Maybe unrelated, but there is (or was) an issue with relationshipKeyPathsForPrefetching not working with child contexts - see this question. Might be worth testing using a parent context.Icelandic
Does the entity for the object you're deleting have relationships of any kind?Counterpressure
@Icelandic Thanks - I will check whether the same happens on a main-queue context when I get timeBronwen
@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 - that that would be enough, and no faults would need to be fired upon delete...Bronwen
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...Bronwen
@Icelandic : I see the same behaviour with an entity with no relationships (so no prefetching), and on the main UI context - see the added example.Bronwen
@TomHarrington : I've added an example showing the behaviour occurring when there are no relationships in the model...Bronwen
C
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.

Counterpressure answered 5/3, 2018 at 23:38 Comment(2)
After reading around to answer this question, I find this to be the most thorough explanation. Thanks for the fork as well.Nickola
Thanks for the answer, and also thanks for the fork - setting up the entity and model in code is v useful!Bronwen
N
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.

Nickola answered 11/3, 2018 at 20:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.