How to implement Unique Constraints in Core Data with iOS 9

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.

Core Data Unique Constraints Dialog in Xcode 7.0
Unique Constraints Dialog for Core Data Entities in Xcode 7.0 (bottom right)
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:

  • Unique Constraints must be strings
  • When you try to save a context with more than one object with the same value for a Unique Constraint, the first one you created is the one that gets saved
  • If you have parent/child contexts, you need to set the merge policy on each one of them
  • The “Tools Version” of your data model file must be set to “Xcode 7.0”
  • Unique Constraints is iOS 9.0 only, so you need to set your deployment target to iOS 9.0 as well
  • 4 thoughts on “How to implement Unique Constraints in Core Data with iOS 9

    1. You still have to implement fetch / parsing / to find the delete records and delete them,

      how do you handle deleted records?

      If in core data I have 20 records for entity A, then I received a network request with 19 records, I still have to find the 1 record that has been deleted

      1. Right, that’s something that has to be adressed with your (REST) API design. It’s not really something that can be solved in CD on the client side alone.

        For example, I use to mark deleted records with an is_deleted flag on the server and include them in GET requests so I know which record I have to delete on the client. Of course I don’t want to get all deleted objects since the beginning of time with each request, so I keep track of the last time I did an update on the client and send that timestamp as a parameter of the GET request. Now the server can filter the results to include only deleted objects that have been deleted since my last request.

        There are probably other ways to do that, but this approach has been working fine for a while now.

    Leave a Reply

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