In a managed object i have stored a path to an image file in the application's container.
When the managed object get’s deleted, the image file should be moved to trash. This should be done as late as possible, so that i can provide undo functionality for as long as possible.
I was following the answers of this question: How to handle cleanup of external data when deleting Core Data objects, and was overriding -didSave in my managed object subclass to trash the files.
Turns out, this works only if:
- the managed object has been added,
- the managed object context has been saved,
- the managed object has been deleted,
- the managed object context is saved.
In the following case however -isSaved is not called in the managed object:
- the managed object has been added,
- the managed object has been deleted,
- the managed object context is saved.
I understand why this happens. As the deleted object was never persisted in the first place, it will not be saved after the deletion, -didSave is not called.
Now i am looking for another place from which to move the referenced file to the trash. Where could that be?
You have two choices for how to implement this using managed object methods: use the managed object life cycle events, or use validation.
However, doing so has some tradeoffs and risks There are other approaches that may work better for you (see recommendations).
Managed Object Life Cycle
Managed objects are observed by the NSManagedObjectContext
that owns them. This is the "managed" in managed object. Most of what a NSManagedObject
instance does is actually performed by the NSManagedObjectContext
. The managed object is informed of changes through the life cycle event methods: awakeFromFetch
, awakeFromInsert
, awakeFromSnapshotEvents:
, didSave
, didTurnIntoFault
, prepareForDeletion
, etc. When implementing these methods you must be careful to not change the managed object in a way that would mark it as "dirty" in the context or would otherwise alter the current transaction. For example, attempting to "resurrect" a deleted object in didSave
, or changing relationships in awakeFromFetch
, or accessing a property in didTurnIntoFault
(which would fire a fault, which would be bad).
During a typical deletion of a saved object, the life cycle events are called in the following order:
- prepareForDeletion
- validateForDelete: This is not considered a life cycle event, but it is very important. More on that in a moment.
- willSave
- prepareForDeletion
- didSave
- didTurnIntoFault
If a save is subsequently done in the object's parent contexts, additional life cycle events may occur on the instances owned by those contexts. This can be very important to keep in mind when dealing with shared resources outside of Core Data.
When an object has not been saved and is deleted from a context, the life cycle events occur in this order:
- prepareForDeletion
- didTurnIntoFault
Using managed object life cycle methods may not be a good solution in this instance. As you can see, prepareForDeletion
is called in the scenarios you are interested in - but it happens before the delete is validated in the case of a save operation.
Validation
Validation is an important Core Data capability. When objects are saved Core Data applies validation rules set in the model as well as any custom validation implemented in NSManagedObject classes. In the case of a delete, Core Data applies the delete rules defined in the model as part of the save operation. prepareForDeletion
is invoked before validation occurs - so if you were trashing data as part of prepareForDeletion
, you may be removing data for an object that will not actually be deleted as part of a save. This may cause you some problems.
You can implement your deletion as part of validateForDelete:
or as a custom validation method that checks the state of the object (isDeleted
, isInserted
, etc.). The super implementation of validateForDelete:
will execute the delete rules, be sure to call it appropriately. Validation will be called automatically as part of a save operation, but you can call it manually at any point (and this is recommended). To perform validation manually, call the appropriate method from your application, in this case validateForDelete:
. Check the BOOL result, and if it returns NO, handle the error appropriately.
Recommendations
It may be best to implement writing the image data to the local filesystem as part of validation or saving. When Core Data performs a save, it's essentially commiting all of the changes in the context as a transaction. When dealing with external resources, it can make a lot of sense to make commiting changes to those external resources part of the same process. For example, in your validateImageURL:error:
method you should at the least validate that the given URL is local filesystem URL, and that you can write to it. In willSave
/didSave
you may write to the URL specified by imageURL
if the object has been inserted or updated, and delete the data at imageURL
if it has been deleted. In the case of an object that has not yet been saved but is being deleted from the context, the data would not have yet been commited to the local filesytem. It would only exist in memory, like everything else associated with the object.
Note that no matter how you implement your reading, writing, and deleting of external data, you should do so using the NSFileCoordinator APIs to coordinate access to the files and directories.
There are still issues with this approach. An NSManagedObjectContext
(and it's objects) is just a collection of references to data in a persistent store. If you are saving external data from an NSManagedObject
, you can run into problems when you have multiple contexts, nested contexts (which you SHOULD be using!), etc. The NSPersistentStore is what manages the persistence of an NSManagedObject
's data, and ideally your interaction with the filesytem would happen at that level - which would address some of the issues that I've mentioned and more. The best way to do so would be to use Core Data's external storage capabilities to manage this data, as that is already built into (some) persistent stores.
You could also attempt to subclass NSPersistentStoreCoordinator
and override the method executeRequest:withContext:error:
to implement your own external storage.
Excellent point - I updated my answer over on that question too, and I'll expand on my favorite approach here:
Forget about will/did save. Not reliable in this case.
Implement prepareForDeletion:
. If you don't need undo, and are sure the deletion will succeed, delete the file on the spot. Otherwise, add the file to a convenient registry (a NSMutableSet
owned by the context owner, or some such.)
If you do need undo/redo, implement awakeFromSnapshotEvents:
to catch un-deletion and re-deletion. Remove/re-add the file from/to the registry as needed.
Register for a didSave notification somewhere convenient. When a save happens, delete all the files listed in the registry and clear it.
This all assumes, by the way, that no two of your owning objects will ever own the same file. If this is a possibility, things get much more complicated - but I imagine you've set up your model specifically to avoid that happening.