Using NSBatchDeleteRequest to delete batches in Core Data

An NSBatchDeleteRequest can be used to efficiently delete a batch of entries from a Core Data SQLite persistent store. It runs faster than deleting Core Data entities yourself on a managed object context as they operate at the SQL level in the persistent store itself. Its usage, however, is a bit less simple compared to the usual delete method which you can use on a managed object context.

So before we dive in and start creating an NSBatchDeleteRequest, we first go over some common pitfalls to give you all the information you need for executing batch deletions.

Common pitfalls when using an NSBatchDeleteRequest

Setting up an NSBatchDeleteRequest is fairly simple. It requires either passing in an NSFetchRequest instance or a collection of managed object identifiers. After that, you simply execute the batch delete request on your managed object context. In the end, you’ve prepared yourself for debugging Core Data, so what can go wrong? Well, it all seems to be easy until you dive in and start using it more intensively.

Updating in-memory objects

After executing the NSBatchDeleteRequest you’ll find out that your entities still exist in memory. This is a result of the fact that the NSBatchDeleteRequest is an NSPersistentStoreRequest which operates at the SQL level in the persistent store itself. Therefore, you need to manually update your in-memory objects after execution.

This can be done quite easily by calling the mergeChanges method on your managed object context.

let fetchRequest = NSFetchRequest(entityName: "Content")
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as! NSBatchDeleteResult
let changes: [AnyHashable: Any] = [
    NSDeletedObjectsKey: result.result as! [NSManagedObjectID]
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [myManagedObjectContext])

This will update your in-memory object and as well send out notifications like NSManagedObjectContextObjectsDidChange.

Validation rules like Core Data relationships are ignored

Entity relationships need to be deleted manually. Any deletion rules you’ve set on an entity will not be respected. Therefore, you could easily end up with a Core Data error telling you that your persistent store is in an invalid state. The following error is an example of that:

CoreData: error: Unhandled error from executeBatchDeleteRequest Constraint violation: 
Batch delete failed due to mandatory OTO nullify inverse on ContentSyncedInfo/content and userInfo {
    "_NSCoreDataOptimisticLockingFailureConflictsKey" =     (
        ""
    );
}

A solution would be to use multiple batch delete requests to manually delete each relationship as well. However, in this case, it might be easier to go for the regular delete method which does respect validation rules.

Initializing with Object IDs requires the same entity

Another common pitfall is to initialize an NSBatchDeleteRequest with a collection of NSManagedObjectID instances from different entities. You’ll soon enough encounter the following error:

mismatched objectIDs in batch delete initializer
(null)
Invalid formatIndex

It basically comes down to the fact that you’re allowed to only use object IDs for objects of the same entity. It’s too bad that the Apple documentation is not really informative here. It’s good to point out that Apple does have some good documentation which does quote the following related information:

The goal of a batch delete is to delete one or more specific entities that are stored in a SQLite persistent store

It clearly points out “specific entities” which makes sense looking at the above error.

Putting it all together in a handy extension

Most of the pitfalls can’t be solved with a handy extension. We can, however, make it easy to execute a batch delete request and directly update the in-memory objects. We do this by creating a simple extension on NSManagedObjectContext.

extension NSManagedObjectContext {
    
    /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
    ///
    /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
    /// - Throws: An error if anything went wrong executing the batch deletion.
    public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
        batchDeleteRequest.resultType = .resultTypeObjectIDs
        let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
        let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
    }
}

You simply pass your NSBatchDeleteRequest instance into this method which will automatically call the mergeChanges method on the given managed object context.

let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Content")
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
managedObjectContext.executeAndMergeChanges(using: batchDeleteRequest)

Conclusion

After all, you have to ask yourself whether you can use an NSBatchDeleteRequest for your deletion. This should be easy by taking into account the following points. In other words, don’t use an NSBatchDeleteRequest if:

  • The entity you’re deleting contains validation rules like relationships
  • You’re deleting multiple different kinds of entities
  • You are not using an SQLite backing store

Hopefully, the above extension makes it all a bit easier to implement and use batch delete requests with Core Data. In the end, it’s a very performant way of deleting a batch of Core Data entities.

Thanks!