Even though Core Data has been around for many years on OS X and iOS, a feature that was added only recently are batch updates. Developers have been asking for this feature for many years and Apple finally found a way to integrate it into Core Data. In this tutorial, I will show you how batch updates work and what they mean for the Core Data framework.
1. The Problem
Core Data is great at managing object graphs. Even complex object graphs with many entities and relationships aren't much of a problem for Core Data. However, Core Data does have a few weak spots, updating large numbers of records being one of them.
The problem is easy to understand. Whenever you update a record, Core Data loads the record into memory, updates the record, and saves the changes to the persistent store, a SQLite database for example.
If Core Data needs to update a large number of records, it needs to load every record into memory, update the record, and send the changes to the persistent store. If the number of records is too large, iOS will simply bail out due to a lack of resources. Even though a device running OS X may have the resources to execute the request, it will be slow and consume a lot of memory.
An alternative approach is to update the records in batches, but that too takes a lot of time and resources. On iOS 7, it's the only option iOS developers have. That's no longer the case since iOS 8 and OS X Yosemite.
2. The Solution
On iOS 8 and up and OS X Yosemite and up, it's possible to talk directly to the persistent store and tell it what you'd like to change. This generally involves updating an attribute. Apple calls this feature batch updates.
There are a number of pitfalls to watch out for though. Core Data does a lot of things for you and you may not even realize it until you use batch updates. Validation is a good example. Because Core Data performs batch updates directly on the persistent store, such as a SQLite database, Core Data isn't able to perform any validation on the data you write to the persistent store. This means that you are in charge of making sure you don't add invalid data to the persistent store.
When would you use batch updates? Apple recommends to only use this feature if the traditional approach is too resource or time intensive. If you need to mark hundreds or thousands of email messages as read, then batch updates is the best solution on iOS 8 and up and OS X Yosemite and up.
3. How Does It Work?
To illustrate how batch updates work, I suggest we revisit Done, a simple Core Data application that manages a to-do list. We'll add a button to the navigation bar that marks every item in the list as done.
Step 1: Projet Setup
Download or clone the project from GitHub and open it in Xcode 7. Run the application in the simulator and add a few to-do items.
Step 2: Create Bar Button Item
Open ViewController.swift and declare a property, checkAllButton
, of type UIBarButtonItem
at the top.
import UIKit import CoreData class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate { let ReuseIdentifierToDoCell = "ToDoCell" @IBOutlet weak var tableView: UITableView! var managedObjectContext: NSManagedObjectContext! var checkAllButton: UIBarButtonItem! ... }
Initialize the bar button item in the viewDidLoad()
method of the ViewController
class and set it as the left bar button item of the navigation item.
// Initialize Check All Button checkAllButton = UIBarButtonItem(title: "Check All", style: .Plain, target: self, action: "checkAll:") // Configure Navigation Item navigationItem.leftBarButtonItem = checkAllButton
Step 3: Implement checkAll(_:)
Method
The checkAll(_:)
method is fairly easy, but there are a few caveats to watch out for. Take a look at its implementation below.
func checkAll(sender: UIBarButtonItem) { // Create Entity Description let entityDescription = NSEntityDescription.entityForName("Item", inManagedObjectContext: managedObjectContext) // Initialize Batch Update Request let batchUpdateRequest = NSBatchUpdateRequest(entity: entityDescription!) // Configure Batch Update Request batchUpdateRequest.resultType = .UpdatedObjectIDsResultType batchUpdateRequest.propertiesToUpdate = ["done": NSNumber(bool: true)] do { // Execute Batch Request let batchUpdateResult = try managedObjectContext.executeRequest(batchUpdateRequest) as! NSBatchUpdateResult // Extract Object IDs let objectIDs = batchUpdateResult.result as! [NSManagedObjectID] for objectID in objectIDs { // Turn Managed Objects into Faults let managedObject = managedObjectContext.objectWithID(objectID) managedObjectContext.refreshObject(managedObject, mergeChanges: false) } // Perform Fetch try self.fetchedResultsController.performFetch() } catch { let updateError = error as NSError print("\(updateError), \(updateError.userInfo)") } }
Create Batch Request
We start by creating an NSEntityDescription
instance for the Item entity and use it to initialize an NSBatchUpdateRequest
object. The NSBatchUpdateRequest
class is a subclass of NSPersistentStoreRequest
.
// Create Entity Description let entityDescription = NSEntityDescription.entityForName("Item", inManagedObjectContext: managedObjectContext) // Initialize Batch Update Request let batchUpdateRequest = NSBatchUpdateRequest(entity: entityDescription!)
We set the result type of the batch update request to .UpdatedObjectIDsResultType
, a member value of the NSBatchUpdateRequestResultType
enum. This means that the result of the batch update request will be an array containing the object IDs, instances of the NSManagedObjectID
class, of the records that were changed by the batch update request.
// Configure Batch Update Request batchUpdateRequest.resultType = .UpdatedObjectIDsResultType
We also populate the propertiesToUpdate
property of the batch update request. For this example, we set propertiesToUpdate
to a dictionary containing one key, "done"
, with value NSNumber(bool: true)
. This means that every Item record will be set to done, which is what we're after.
// Configure Batch Update Request batchUpdateRequest.propertiesToUpdate = ["done": NSNumber(bool: true)]
Execute Batch Update Request
Even though batch updates bypass the managed object context, executing a batch update request is done by calling executeRequest(_:)
on an NSManagedObjectContext
instance. This method accepts one argument, an instance of the NSPersistentStoreRequest
class. Because executeRequest(_:)
is a throwing method, we execute the method in a do-catch
statement.
do { // Execute Batch Request let batchUpdateResult = try managedObjectContext.executeRequest(batchUpdateRequest) as! NSBatchUpdateResult } catch { let updateError = error as NSError print("\(updateError), \(updateError.userInfo)") }
Updating the Managed Object Context
Even though we hand the batch update request to a managed object context, the managed object context is not aware of the changes as a result of executing the batch update request. As I mentioned earlier, it bypasses the managed object context. This gives batch updates their power and speed. To remedy this issue, we need to do two things:
- turn the managed objects that were updated by the batch update into faults
- tell the fetched results controller to perform a fetch to update the user interface
This is what we do in the next few lines of the checkAll(_:)
method in the do
clause of the do-catch
statement. If the batch update request is successful, we extract the array of NSManagedObjectID
instances from the NSBatchUpdateResult
object.
// Extract Object IDs let objectIDs = batchUpdateResult.result as! [NSManagedObjectID]
We then iterate over the objectIDs
array, ask the managed object context for the corresponding NSManagedObject
instance, and turn it into a fault by invoking refreshObject(_:mergeChanges:)
, passing in the managed object as the first argument. To force the managed object into a fault, we pass false
as the second argument.
// Extract Object IDs let objectIDs = batchUpdateResult.result as! [NSManagedObjectID] for objectID in objectIDs { // Turn Managed Objects into Faults let managedObject = managedObjectContext.objectWithID(objectID) managedObjectContext.refreshObject(managedObject, mergeChanges: false) }
Fetching Updated Records
The last step is to tell the fetched results controller to perform a fetch to update the user interface. If this is unsuccessful, we catch the error in the catch
clause of the do-catch
statement.
do { // Execute Batch Request let batchUpdateResult = try managedObjectContext.executeRequest(batchUpdateRequest) as! NSBatchUpdateResult // Extract Object IDs let objectIDs = batchUpdateResult.result as! [NSManagedObjectID] for objectID in objectIDs { // Turn Managed Objects into Faults let managedObject = managedObjectContext.objectWithID(objectID) managedObjectContext.refreshObject(managedObject, mergeChanges: false) } // Perform Fetch try self.fetchedResultsController.performFetch() } catch { let updateError = error as NSError print("\(updateError), \(updateError.userInfo)") }
While this may seem cumbersome and fairly complex for an easy operation, keep in mind that we bypass the Core Data stack. In other words, we need to take care of some housekeeping that's usually done for us by Core Data.
Step 4: Build & Run
Build the project and run the application in the simulator or on a physical device. Click or tap the bar button item on the right to check every to-do item in the list.
Conclusion
Batch updates are a great addition to the Core Data framework. Not only does it answer a need developers have had for many years, it isn't difficult to implement as long as you keep a few basic rules in mind. In the next tutorial, we'll take a closer look at batch deletes, another feature of the Core Data framework that was added only recently.