An application that grows and gains features also gains new requirements. The data model, for example, grows and changes. Core Data handles changes pretty well as long as you play by the rules of the framework.
In this tutorial, we take a look at how Core Data helps us manage changes of the data model and what pitfalls we absolutely need to avoid.
Modifying the Data Model
Download or clone the project we created in the previous tutorial and open it in Xcode. Run the application in the simulator or on a physical device to make sure everything is set up correctly.
git clone https://github.com/bartjacobs/MoreFetchingAndDeletingManagedObjects
Open the project's data model by selecting Lists.xcdatamodeld in the Project Navigator. Add an entity to the data model and name it User. Add two attributes to the User entity:
firstName
of typeString
lastName
of typeString
Add a relationship, lists
, to the User entity and set its destination to the List entity. In the Data Model Inspector, set Type to To Many.
Select the List entity and create a To One relationship with the User entity as its destination. Name the relationship user
and set the inverse relationship to lists
. Remember that the inverse relationship of the lists
relationship is automatically set for us by Core Data. This is what the data model now looks like.
It's time to verify that everything is still working. Run the application in the simulator or on a physical device. Ouch. Did that crash take you by surprise? That's what happens if you mess with the data model without telling Core Data about it.
In this tutorial, we find out what happened, how we can prevent it, and how we can modify the data model without running into a crash.
Finding the Root Cause
Finding out the cause of the crash is easy. Open CoreDataManager.swift and inspect the implementation of the persistentStoreCoordinator
property. If adding the persistent store to the persistent store coordinator fails, the application invokes the fatalError()
function, which causes the application to terminate immediately.
The goal of this tutorial is to prevent that adding the persistent store to the persistent store coordinator fails. Run the application again and inspect the output in the console. The following line tells us what went wrong.
reason = "The model used to open the store is incompatible with the one used to create the store";
We are closing in on the root of the problem. Core Data tells us that the data model is not compatible with the data model we used to create the persistent store. What does that mean? When you downloaded or cloned the project from GitHub, I asked you to run the application to make sure everything was set up correctly. By running the application for the first time, Core Data automatically created a persistent store based on the data model of the project.
We then modified the data model by creating the User entity and defining several attributes and relationships. With the new data model in place, we ran the application again. And you know what happened next.
Before a persistent store is added to the persistent store coordinator, Core Data checks if a persistent store already exists. If it finds one, Core Data makes sure the data model is compatible with the persistent store. How this works becomes clear in a moment.
The error message in the console indicates that the data model that was used to create the persistent store is not identical to the current data model. As a result, Core Data bails out and throws an error. Read this paragraph again. It is important that you understand the root cause of the problem.
Versioning the Data Model
You should never modify a data model without telling Core Data about the changes you made. But how do you tell Core Data about the changes you make to a data model? The answer is versioning.
You should never modify a data model without telling Core Data about the changes you made.
The idea is simple. Core Data tells us that the current data model is not the one that was used to create the persistent store. To solve that problem, we first and foremost leave the data model that was used to create the persistent store untouched. That is one problem solved.
To make changes to the data model, we make a new version of the data model. Each data model version has a unique identifier and Core Data stores this identifier in the persistent store to know what model was used to create it. Before we version the data model, we need to revert the data model to its original state.
Select Lists.xcdatamodeld and remove the User entity and the user
relationship of the List entity. Run the application again. Because we reverted the data model to its original state, the application should no longer crash.
It is time to version the data model. With the data model selected, choose Add Model Version... from the Editor menu. Name the version Lists 2 and base the data model version on Lists. It's the only option available.
A small triangle has appeared on the left of the data model in the Project Navigator. Click the triangle to show the list of data model versions.
Note that a green checkmark is added to Lists.xcdatamodel. This indicates that Lists.xcdatamodel is the active data model version. If we were to run the application, Core Data would continue to use the original data model version. That's not what we have in mind, though. Before we make any changes, select Lists.xcdatamodeld (not Lists.xcdatamodel), open the File Inspector on the right, and set Model Version to Lists 2, the data model version we created a moment ago.
Note that the green checkmark has moved from Lists.xcdatamodel to Lists 2.xcdatamodel. Because we haven't run the application yet, we can still modify the new data model version without running into compatibility issues. Select Lists 2.xcdatamodel and create the User entity we added earlier. Don't forget to also add the user
relationship to the List entity.
Run the application to see if we solved the incompatibility problem we ran into earlier. Are you still running into a crash? To make changes to the data model, we've added a new data model version. We also marked the new data model version as the active data model version. What we haven't told Core Data is what it should do if it runs into an incompatibility issue. We need to tell it to perform a migration.
Migrations
I already told you that a persistent store is tied to a particular version of the data model. It keeps a reference to the identifier of the data model. If the data model changes, we need to tell Core Data how to migrate the data of the persistent store to the new data model version.
There are two types of migrations:
- lightweight migrations
- heavyweight migrations
Heavyweight migrations are pretty complex and you should try to avoid them whenever possible. Lightweight migrations are much easier because Core Data takes care of the heavy lifting for us.
To add support for lightweight migrations to the CoreDataManager
class, we need to make a minor change. Do you remember that addPersistentStore(ofType:configurationName:at:options:)
accepts a dictionary of options as its last parameter? To add support for migrations, we pass in a dictionary of options with two keys:
NSInferMappingModelAutomaticallyOption
: If the value of this key is set totrue
, Core Data attempts to infer the mapping model for the migration based on the data model versions of the data model.NSMigratePersistentStoresAutomaticallyOption
: By setting the value of this key totrue
, we tell Core Data to automatically perform a migration if it detects an incompatibility.
What is a mapping model? A mapping model defines how one version of the data model relates to another version. For lightweight migrations, Core Data can infer the mapping model by inspecting the data model versions. This isn't true for heavyweight migrations and that is what makes heavyweight migrations complex and tedious. For heavyweight migrations, the developer is responsible for creating the mapping model.
With this in mind, we can update the implementation of the do
clause of the do-catch
statement in the CoreDataManager
class.
do {
let options = [ NSInferMappingModelAutomaticallyOption : true,
NSMigratePersistentStoresAutomaticallyOption : true]
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: persistentStoreURL,
options: options)
} catch {
fatalError("Unable to Load Persistent Store")
}
Run the application to see if the solution works. If you don't run into a crash, then Core Data has successfully migrated the persistent store to the new data model version.
Keep It Lightweight
Whenever you make a change to a data model, you need to consider the consequences. Lightweight migrations carry little overhead. Heavyweight migrations, however, are a pain. Avoid them whenever possible.
How do you know if a data model change requires a lightweight or a heavyweight migration? You always need to test the migration to be sure. That said, Core Data is pretty clever and is capable of migrating the persistent store most of the times without your help.
Adding or removing entities, attributes, and relationships are no problem for Core Data. Modifying the names of entities, attributes, and relationship, however, is less trivial for Core Data. If you change the cardinality of a relationship, then you are in for a wild ride.
Plan, Plan, Plan
Every form of persistence requires planning. I cannot stress enough how important this phase of a project is. If you don't invest time architecting the data model, chances are you run into problems that could have been avoided.
It is fine to make incremental changes to the data model as your application grows, but once your application is in the hands of users you need to make sure they don't lose their data due to a problematic migration. And always test migrations before shipping a new version of your application.
Questions? Leave them in the comments below or reach out to me on Twitter. You can download the source files of the tutorial from GitHub.
Migrations are an important aspect of Core Data because most applications grow and that often means you need to make changes to the data model. Data model changes and migrations are not hard, but they require attention and testing.
Now that you know what Core Data is and how the Core Data stack is set up, it's time to write some code. If you're serious about Core Data, check out Mastering Core Data With Swift. We build an application that is powered by Core Data and you learn everything you need to know to use Core Data in your own projects.