Mastodon

Core Data Backups, Redux

This is the second of two posts following up on my earlier post about backing up and restoring Core Data.

My past post covered the difficulty of learning how to use mostly-undocumented framework methods, specifically about a Core Data method called replacePersistentStore(...). Arnaud Joubay recently messaged me to ask why I used a different approach when backing up and restoring persistent stores. Now that I’ve had some time to look at that method and work out what I can about how to use it, let’s see how it works in practice.

Replacing, not Migrating

Last time I used migratePersistentStore(...) for backups and restores. This time I’ll use replacePersistentStore(...). Superficially, at least, these methods seem nearly identical. Both take an existing persistent store and make a copy at a new location. They take approximately the same arguments, which specify persistent store options and store type. There’s no obvious reason to choose one or the other.

Why update my post then? There’s this post on Apple’s dev forum site from last summer that says, in part:

Additionally you should almost never use NSPersistentStoreCoordinator’s migratePersistentStore… method but instead use the newer replacePersistentStoreAtURL.. (you can replace emptiness to make a copy). The former loads the store into memory so you can do fairly radical things like write it out as a different store type. It pre-dates iOS. The latter will perform an APFS clone where possible.

I don’t know if that counts as documentation, but it’s from an Apple engineer, so… maybe? In any case it suggests that replace should be preferred to migrate, and that doing so might be more efficient due to APFS cloning. An anonymous forum post five years after the function was introduced isn’t a very clear endorsement but I’ll go with it for now and see where it takes me.

The demo app I’ve been using is now on GitHub. You can take a look here. Or go directly to the diff of replacing migrate with replace here.

Backing Up

The backup process is simpler than it used to be, because replace doesn’t have the same side-effect that migrate did of unloading the persistent store. I used a workaround of creating a secondary persistent store controller to deal with that, but it’s not necessary any more.

Backing up is still a relatively straightforward process, since it doesn’t interfere with the app’s current Core Data stack. Anything that you’ve loaded from Core Data continues to be valid, but afterward you have a new copy of the underlying files.

The guts of the backup code, somewhat simplified for this post, now look like this:

for persistentStoreDescription in persistentStoreDescriptions {
    guard let storeURL = persistentStoreDescription.url else {
        continue
    }
    let destinationStoreURL = destinationURL.appendingPathComponent(storeURL.lastPathComponent)

    do {
        try persistentStoreCoordinator.replacePersistentStore(at: destinationStoreURL, destinationOptions: persistentStoreDescription.options, withPersistentStoreFrom: storeURL, sourceOptions: persistentStoreDescription.options, ofType: persistentStoreDescription.type)
    } catch {
        throw CopyPersistentStoreErrors.copyStoreError("\(error.localizedDescription)")
    }
}

In essence it loops through all current persistent stores and does a replace for each one, creating a backup copy with the same options and store type. It’s OK if the destination doesn’t already exist, that just means replace will create a new store at the location. If the destination does exist, well, we just said to replace it, and that’s what happens. There’s no return value, but the function will presumably throw to tell us if it didn’t succeed.

Return to the Past

Even though the migrate and replace methods seem pretty similar, the semantics are slightly different when the destination is a currently-loaded store. My new restore code reflects that.

In the old approach it went like this:

  1. Get rid the persistent store you’re using with destroyPersistentStore.
  2. Load the backup copy in-place, that is, call addPersistentStore with the backup store URL.
  3. Tell the persistent store to migrate the store to the original URL. It would then use the original URL for new changes.

With the newer method, the process is:

  1. Tell the persistent store to replace the current store’s URL with data from the backup URL. This has an undocumented side-effect that it unloads the current store!
  2. Because of that side effect, re-add the URL for the persistent store you’ve been using.

These differences also mean that the new restore code works on the currently loaded persistent store descriptions, not the currently loaded persistent stores. The guts (again somewhat simiplified here) look like this:

for persistentStoreDescription in persistentStoreDescriptions {
    guard let loadedStoreURL = persistentStoreDescription.url else {
        continue
    }
    let backupStoreURL = backupURL.appendingPathComponent(loadedStoreURL.lastPathComponent)
    do {
        let storeOptions = persistentStoreDescription.options
        let configurationName = persistentStoreDescription.configuration
        let storeType = persistentStoreDescription.type
        
        try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupStoreURL, sourceOptions: storeOptions, ofType: storeType)
        try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions)
    } catch {
        throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)")
    }
}

The core of that is just a replace call where the source is the backup store URL and the destination is the URL of the store we’re already using. Since that gets unloaded, the next step is to re-add it.

As in my original post, restoring is a more delicate operation than backing up, because you’re changing the data that the app is using. And not in the sense of “normal” Core Data updates, but in the sense of yanking out the lowest level of the Core Data stack and replacing it, while the app is running. You still need to take precautions like making sure you don’t have any live Core Data-related objects, because they’ll all become invalid after restoring from backup.

Replacing Stores FT…W?

The new code works! Is it better? Well, probably. Documentation of why one way might be better than the other is sparse, at best. The only solid clues that this is better are an anonymous developer forums post and a somewhat cryptic comment in NSPersistentStoreCoordinator.h. It’s said to be preferred, and it might be more efficient, and it might or might not use APFS clones internally depending how you read things. If anyone from Apple sees this, please take a look at FB9054409, because some clear guidance would really help.