With iOS 9, Apple introduces Unique Constraints for Core Data. This new feature was briefly demonstrated at WWDC in June (Session 220). When I tried to implement it, I came across a few pitfalls and unexpected behavior, so I thought this might be worth a blog post.
Unique Constraints are a way to declare a custom attribute to be unique across all instances of an entity. Its intended use case is the import of external data that should be merged with existing objects in the database.
No more fetch/if/else
Until now, when you wanted to import data objects into Core Data from a file or network request, you had to create a fetch request for each incoming object with a predicate that matches the id and then execute it to look for an existing version of that object. If you found one, you would update it, otherwise create a new object. With Unique Constraints, you don’t have to do this fetch/if/else anymore and save lots of DB requests while parsing the data.
Instead, you list the name of the id attribute(s) that you use in the new Constraints box of the Entity Property Inspector in the Data Model View in Xcode 7. In your code you then just create a new object with NSEntityDescription.insertNewObjectForEntityForName(_:inManagedObjectContext:), set the value for the unique attribute (and all other attributes you’re importing) and finally save the context. Core Data will automatically check if there is an existing object with the same unique attribute and … throw an error if it finds one. At least that’s the default behavior, which caused some confusion.
Unique Constraints conflicts are like merge conflicts…
Core Data handles the new kind of conflict caused by multiple instances with the same custom unique attribute in the same way as conflicts between different versions of an object with the same internal objectID (which can happen when you use the same persistent store with different contexts). That makes perfect sense but was a little surprising to me at first, because I never used to get this kind of conflict with newly created objects (they can’t have a newer version with the same objectID in the persistent store).
It doesn’t matter if the conflict exists between two newly created objects in the same context or between a new object and an existing object in the database. Core Data handles all conflicts according to the setting for the mergePolicy of the context that you’re trying to save. The default value NSErrorMergePolicy throws an error that includes a list of conflicting objects, so you could resolve the conflicts yourself (which wouldn’t be much of an improvement over the old fetch/if/else).
What you probably want to use instead is NSMergeByPropertyObjectTrumpMergePolicy, which overwrites every property of the old object that has changed in the new one. At least that’s how the latter works for conflicts between existing objects.
… but different
With Unique Constraints, NSMergeByPropertyObjectTrumpMergePolicy overwrites all attributes, just not the relationships. I guess that is inevitable as even an optional attribute on the new object that has not been set (i.e equals nil), is still “new” information compared to the existing object. Core Data cannot know if you haven’t set it because you want to keep the old value or if you want it to be nil on purpose in order to remove it.
So, if you just want to augment existing objects with new data, you’ll have to resort to solving the conflicts yourself (you could also create your own subclass of NSMergePolicy to do so).
A more elegant way in such a case (and probably also a better overall design) is to split your object into separate entities that are connected with a relationship. Take for example a shopping app where the user can tag the products. When you update the product price by importing external data, you don’t want to lose the user tags. Instead of writing the tags into a string-attribute of your Product entity, you could create a Tag entity, attached to the product with a to-many relationship. Now when you insert a new Product with the updated price and save using NSMergeByPropertyObjectTrumpMergePolicy, Core Data will overwrite the price (together with all other attributes) but keep the relationship to the user-defined tags.
A few other things I came across: