Core Data Migration - Part 1 (Adding new fields)

Core Data Migration - Part 1 (Adding new fields)

While working on tiny side project, I realized that Xcode has made significant change in how core data migration takes place. Earlier, like couple of years ago I recall how I ran into this problem with core data migration.

The problem was related with data migration. Let's say you have a project supporting core data.

For starters, here is how you can enable core data in the beginning while creating a new project. Just check the option which says Use Core Data while making a new project

use_core_data_option

Data migration is the process of migrating data from one structure to another when underlying schema changes. That means any time you add/edit/remove a column from database, data migration should happen. Otherwise application may,

  1. Lose all the previous data
  2. Simply crash since iOS does not know how to handle new schema

I would prefer latter case over the former. Latter case explicitly lets user know about the problem while former silently fails losing previously cached data

Recently though I observed that changing the schema does not cause app to go through either of cases mentioned above. I was quite surprised by this behavior. Upon further investigations I discovered following things.

When core data is enabled in project, Xcode automatically adds the code which allows us to directly use the NSPersistentContainer instance which is used as a central for all the Core Data activities. Below is the sample code generated by Xcode. This code resides in AppDelegate class. NSPersistentContainer instance can be picked by by simply using

let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer

Please don't use ! in the production code. I have added it here just for an example and to reduce the size of code. Otherwise I am not a big fan of it either


lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "PlainCoreData")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

I dived into why Core data would handle migration and infer mapping from old to new model automatically. Looks like the container in above code which is an instance of NSPersistentContainer has a property called persistentStoreDescriptions.

To quote Apple,

The persistent store descriptions used to create the persistent stores referenced by this persistent container

Depending on how many stores you are using, it may hold one or more descriptions in the container. For simplicity, let's assume we are only using one. For each NSPersistentStoreDescription object associated with model it has two properties as follows,

open var shouldMigrateStoreAutomatically: Bool
open var shouldInferMappingModelAutomatically: Bool

Both these properties are true by default for any of NSPersistentStoreDescription instance you may encounter. That explains why it is handled automatically without having developer to manually do it.

let desc = container.persistentStoreDescriptions.first
print("Should Infer Mapping Model Automatically \(desc?.shouldInferMappingModelAutomatically ?? false)")
print("Should Migrate Store Automatically \(desc?.shouldMigrateStoreAutomatically ?? false)")

prints,

(lldb) po desc?.shouldInferMappingModelAutomatically
▿ Optional
  - some : true


(lldb) po desc?.shouldMigrateStoreAutomatically
▿ Optional
  - some : true

So that explains why database does not enter the invalid state in spite of changing the underlying model.

This is simple part since adding new column does not change the underlying structure and app does not crash. However, there are other cases where this will not play so nice. For example, if you try to remove one of the existing column and update the datatype, it will crash the app unless database has already been migrated.

I will talk about how to handle migrations in those cases in the next blog post, Core Data Migration - Part 2