In the previous articles of this series, we've encountered an annoying issue that we need to address. Whenever we modify the data model of a Core Data application, the persistent store becomes incompatible with the data model. The result is a crash on launch, rendering the application unusable, a serious problem if this happens to an application in the App Store.
Our application crashes because we invoke abort
if adding the persistent store to the persistent store coordinator is unsuccessful. To be clear, the abort
function causes the application to terminate immediately.
do { // Add Persistent Store to Persistent Store Coordinator try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: nil) } catch { // Populate Error var userInfo = [String: AnyObject]() userInfo[NSLocalizedDescriptionKey] = "There was an error creating or loading the application's saved data." userInfo[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data." userInfo[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "com.tutsplus.Done", code: 1001, userInfo: userInfo) NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() }
However, there is no need to terminate our application, let alone crash it. If Core Data tells us the data model and persistent store are incompatible, then it's up to us to resolve that.
In this article, we'll discuss two options to recover from such a situation, migrating the persistent store and creating a new persistent store that is compatible with the modified data model.
1. The Problem
Let me first clarify the problem that we're trying to solve. Download the sample project we created in the previous article and run it in the simulator. The application should run and work just fine.
Open Done.xcdatamodeld and add an attribute updatedAt of type Date to the Item entity. Run the application one more time and notice how the application crashes as soon as it's launched. Luckily, Core Data gives us a clue as to what went wrong. Take a look at the output in Xcode's console.
Done[897:14527] CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:file:///Users/Bart/Library/Developer/CoreSimulator/Devices/A263775B-4D73-48C8-BD79-825E0BED5128/data/Containers/Data/Application/E46663CA-79AF-4645-AF78-0A17236943E1/Documents/Done.sqlite options:(null) ... returned error Error Domain=NSCocoaErrorDomain Code=134100 "(null)" UserInfo={metadata={ NSPersistenceFrameworkVersion = 640; NSStoreModelVersionHashes = { Item = <4c880226 3219fc66 283b28c5 54f026dc 7f95af5f c19fb76e 255a26a7 2a2a79f5>; }; NSStoreModelVersionHashesVersion = 3; NSStoreModelVersionIdentifiers = ( "" ); NSStoreType = SQLite; NSStoreUUID = "F0F98261-4F60-451A-9606-91E1F60425B9"; "_NSAutoVacuumLevel" = 2; }, reason=The model used to open the store is incompatible with the one used to create the store} with userInfo dictionary { metadata = { NSPersistenceFrameworkVersion = 640; NSStoreModelVersionHashes = { Item = <4c880226 3219fc66 283b28c5 54f026dc 7f95af5f c19fb76e 255a26a7 2a2a79f5>; }; NSStoreModelVersionHashesVersion = 3; NSStoreModelVersionIdentifiers = ( "" ); NSStoreType = SQLite; NSStoreUUID = "F0F98261-4F60-451A-9606-91E1F60425B9"; "_NSAutoVacuumLevel" = 2; }; reason = "The model used to open the store is incompatible with the one used to create the store"; }
Near the end, Core Data tells us that the data model that was used to open the persistent store is incompatible with the data model that was used to create the persistent store. Wait. What?
When we launched the application for the first time, Core Data created a SQLite database based on the data model. However, because we changed the data model by adding an attribute to the Item entity, updatedAt, Core Data no longer understands how it should store Item records in the SQLite database. In other words, the modified data model is no longer compatible with the persistent store, the SQLite database, it created earlier.
2. The Solution
Fortunately for us, a few clever engineers at Apple have created a solution to safely modify a data model without running into compatibility issues. To solve the problem we're facing, we need to find a way to tell Core Data how one version of the data model relates to another version. That's correct, versioning the data model is part of the solution.
With that information, Core Data can understand how the persistent store needs to be updated to be compatible with the modified data model, that is, the new version of the data model. In other words, we need to hand Core Data the necessary information to migrate the persistent store from one version of the data model to another.
3. Migrations
There are two types of migrations, lightweight and heavy migrations. The words lightweight and heavy are pretty descriptive, but it's important that you understand how Core Data handles each type of migration.
Lightweight Migrations
Lightweight migrations require very little work from your part, the developer. I strongly recommend that you choose a lightweight migration over a heavy migration whenever you can. The cost of a lightweight migration is substantially lower than that of a heavy migration.
Of course, the flip side of lightweight migrations is that they are less powerful than heavy migrations. The changes you can make to a data model with lightweight migrations are limited. For example, a lightweight migration lets you add or rename attributes and entities, but you cannot modify the type of an attribute or the relationships between existing entities.
Lightweight migrations are ideal for expanding the data model, adding attributes and entities. If you plan to modify relationships or change attribute types, then you're in for a wild ride with heavy migrations.
Heavy Migrations
Heavy migrations are a bit trickier. Let me rephrase that. Heavy migrations are a pain in the neck and you should try to avoid them if possible. Heavy migrations are powerful, but that power comes at a cost. Heavy migrations require a lot of work and testing to make sure the migration completes successfully and, more importantly, without data loss.
We enter the world of heavy migrations if we make changes that Core Data cannot automatically infer for us by comparing versions of the data model. Core Data will then need a mapping model to understand how the versions of the data model relate to one another. Because heavy migrations are a complex topic, we won't cover it in this series.
4. Versioning
If you've worked with Ruby on Rails or any other framework that supports migrations, then Core Data migrations will make a lot of sense to you. The idea is simple but powerful. Core Data allows us to version the data model and this enables us to safely modify the data model. Core Data inspects the versioned data model to understand how the persistent store relates to the data model. By looking at the versioned data model, it also knows if the persistent store needs to be migrated before it can be used with the current version of the data model.
Versioning and migrations go hand in hand. If you wish to understand how migrations work, you'll first need to understand how to version the Core Data data model. Let's revisit the to-do application we created in the previous article. As we saw earlier, adding an attribute, updatedAt, to the Item entity results in the persistent store being incompatible with the modified data model. We now understand the cause of this.
Let's start with a clean slate by opening Done.xcdatamodeld and removing the updatedAt attribute from the Item entity. It's time to create a new version of the data model.
With the data model selected, choose Add Model Version... from the Editor menu. Xcode will ask you to name the new data model version and, more importantly, on which version the new version should be based. To ensure Core Data can migrate the persistent store for us, it's important that you choose the previous version of the data model. In this example, we have only one choice.
The result of this action is that we can now see three data model files in the Project Navigator. There is one top level data model with a .xcdatamodeld extension and two children with a .xcdatamodel extension.
You can see the .xcdatamodeld file as a package for the versions of the data model, with each version represented by an .xcdatamodel file. You can verify this by right-clicking the .xcdatamodeld file and selecting Show in Finder. This will take you to the data model in the Xcode project. You should see the two versions of the data model, Done.xcdatamodel and Done 2.xcdatamodel.
Have you noticed in the Project Navigator that one of the versions has a green checkmark? This checkmark indicates what the current model version is, Done.xcdatamodel in this example. In other words, even though we've created a new version of the data model, it isn't put to use by our application yet. Before we change this, though, we need to tell Core Data what it should do with the versioned data model.
We need to tell Core Data how to migrate the persistent store for the data model. We do this when we add the persistent store to the persistent store coordinator in AppDelegate.swift. In the implementation of the persistentStoreCoordinator
property, we create the persistent store coordinator and add a persistent store to it by invoking addPersistentStoreWithType(_:configuration:URL:options:)
. This should feel familiar by now.
The fourth parameter of this method is a dictionary of options, which is currently nil
. This dictionary of options includes instructions for Core Data. It gives us the opportunity to tell the persistent store coordinator how it should migrate the persistent store that we want to add to it.
Take a look at the following code snippet in which we pass a dictionary of options with two key-value pairs.
// Declare Options let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ] // Add Persistent Store to Persistent Store Coordinator try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options)
The first key, NSMigratePersistentStoresAutomaticallyOption
, tells Core Data that we'd like it to attempt to migrate the persistent store for us. The second key, NSInferMappingModelAutomaticallyOption
, instructs Core Data to infer the mapping model for the migration. This is exactly what we want. It should work without issues as long as we're dealing with lightweight migrations.
With this change, we are ready to migrate the data model to the new version we created a few moments ago. Start by selecting the new version, Done 2.xcdatamodel, and add a new attribute updatedAt of type Date to the Item entity.
We also need to mark the new data model version as the version to use by Core Data. Select Done.xcdatamodeld in the Project Navigator and open the File Inspector on the right. In the section Model Version, set Current to Done 2.
In the Project Navigator, Done 2.xcdatamodel should now have a green checkmark instead of Done.xcdatamodel.
With this change, you can safely build and run the application. If you've followed the above steps, Core Data should automatically migrate the persistent store for you by inferring the mapping model based on the versioned data model.
Note that there are a few caveats you should be aware of. If you run into a crash, then you've done something wrong. For example, if you've set the data model version to Done 2.xcdatamodel, run the application, and then make changes to Done 2.xcdatamodel, then you'll most likely run into a crash due to the persistent store being incompatible with the data model. Lightweight migrations are relatively powerful and they are easy to implement, but that doesn't mean you can modify the data model at any time.
The data layer of a software project requires care, attention, and preparation. Migrations are great, but they should be used sparingly. Crashes are no problem during development, but they are catastrophic in production. In the next section, we take a closer look at what this means and how to prevent crashes due to a problematic migration.
5. Avoiding Crashes
I have never come in a situation that warranted calling abort
in production and it pains me when I browse a project in which Apple's default implementation for setting up the Core Data stack is used, in which abort
is called when adding a persistent store is unsuccessful.
Avoiding abort
is not that difficult, but it requires a few lines of code and informing the user about what went wrong in case something does go wrong. Developers are only human and we all make mistakes, but that doesn't mean you should throw in the towel and call abort
.
Step 1: Getting Rid of abort
Start by opening AppDelegate.swift and remove the line in which we call abort
. That's the first step to a happy user.
do { // Declare Options let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ] // Add Persistent Store to Persistent Store Coordinator try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options) } catch { // Populate Error var userInfo = [String: AnyObject]() userInfo[NSLocalizedDescriptionKey] = "There was an error creating or loading the application's saved data." userInfo[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data." userInfo[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "com.tutsplus.Done", code: 1001, userInfo: userInfo) NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") }
Step 2: Moving the Incompatible Store
If Core Data detects that the persistent store is incompatible with the data model, we first move the incompatible store to a safe location. We do this to make sure the user's data isn't lost. Even if the data model is incompatible with the persistent store, you may be able to recover data form it. Take a look at the updated implementation of the persistentStoreCoordinator
property in AppDelegate.swift.
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { // Initialize Persistent Store Coordinator let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) // URL Persistent Store let URLPersistentStore = self.applicationStoresDirectory().URLByAppendingPathComponent("Done.sqlite") do { // Declare Options let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ] // Add Persistent Store to Persistent Store Coordinator try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options) } catch { let fm = NSFileManager.defaultManager() if fm.fileExistsAtPath(URLPersistentStore.path!) { let nameIncompatibleStore = self.nameForIncompatibleStore() let URLCorruptPersistentStore = self.applicationIncompatibleStoresDirectory().URLByAppendingPathComponent(nameIncompatibleStore) do { // Move Incompatible Store try fm.moveItemAtURL(URLPersistentStore, toURL: URLCorruptPersistentStore) } catch { let moveError = error as NSError print("\(moveError), \(moveError.userInfo)") } } } return persistentStoreCoordinator }()
Note that I've changed the value of URLPersistentStore
, the location of the persistent store. It points to a directory in the Documents directory in the application's sandbox. The implementation of applicationStoresDirectory()
, a helper method, is straightforward as you can see below. It's certainly more verbose than in Objective-C. Also note that I force unwrap the result of path()
of the URL
constant, because we can safely assume that there's an application support directory in the application's sandbox, both on iOS and on OS X.
private func applicationStoresDirectory() -> NSURL { let fm = NSFileManager.defaultManager() // Fetch Application Support Directory let URLs = fm.URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask) let applicationSupportDirectory = URLs[(URLs.count - 1)] // Create Application Stores Directory let URL = applicationSupportDirectory.URLByAppendingPathComponent("Stores") if !fm.fileExistsAtPath(URL.path!) { do { // Create Directory for Stores try fm.createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil) } catch { let createError = error as NSError print("\(createError), \(createError.userInfo)") } } return URL }
If the persistent store coordinator is unable to add the existing persistent store at URLPersistentStore
, we move the persistent store to a separate directory. To do that, we make use of two more helper methods, applicationIncompatibleStoresDirectory()
and nameForIncompatibleStore()
. The implementation of applicationIncompatibleStoresDirectory()
is similar to that of applicationStoresDirectory()
.
private func applicationIncompatibleStoresDirectory() -> NSURL { let fm = NSFileManager.defaultManager() // Create Application Incompatible Stores Directory let URL = applicationStoresDirectory().URLByAppendingPathComponent("Incompatible") if !fm.fileExistsAtPath(URL.path!) { do { // Create Directory for Stores try fm.createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil) } catch { let createError = error as NSError print("\(createError), \(createError.userInfo)") } } return URL }
In nameForIncompatibleStore()
, we generate a name for the incompatible store based on the current date and time to avoid naming collisions.
private func nameForIncompatibleStore() -> String { // Initialize Date Formatter let dateFormatter = NSDateFormatter() // Configure Date Formatter dateFormatter.formatterBehavior = .Behavior10_4 dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss" return "\(dateFormatter.stringFromDate(NSDate())).sqlite" }
Step 3: Creating a New Persistent Store
It's time to create a new persistent store to finish the setup of the Core Data stack. The next few lines should look very familiar by now.
do { // Declare Options let options = [ NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true ] // Add Persistent Store to Persistent Store Coordinator try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: URLPersistentStore, options: options) } catch { let storeError = error as NSError print("\(storeError), \(storeError.userInfo)") }
If Core Data is unable to create a new persistent store, then there are more serious problems that are not related to the data model being incompatible with the persistent store. If you do run into this issue, then double-check the value of URLPersistentStore
.
Step 4: Notify the User
This step is probably the most important one in terms of creating a user friendly application. Losing the user's data is one thing, but pretending that nothing happened isn't nice. How would you feel if an airline lost your luggage, pretending as if nothing happened.
let storeError = error as NSError print("\(storeError), \(storeError.userInfo)") // Update User Defaults let userDefaults = NSUserDefaults.standardUserDefaults() userDefaults.setBool(true, forKey: "didDetectIncompatibleStore")
If Core Data was unable to migrate the persistent store using the data model, we remember this by setting a key-value pair in the application's user defaults database. We look for this key-value pair in the viewDidLoad()
method of the ViewController
class.
// MARK: - // MARK: View Life Cycle override func viewDidLoad() { super.viewDidLoad() do { try self.fetchedResultsController.performFetch() } catch { let fetchError = error as NSError print("\(fetchError), \(fetchError.userInfo)") } let userDefaults = NSUserDefaults.standardUserDefaults() let didDetectIncompatibleStore = userDefaults.boolForKey("didDetectIncompatibleStore") if didDetectIncompatibleStore { // Show Alert let applicationName = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleDisplayName") let message = "A serious application error occurred while \(applicationName) tried to read your data. Please contact support for help." self.showAlertWithTitle("Warning", message: message, cancelButtonTitle: "OK") } }
The implementation of showAlertWithTitle(_:message:cancelButtonTitle:)
is similar to the one we've seen in the AddToDoViewController
. Note that we remove the key-value pair when the user taps the button of the alert.
// MARK: - // MARK: Helper Methods private func showAlertWithTitle(title: String, message: String, cancelButtonTitle: String) { // Initialize Alert Controller let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) // Configure Alert Controller alertController.addAction(UIAlertAction(title: cancelButtonTitle, style: .Default, handler: { (_) -> Void in let userDefaults = NSUserDefaults.standardUserDefaults() userDefaults.removeObjectForKey("didDetectIncompatibleStore") })) // Present Alert Controller presentViewController(alertController, animated: true, completion: nil) }
We show an alert to the user, but it's a good idea to take it a few steps further. For example, you could urge them to contact support and you can even implement a feature that lets them send you the corrupt store. The latter is very useful for debugging the issue.
Step 5: Testing
Persisting data is an important aspect of most applications. It's therefore important to properly test what we've implemented in this article. To test our recovery strategy, run the application in the simulator and double-check that the persistent store was successfully created in the Application Support directory, in the Stores subdirectory.
Add a new attribute to the Item entity in Done 2.xcdatamodel and run the application one more time. Because the existing persistent store is now incompatible with the data model, the incompatible persistent store is moved to the Incompatible subdirectory and a new persistent store is created. You should also see an alert, informing the user about the problem.
Conclusion
Migrations are an important topic if you plan to make extensive use of Core Data. Migrations let you safely modify your application's data model and, in the case of lightweight migrations, without much overhead.
In the next article, we focus on subclassing NSManagedObject
. If a Core Data project has any kind of complexity, then subclassing NSManagedObject
is the way to go.
Comments