Introducing the Open-Source Big Nerd Ranch Core Data Stack
When we at the Ranch use Core Data, we inevitably end up using it in a multi-threaded environment. The Right Way™ to use Core Data across multiple threads is a topic of fierce debate, one that we had ourselves when we set out to create a shared Core Data stack.
And now we want to share the Core Data stack we created with you. The Big Nerd Ranch Core Data Stack was designed to effortlessly fit the needs of most users, while providing the flexibility to accommodate more esoteric needs.
Stack Design Patterns
To best describe the design choices made in the Big Nerd Ranch Core Data stack, we’ll first walk through the various patterns of multi-threaded Core Data stacks.
The first pattern is constructed with a single persistent store coordinator, connected to a main queue managed object context. Background worker managed object contexts are created as needed and share the same single instance of the persistent store coordinator.
Saving any one of these managed object contexts will push changes through the shared coordinator and on to the backing store file. While the changes from each context are available in the store after a save, no context is aware of the changes made in any other context.
In order to keep all contexts in sync, the stack must subscribe to
NSManagedObjectContextDidSaveNotification notifications and then call
mergeChangesFromContextDidSaveNotification() on the remaining contexts.
- Worker contexts are kept in sync with each other
- Can be more performant with large data sets. More to come on this later in the post.
- Greater complexity with setup to ensure consistency across contexts
- Increased chance of conflicting changes at the store level since they share a single coordinator
- Persistent store coordinator spends more time locking the store, affecting performance
In iOS 5.0 / OS X 10.7, Apple introduced the concept of nested managed object context through the property
parentContext. Fetch requests performed on a context will work their way up through the context’s
parentContexts until it reaches an
NSPersistentStore where the objects are retrieved. Performing a save operation on a context will push the changes up a single level.
- Much simpler setup compared to Shared Persistent Store Coordinator Pattern
- Children contexts always have most recent changes from parents available
- Minimizes contexts interfacing with the coordinator to a single context
- Writes to the store performed on a background queue
- Slower than Shared Persistent Store Coordinator Pattern when inserting large data sets
awakeFromInsertbeing called on
NSManagedObjectsubclasses for each context in the parent chain
- Merge policies only apply to a context saving to a store and not to its parent context
One criticism of the nested managed object context pattern is that inserting a large number of objects (on the order of thousands) is significantly slower than doing the same operation with a shared persistent store coordinator stack. See Concurrent Core Data Stack Performance Shootout. For this reason some have outright avoided using a nested managed object context pattern in favor of the shared persistent store coordinator pattern.
Apple, however, recommends an alternate approach for solving this type of performance issue, saying:
If you’ve exhausted all other optimizations and are still seeing performance issues, you might want to consider using a different concurrency style, and this time you have two persistent store coordinators, two almost completely separate Core Data stacks. One stack for your background work, one stack for your main queue work and they both talk to the same persistent store file.
This setup allows the two stacks to work in parallel with the exception of locking the store file, which is a very fast operation.
The changes could be propagated across contexts using the same pattern described in shared persistent store coordinator pattern using
mergeChangesFromContextDidSaveNotification(). However, since this pattern is built for performance, it’s preferable to simply listen for the
NSManagedObjectContextDidSaveNotification and re-perform your fetch request on the main queue context stack. This will be considerably faster than trying to merge thousands of objects between the two stacks.
- Much faster when inserting large data sets
- Heavy-weight setup when creating the background context because of the need to create an entire stack
- More complex setup compared to nested managed object context pattern
- Syncing changes between stacks is manual
The Big Nerd Ranch Core Data Stack
This brings us to the Big Nerd Ranch Core Data Stack. For us, the simplicity of the nested managed object context pattern greatly outweighs any performance deficiencies.
At the root of our stack is a
PrivateQueueConcurrencyType managed object context that handles writing to our store. Because this context is on a private queue, we benefit from moving disk writing off the main queue. Interacting with managed objects themselves should occur through one of this context’s children.
The root context has one child context, which is a
MainQueueConcurrencyType context. This context should be used for any main queue or UI related tasks. Examples include setting up an
NSFetchedResultsController, performing quick fetches, making UI related updates like a bookmark or favoriting an object.
For any longer running task, such as inserting or updating data from a web service, you should use a new background worker context. The stack will vend you one via the
Since saving an
NSManagedObjectContext will only propagate changes up a single level to the
parentContext, the Big Nerd Ranch Core Data Stack listens for save notifications and ensures that the changes get persisted all the way up the chain to your store.
You may sometimes need to perform large import operations where the nested context performance would be the bottleneck. For that, we’ve included a function to vend you a managed object context with its own stack
newBatchOperationContext(setupCallback: CoreDataStackBatchMOCCallback). This follows the pattern outlined in Shared Store Stack Pattern.
The Big Nerd Ranch Core Data stack can be constructed with either an
NSSQLiteStoreType store or an
NSInMemoryStoreType store. Since a SQLite backed store could potentially need to perform model migrations and take an indefinite amount of time, the stack construction is asynchronous. Similarly, creating a new batch operation context will also be asynchronous, since it relies on constructing a discrete stack of its own.
For usage details, see Usage – Standard SQLite Backed.
Large Import Operation Context
In most cases, offloading your longer-running work to a background worker context will be sufficient to alleviate performance woes. If you find yourself inserting or updating thousands of objects, then perhaps the shared store pattern is a good fit for your use case. The following WWDC videos are helpful in ensuring you’ve done your due diligence in optimizing your fetches, inserts and queries before opting to go this route:
- Core Data Performance Optimization and Debugging
- Optimizing Your Core Data Application
- Optimizing Core Data Performance on iPhone OS
For usage details, see Usage – Large Import Operation Context.
Our Core Data Stack and Your Projects
We’re happy to say we’ve had success incorporating this stack on multiple client projects. We hope it brings success to your project.