ylliX - Online Advertising Network

Backing up Core Data Stores


Today we’re going to travel back in time a little with Core Data. Or at least find out how your app can do so. What if you want to make a backup copy of your app’s data? What if you want to restore from that backup later on? This won’t be mainly about data safety, because your app’s data will be getting backed up to the user’s iCloud account and, maybe, their Mac. But you might have an app that, for whatever reason, needs to be able to revert to an older version of its data.

It’s just files, right?

Core Data is a file-based persistent store, so it might seem like a good idea to just copy files around. Head over to the FileManager documentation, figure out where the persistent store is located (hint: if you’re not sure, you can use the NSPersistentStoreDescription), and hey, you’re done.

In a very simple case, that might work. But, hey, make sure you also copy the journal files. Both of them. And oh, did you turn on “allows external binary storage” anywhere in your data model? Better copy the external binary files too. Also, the location of those files is undocumented. If you figure it out, it probably won’t change, but you can’t be sure. There might be other extra things to think about in the next version of iOS, who knows?

And of course, dont forget the possibility that not everything has been flushed from memory to the files when you copy them. It probably has been, of course. Probably.

Fortunately Core Data has a built-in mechanism for this exact purpose. Rather than know every little detail of copying the files, you can ask Core Data to copy whatever’s needed.

Migrate the Data Store!

No, not that kind of migration.

With Core Data, data migration usually refers to changing your data model and migrating a persistent store to the new model. That’s not what I’m talking about here, but the function naming is a little confusing.

There’s a method on NSPersistentStoreCoordinator which unfortunately has a name that makes it sound like it’s related to model migration. The migratePersistentStore(_:to:options:withType:) method actually does something completely different. It’s been around approximately forever but doesn’t seem to be well understood, if discussions on Stack Overflow and in developer Slack teams is any indication.

You can use this method to make a new copy of a persistent store at a new location, which is conveniently the exact thing you need to make a backup copy. (It also lets you change the type of persistent store, but I’ll ignore that since it’s not relevant here). The basic way to use it is

  1. Load a persistent store.
  2. Choose a new location for the store.
  3. Use this method to write a copy of the store at the new location.

It’s almost that simple. Except for one warning included in the documentation:

Important

After invocation of this method, the specified store is removed from the coordinator thus store is no longer a useful reference.

This is a little scary. Once you use this method, the persistent store you migrated is removed from the persistent store coordinator. It also adds the newly-migrated store to the coordinator. So now the store is using the new copy instead of the old one. That’s not good for a backup, since the new copy is the backup you just made that you want to leave alone. It’s also a potential app crasher, since any managed objects you already fetched came from a persistent store that’s no longer available.

We can deal with that. But first, to clear up a point of confusion for some. The existing persistent store is removed only from the persistent store coordinator. The actual persistent store files don’t get removed, and all their data is intact.

We can make a backup safely by modifying the steps above slightly.

  1. Load a persistent store.
  2. Create a secondary persistent store coordinator and load the store again.
  3. Choose a new location for the store.
  4. Use this function on the secondary coordinator to write a copy of the store at the new location.

This leaves the existing Core Data stack intact, so the app keeps working with its existing persistent store. The secondary persistent store coordinator makes the backup copy. When the copy finishes, the backup copy is loaded in the secondary coordinator. But we won’t use that coordinator any more, so it doesn’t matter.

One possible implementation of this can be found in the copyPersistentStores function at this Gist. The core of it is

let temporaryPSC = NSPersistentStoreCoordinator(managedObjectModel: 
    persistentStoreCoordinator.managedObjectModel)
let destinationStoreURL = destinationURL.appendingPathComponent(storeURL.lastPathComponent)
do {
    let newStore = try temporaryPSC.addPersistentStore(ofType: persistentStoreDescription.type, 
        configurationName: persistentStoreDescription.configuration, 
        at: persistentStoreDescription.url,
        options: persistentStoreDescription.options)
    let _ = try temporaryPSC.migratePersistentStore(newStore, 
        to: destinationStoreURL, 
        options: persistentStoreDescription.options, 
        withType: persistentStoreDescription.type)
} catch {
    // ...
}

This takes an existing, loaded persistent store, and creates a copy in a new directory with the same file name, options, and type. I wrote this as an extension on NSPersistentContainer since it’s where recent apps load their persistent stores.

One extremely handy thing about this is that if the destination backup store already exists, it gets updated in place. It’s not necessary to remove the old backup first. Unless you want to offer multiple backups, you can back up to the same destination over and over and it’s fine.

I Want My Data Back

That’s good, but what about restoring from the backup?

This can be a little tricky to get right. Restoring the persistent store isn’t hard. Restoring and not crashing your app can be harder, because now you’re replacing the data store for a live Core Data stack. You’re replacing the lowest level of the stack and trying to prevent the whole thing from falling over. If you restore a backup, you replace the loaded store. Your app has probably loaded some data from its persistent store. Any managed objects in memory are now the app equivalent of a land mine. If you don’t touch them, nothing bad happens, but if you do, well, bad things happen.

We can use the same method to restore a persistent store as we did when backing it up. Migration can go either way, so restoring is a migration from the backup copy to the live copy. But remember what I said earlier about how the migration destination gets updated in place? That happens here too. If the primary store exists during restoration, you could end up duplicating data. That would be bad.

The full series of steps to restore is

  1. Make sure you can avoid using any existing managed objects.
  2. Remove the persistent store from memory. (This is the part that causes in-memory land mines if you weren’t careful).
  3. Destroy the main persistent store. I’m saying “destroy” because the function is literally named destroyPersistentStore(at:ofType:options:). This removes current data, making room for the restore.
  4. Load the backup store at its current location. For the moment the app will be using that persistent store, but not for long.
  5. Migrate the backup store to the primary store location. This will remove the backup store from memory and add the newly migrated primary store.

An implementation of this can be found in the restorePersistentStores function at that same Gist.

One difference from the backup process is that a temporary persistent store coordinator isn’t necessary. For a restore, the migration destination is the one we want to use, so we don’t need to set up a separate coordinator.

Step 1 can be the hardest part of the whole process, and the details depend completely on your app. Any managed objects that were fetched before restoring, or anything that has a reference to one of them, needs to be out of memory. You can then redo any fetches and update your UI. If your app is really simple, you might only need to do this after restoring:

do {
    try self.fetchedResultsController.performFetch()
} catch {
    print("Perform fetch error: \(error)")
}
tableView.reloadData()

But most apps will need more, and what works for one app won’t be right for another. You might need to design the restore-from-backup UI to be completely separate from the rest of the app so that you can completely unload any existing in-memory data that might be a problem.

Steps 2-5 above look something like this (see the gist for the full version):

do {
    // Remove the existing persistent store first
    try persistentStoreCoordinator.remove(persistentStore)
    // Clear out the existing persistent store so that we'll have a 
    // clean slate for restoring.
    try persistentStoreCoordinator.destroyPersistentStore(at: loadedStoreURL, 
        ofType: persistentStore.type, 
        options: persistentStore.options)
    // Add the backup store at its current location
    let backupStore = try persistentStoreCoordinator.addPersistentStore(ofType: persistentStore.type, 
        configurationName: persistentStore.configurationName, 
        at: backupStoreURL, 
        options: persistentStore.options)
    // Migrate the backup store to the non-backup location. This leaves
    // the backup copy in place in case it's needed in the future, but 
    // backupStore won't be useful anymore.
    let _ = try persistentStoreCoordinator.migratePersistentStore(backupStore, 
        to: loadedStoreURL, 
        options: persistentStore.options, 
        withType: persistentStore.type)
} catch {
    // ...
}

When this finishes, the main persistent store has been replaced by a copy of the backup store. The backup is still there, but the Core Data stack is using the restored copy.

Other possibilities

This all assumes you want just one backup which you might later restore. If you wanted multiple backups, you would need to add some way to separate the backups and tag them with whatever relevant details distinguish one from another. If it’s only the date, one way would be to use different backup destination URLs, and include a timestamp or date string in the name. An alternative would be to use metadata on each backup to include as much information as necessary. See the NSPersistentStoreCoordinator documentation for details.

I’d be interested to hear what other approaches people use for Core Data backup and restore. You can find me at any of the social networking links in the sidebar, or else leave comments (pr revisions) on the Gist with the sample code.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *