Core Data Fundamentals

Data Model Migrations

Resources

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 episode, we take a close look at the cause of the crash we ran into in the previous episode. We learn how Core Data helps us manage changes of the data model and what pitfalls we absolutely need to avoid.

Finding the Root Cause

Finding the root 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 throws a fatal error, immediately terminating the application. As I mentioned earlier in this series, you shouldn't throw an error if adding the persistent store fails.

CoreDataManager.swift

private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    ...

    do {
        // Add Persistent Store
        try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: persistentStoreURL, options: nil)

    } catch {
        fatalError("Unable to Add Persistent Store")
    }

    return persistentStoreCoordinator
}()

In this episode, I want to show you what we need to do to prevent that adding the persistent store to the persistent store coordinator fails. Run the application again and inspect the output in the console. This line tells us what went wrong.

reason = "The model used to open the store is incompatible with the one used to create the store";

Finding the Root Cause

We're getting closer to the root of the problem. Core Data tells us that the data model isn't compatible with the data model we used to create the persistent store. What does that mean? And how is that possible?

Remember that Core Data automatically creates a persistent store if it isn't able to find one to add. To create the persistent store, the framework first inspects the data model. For a SQLite database, for example, Core Data needs to know what the database schema should look like. It fetches this information from the data model.

In the previous episode, we modified the data model by adding the colorAsHex attribute to the Category entity. With the new data model in place, we ran the application again ... and you know what happened next.

Modifying the Data Model

Before the persistent store coordinator adds a persistent store, it 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 isn't identical to the current data model. As a result, Core Data bails out and throws an error.

Versioning the Data Model

You should never modify a data model without telling Core Data about the changes you made. Let me repeat that. 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 made to the data model? The answer is versioning.

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's 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 the persistent store.

Core Data stores the unique identifier of the data model in the persistent store.

We can verify this by inspecting the persistent store of the application. If we inspect the SQLite database, we see a table named Z_METADATA. This table contains the unique identifier of the data model. The unique identifier changes when the data model changes.

Inspecting the Persistent Store

Before the persistent store coordinator adds a persistent store, it compares the unique identifier stored in the persistent store with that of the current data model.

Comparing the Unique Identifier of the Current Data Model

Now that we know what went wrong, we can implement a solution. Fortunately, Core Data makes versioning the data model very easy.

Before You Go

I already mentioned several times that you shouldn't throw a fatal error if adding the persistent store fails in production. It's fine to throw a fatal error if you're developing your application. Once your application is in the hands of users, though, you need to handle the situation more gracefully.

Throwing a fatal error immediately terminates your application, resulting in a bad user experience. Most users don't understand what went wrong and, all too often, they delete the application to resolve the issue. This is ironic since that's the only solution that works if your application doesn't have a solution in place to recover from this scenario.

How you handle failing to add a persistent store depends on your application. To recover from this scenario, you could delete the persistent store and try adding the persistent store again. If that operation fails as well, then you have bigger problems to worry about. Always remember that deleting the persistent store is synonymous to data loss. Try to avoid that at all cost.

The first action I usually take is moving the existing persistent store (the one that cannot be added to the persistent store coordinator) to a new location to prevent data loss. This doesn't resolve the issue, but it prevents immediate data loss. You can then safely add a new persistent store to the persistent store coordinator without losing the user's data. This means the user can continue using the application without running into a crash. It can also help debugging the issue if you add a mechanism that enables the user to send you the corrupt persistent store.

The second action is notifying the user about the problem. If the user opens your application and sees that it's empty, they think they lost their data. It's important that you inform them about the situation and how to handle it. Explain the problem in easy to understand words. Make sure they don't panic and ask them to get in touch with you to debug the issue.

I want to emphasize that there's no one solution to this problem. The point I want to drive home is that you need to prepare for this scenario. If this happens, it doesn't necessarily mean you made a mistake. But it does mean that it's up to you to solve the problem.

Resources
Next Episode "Versioning the Data Model"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By