Core Data Transactions

In my time getting acquainted with Core Data, I learned that while overall it is very handy, there are shortcomings that a developer must take into account especially when dealing with asynchronous calls and large amounts of data.

While working on my latest project I had to interface a lot with Core Data and really test its limits. Core Data is an abstraction layer on top of SQLite which aims to make working with databases in iOS simpler. It does a lot of things very well, helping save a lot of time and code. The interface in XCode streamlines the processes for creating your entities (tables) and fetch requests (queries), as well as adding attributes (columns) and relationships to the different entities. Another nice feature is the ability to create your NSManagedObject files right out of the Core Data model.

When creating a new object, Apple’s documentation states that it is preferred to save the new object on the main thread while creating a temporary managed object context that will be merged with the main managed object context after the save is successful. There are multiple reasons for this. One reason is that when saving is done on the main thread, the writes are synchronous assuring that two writes never occur at the same time which would cause the application to fail. Another reason is that there is no guarantee that an asynchronous task will finish, or when it will happen. While this is ideal, it is expensive making these calls on the main thread and creates a poor user experience.

That being said, I began to work on a way to account for the concerns of writing the new object asynchronously. To begin, we make a call to create an object in the background:

- (void)addRecord {
    dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        Record *record = [Record alloc];
        [record addRecordWithDictionary:self.parameters];
        [record release];
    });
}

Next, we need to create the temporary managed object context that we will merge later:

- (NSManagedObjectContext *)tempManagedObjectContext {
    NSManagedObjectContext *tempManagedObjectContext = nil;
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];

    if (coordinator != nil) {
        tempManagedObjectContext = [[[NSManagedObjectContext alloc] init] autorelease];
        [tempManagedObjectContext setPersistentStoreCoordinator:coordinator];
    }

    return tempManagedObjectContext;
}

Once we’ve done this we can create our object from our background thread:

- (void)addRecordWithDictionary:(NSDictionary *)params {
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = [appDelegate.coreDataManager tempManagedObjectContext];

    Record *newRecord = [appDelegate.coreDataManager addObjectForType:CDRecord context:context];
    [Record setInformationFromDictionary:params forRecord:newRecord];
    [appDelegate.coreDataManager saveTempContext:context];
}

Now, when we’re saving the temporary managed object context, we’ll address the other concerns of not being guaranteed to finish, and the possibility of two writes happening at the same time:

- (void)saveTempContext:(NSManagedObjectContext *)tempContext {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(tempContextSaved:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:tempContext];

    [self saveContext:tempContext];
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:NSManagedObjectContextDidSaveNotification
                                                  object:tempContext];
}

- (void)tempContextSaved:(NSNotification *)notification {
    // Merge the changes into the original managed object context
    UIApplication* app = [UIApplication sharedApplication];

    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    dispatch_sync(coreDataQueue, ^{
        [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_DATA_UPDATED
                                                            object:nil];
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}

When the temporary context is saved, it fires off a notification to let the main managed object context know that it is safe to merge. Here we ask the system for enough time to finish the task (also known as Task Finishing). Next, we throw the merge on it’s own synchronous thread to ensure that multiple writes will not happen at the same time. Using dispatch_sync, we throw the save onto our custom queue known here as coreDataQueue. We then send a notification to tell any observers that Core Data has been updated. While this solved the issue of creating an object in a background thread, writing every time to the database can be incredibly slow when passed large amounts of data. To account for this, I created a way of making database transactions so that one temporary context can account for an array of objects before saving and merging:

- (NSManagedObjectContext *)startTransaction {
    return [self tempManagedObjectContext];
}

- (void)endTransactionForContext:(NSManagedObjectContext *)context {
    if ([context hasChanges]) {
        [self saveTempContext:context];
    }
}

Using previously written methods, I created two simple methods to make it apparent that a transaction is occurring. Now adding an array of records will look like this:

- (void)addFromArray:(NSArray *)array {
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = [appDelegate.coreDataManager startTransaction];

    for (NSDictionary *dictionary in array) {
        [Record addWithParams:dictionary forContext:context];
    }

    [appDelegate.coreDataManager endTransactionForContext:context];
}

Doing this helped to cut down time to save and get back to the user significantly, as opposed to holding up the app for far too long to save a large amount of data. When I benchmarked the difference I had an average of 15.7 seconds so save 500 records to Core Data without using a transaction. Once this transaction implementation was added, it only took 0.5 seconds to save all 500 records to the database.

Core Data is a very handy tool. By adding these few ideas to it, Core Data becomes much more viable for apps that need to scale to handle writing large amounts of data.