0

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.

Optional vs. Non-Optional 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.

How To Populate Attribute?

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?

alexkaessner
  • 1,966
  • 1
  • 14
  • 39

2 Answers2

0

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:

  • I made a subclass of NSEntityMigrationPolicy with this code:
class Migration1to2: NSEntityMigrationPolicy {
    @objc func newUUID() -> UUID {
        return UUID()
    }
}
  • I made a new mapping model that uses the original no-UUID model as the source and the newer with-UUID model as the destination. For my sample entity Event, I configured the mapping model to use the code above like this. The app name here is CDTest:

enter image description here

  • For the new property I made the entry in the mapping model look like this:

enter image description here

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:

enter image description here

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.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • That makes sense, thanks! Is there maybe a value expression to simply fill the id attribute with a generated UUID? – alexkaessner Aug 26 '23 at 09:49
  • I'm not sure what you mean about a value expression here. You'll need to create an instance of `UUID`. – Tom Harrington Aug 26 '23 at 17:45
  • At the attribute mappings steps (2. & 6.) in your [other answer](https://stackoverflow.com/questions/40647764/swift-coredata-migration-set-new-attribute-value-according-to-old-attribute/40662940#40662940) there is a value expression you can input. I was wondering if I can directly use a value expression there to simply fill in a random UUID aka performing `UUID()`. Or do I have to do the custom function that does this? – alexkaessner Aug 28 '23 at 09:54
  • 1
    You need a custom function. Step 2 there shows mappings from old values to new ones. Step 6 says what custom function to call when a simple mapping isn’t enough. – Tom Harrington Aug 28 '23 at 14:50
  • I’ve followed your steps now and it still doesn’t work for some reason. I have a custom function that simply returns a UUID in my `NSEntityMigrationPolicy` class: `@objc func createID() -> UUID { return UUID() }` The value expression in the mapping model uses this `FUNCTION($entityPolicy, "createID")`. When running the code it still crashes with `Validation error missing attribute values on mandatory destination attribute`. Any idea what this could be? I’ve tested the code and steps in a sample project and it works there … – alexkaessner Aug 30 '23 at 18:31
  • Are you sure that your function is being called? Try putting a breakpoint in the function to see if you get there. – Tom Harrington Aug 31 '23 at 03:46
  • For some reason it’s not even called. I suspected that and yep, the breakpoint is never reached … What could that be though? Maybe the module name? It’s defined via `$(PRODUCT_NAME:c99extidentifier)` in the build settings. So it should be the target name, because that one is also derived from `$(TARGET_NAME)` (as usual with modern Xcode projects). I’ve checked and copied the target name multiple times, but it still doesn’t reach my custom policy as defined in the mapping model by: `MeterStats.MigrationV1toV2` – alexkaessner Aug 31 '23 at 08:07
  • Thanks for the updated post! I’ve now double checked everything and recreated the mapping model once again. Though still I get the following error: `mismatched source and destination hashes for mapping model MappingV1toV2.cdm` in combination with `no match found for 1 of 4 mapping model source hashes` (same for destination hashes). I noticed that the hash of one entity that doesn’t change during migration is the problem. But it shouldn’t be?! In a test project that just copied over with no issues. I’m out of ideas as I recreated the mapping model so many times already … – alexkaessner Sep 01 '23 at 08:34
  • @alexkaessner I watched your video but I have no answers. I'm not familiar with that error. Since you have a test project where this works, you probably should carefully compare the two and hope something turns up. Sorry but that's all I have. – Tom Harrington Sep 01 '23 at 23:03
0

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.

Michael Tsai
  • 1,945
  • 17
  • 41
  • Doing such a fetch when starting the app would require to check for some flag or something right? I don’t wanna fetch the whole database every start to check for the id’s. How would you do such a flag? – alexkaessner Aug 26 '23 at 09:50
  • Good to know that only relationships have to be optional for `CloudKit`! That means I can go for a _non-optional_ UUID here, which makes more sense after all. Though, leads to a crash for the lightweight migration, because there is no value set for existing entities and also no default value to infer. I don’t wanna set a fixed UUID as a default value, that defeats the purpose. – alexkaessner Aug 26 '23 at 09:56
  • You would only do the fetch once. One way is to store a flag afterwards (e.g. in the store metadata), but you can also detect in advance whether migration is necessary by checking the store’s model version and then you would know to do the post-migration fixup. If you want it to be non-optional, use a fixed UUID as a placeholder and search for that in the fixup fetch. – Michael Tsai Aug 27 '23 at 02:38