Doing Bulk Updates

Doing a bulk update isn’t a common event in most application life cycles. Selecting a large number of emails, or a large number of news items, and marking them as read is a common example of doing a bulk update. Although these situations do occur, they are unusual and shouldn’t be considered a core function of the application. Bulk updates are generally used to get us out of a coding or design “corner.”

In our recipes application, we’re going to use the bulk update API to change the values of some of our recipes on the first launch after a migration. When we migrate the application to the fourth version, we’ll add a Boolean to indicate whether it’s a favorite recipe; the default for all recipes is NO. Once the migration is complete, we then want to go through all of the recipes and change that default to YES for some of them.

To start with, we want to detect if this change has already been made. There are several ways to accomplish this, and we’ve used other methods in the past. In this demonstration, we’re going to use the metadata that’s contained with the persistent store to determine whether the change has already been processed. This change to the initialization of our Core Data stack determines whether we need to do any post-migration processing.

Batch/PPRecipes/PPRDataController.m
 dispatch_queue_t queue = NULL;
 queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 dispatch_async(queue, ^{
  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSURL *storeURL = nil;
  storeURL = [[fileManager URLsForDirectory:NSDocumentDirectory
  inDomains:NSUserDomainMask] lastObject];
  storeURL = [storeURL URLByAppendingPathComponent:​@"PPRecipes.sqlite"​];
 
  NSMutableDictionary *options = [[NSMutableDictionary alloc] init];
  [options setValue:[NSNumber numberWithBool:YES]
  forKey:NSMigratePersistentStoresAutomaticallyOption];
  [options setValue:[NSNumber numberWithBool:YES]
  forKey:NSInferMappingModelAutomaticallyOption];
  NSError *error = nil;
  NSPersistentStore *store = nil;
  store = [psc addPersistentStoreWithType:NSSQLiteStoreType
  configuration:nil
  URL:storeURL
  options:options
  error:&error];
 if​ (!store) {
  ALog(​@"Error adding persistent store to coordinator %@​​\n​​%@"​,
  [error localizedDescription], [error userInfo]);
  }
 
  NSDictionary *metadata = [store metadata];
 if​ (!metadata[FAVORITE_METADATA_KEY]) {
  [self bulkUpdateFavorites];
  }

Every persistent store contains metadata. The metadata resolves to a NSDictionary that we can query. We can also update this metadata as needed.

In this part of the code, we’re looking for a key named FAVORITE_METADATA_KEY. If that key exists, then we know that this particular bit of post-processing has already been done. If the key is missing, we need to perform the task.

Batch/PPRecipes/PPRDataController.m
 -​ (​void​)bulkUpdateFavorites
 {
  NSManagedObjectContext *moc = [self writerContext];
 
  [moc performBlock:^{
  NSBatchUpdateRequest *request = nil​;
  NSMutableDictionary *propertyChanges = nil​;
  NSPredicate *pred = nil​;
  NSBatchUpdateResult *result = nil​;
  NSError *error = nil​;
 
  request = [[NSBatchUpdateRequest alloc] initWithEntityName:​@"Recipe"​]​;
 
  NSDate *aMonthAgo = [self dateFrom1MonthAgo]​;
  pred = [NSPredicate predicateWithFormat:​@"lastUsed >= %@"​, aMonthAgo]​;
  [request setPredicate:pred]​;
 
  propertyChanges = [NSMutableDictionary new]​;
  propertyChanges[​@"favorite"​] = ​@​(YES)​;
  [request setPropertiesToUpdate:propertyChanges]​;
  [request setResultType:NSUpdatedObjectIDsResultType]​;
 
  result = [moc executeRequest:request error:&error]​;
 if​ (!result) {
  ALog(​@"Failed to execute batch update: %@​​\n​​%@"​,
  [error localizedDescription], [error userInfo])​;
  }
 
 //Notify the contexts of the changes
  [self mergeExternalChanges:[result result] ofType:NSUpdatedObjectsKey]​;

The -bulkUpdateFavorites method is where we’re using the bulk update API. Once we are positive that we are executing on the proper queue for our main NSManagedObjectContext, we start off by creating a new NSBatchUpdateRequest. The NSBatchUpdateRequest is a subclass of NSPersistentStoreRequest, which is a class that was introduced in OS X 10.7 and iOS 5. An NSBatchUpdateRequest contains all of the properties that Core Data needs to execute our update directly on disk. First, we initialize the request with the name of the entity we want to access. We then pass it the predicate to filter the entities that will be updated.

In this example, we’re going to find all the recipe entities that have been used in the last month and mark those as favorites. We construct a date object that represents one month ago and then pass that to the predicate and then pass the predicate into the NSBatchUpdateRequest.

In addition to the predicate, we need to tell Core Data what properties need to be changed. We do this with a NSDictionary, where the key is the property to change and the value is the new value to apply to the entity. As you can see, we don’t have a lot of control over the changes here. There’s no logic that we can apply. These are simple, brute-force data changes at the database/persistent store level.

Once we pass the dictionary to NSBatchUpdateRequest via -propertiesToUpdate, we can define what kind of result we want back. We have three options:

In this example, we’ll walk through updating the user interface of the changes, so we’ll ask for the NSManagedObjectID instances back.

Once we have the NSBatchUpdateRequest fully constructed, we can then hand it off to any NSManagedObjectContext we want for processing. Here I’m using the writer context because it’s closest to the NSPersistentStoreCoordinator. But since this API doesn’t notify the NSManagedObjectContext of the change, it really doesn’t matter which context we use.

The call to -executeRequest: error: returns a simple id, and it’s up to us to know what the call is giving back to us. Since we set the -resultType to be NSUpdatedObjectIDsResultType, we know that we’re going to be getting an NSArray back.

If we get back a nil from this API call, we know that there was an error and we can respond to that error. As always in the code in this book, we’re going to treat the error as a fatal condition and crash. How to respond to those errors is a business decision determined by your application’s design and requirements.

The call to -executeRequest: error: is a blocking call. This means that the call can take a significant amount of time—still far less than loading all of the objects into memory, performing the change, and saving them back out to disk, but it will take some time. This is another argument for using the API against the private writer context instead of the context that the user interface is associated with.