Reputation: 2898
I’ve realized I need id’s for all of my Core Data entities to fetch by those id’s. As the NSManagedObjectID
can change, I want to add a custom id attribute of the type UUID
.
First I planned to add the id as a non-optional UUID. Though, as CloudKit seems to require that all attributes are optional anyway, it seems like a good idea to mark this new UUID as optional (should I add iCloud sync later). This makes it also eligible for a lightweight Core Data migration.
Even though, the id is marked optional in the Core Data Model, I want to ensure all of the entities (both existing and new ones) actually have an id/UUID saved!
How do I populate the new UUID attribute during the Core Data migration? Do I have to use a manual migration for this?
Upvotes: 1
Views: 665
Reputation: 2898
After testing the approach by Tom Harrington, I couldn’t find out why my mapping model isn’t working for some unknown reason. I’ve redone it so many times and checked every possible part.
Nevertheless, I’ve decided to go for a lightweight migration now. It’s recommended by Apple anyway and this is future proof for adding CloudKit support later on. As CloudKit
requires attributes to be optional or have a default value set, if non-optional. As both options don’t really make sense for an ID, I have to handle this myself to ensure the id’s are unique anyway.
I’m basically following Michael Tsai’s suggestion now, but wanted to elaborate a little further.
First I’ve created a new version of my Core Data Stack via Editor > Add Model Version… and edited the new version.
Next I’ve set the active Core Data Stack to the new version via the editor sidebar. Besides that I’ve also set a model version identifier in in the sidebar:
To catch the database update and perform the actual migration I’ve added a custom implementation when creating my Core Data Controller.
container = NSPersistentContainer(name: "Migration_Test")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Core Data Loading Error: \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
// MANUAL MIGRATION (if needed)
// Get User Default for Last Migration Version (0 if not set)
let lastMigration = UserDefaults.standard.integer(forKey: "lastMigrationVersion")
// Get Core Data Model Version Identifier (Set in Sidebar)
let modelIdentifier = container.viewContext.persistentStoreCoordinator?.managedObjectModel.versionIdentifiers.first as? String
let currentModelVersion = Int(modelIdentifier ?? "") ?? 0
// Check if migration is needed
if lastMigration < currentModelVersion {
do {
// Perform Actual Migration
try ManualMigration().migrate(from: lastMigration, to: currentModelVersion, in: container.viewContext)
// Save Latest Migration Version to User Defaults
UserDefaults.standard.set(currentModelVersion, forKey: "lastMigrationVersion")
} catch {
print("Failed Migration to Version \(currentModelVersion)")
}
}
Inside my ManualMigration().migrate()
class/function I simply check which version I want to migrate to. In my case I’ve then iterated trough all my entities that changed via a fetch request that fetches all items of each give entity and saves a new UUID.
Upvotes: 0
Reputation: 70936
Yes, you'll have to do a manual migration for this, if you want every object to have a UUID immediately. To create UUIDs, you'll need to run code during migration, and to run code, you need to write a custom migration policy-- that is, a subclass of NSEntityMigrationPolicy
. The general process will be similar to something I described in a previous answer.
If you don't need every object to have a unique ID immediately, you could do it on the fly, as you access records. One way to do that would be to implement awakeFromFetch
in your Core Data subclass. In that function, check to see if the object has a UUID, and create one if necessary. Then when you save changes you'll save the UUID. This could be simpler but it would also mean you can't be sure that every object has a UUID.
Update with a lot more detail...
I tried out a similar migration, adding a non-optional UUID property to an entity. Here's how I set it up:
NSEntityMigrationPolicy
with this code:class Migration1to2: NSEntityMigrationPolicy {
@objc func newUUID() -> UUID {
return UUID()
}
}
Event
, I configured the mapping model to use the code above like this. The app name here is CDTest
:The migration worked. If yours still doesn't, try turning on migration debug logging. You can do this by editing the scheme and adding this to the "arguments" section:
When Core Data attempts to migrate, you'll see a lot of messages in Xcode's console about what's happening. (If no migration is needed, you'll see no messages from Core Data at all).
When migration works, it prints a bunch of messages about Incompatible version schema for persistent store
and version hashes, and then eventually a message that says found compatible mapping model
, and then the migration happens.
If you don't see that then Core Data couldn't match your mapping model to the model versions. That might mean that you edited the model after you created the mapping model. If that's true then you need to delete the mapping and re-create it. Another symptom of this is if the logs say Beginning lightweight migration on connection
anywhere. If it's trying lightweight migration then it couldn't find your mapping, so try the mapping model again.
Upvotes: 2
Reputation: 2030
CloudKit only requires that relationships be optional.
I would add the UUID attribute using lightweight migration and then fetch batches of the objects to set the UUID. This is likely simpler and much faster than doing a custom migration, and you don’t have to worry about whether your whole database fits in RAM.
Upvotes: 1