How to implement the new Core Data model builder 'unique' property in iOS 9.0 Beta - ios

In the WWDC15 video session, 'What's New in Core Data' at 10:45 mins (into the presentation) the Apple engineer describes a new feature of the model builder that allows you to specify unique properties. Once you set the those unique properties, Core Data will not create a duplicate object with that property. This is suppose to eliminate the need to check if an identical object before you create a new object.
I have been experimenting with this but have no luck preventing the creation of new objects with identical 'unique' properties (duplicate objects). Other than the 5 minute video explanation, I have not found any other information describing how to use this feature.
Does anyone have any experience implementing the 'unique' property attribute in the Core Data Model?

Short answer:
You'll need to add this line to your Core Data stack setup code:
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Long answer: I struggled with this for some time, but I think I have figured it out now:
Unique Constraints (UC) do not prevent creating duplicates in a context. Only when you try to save that context, Core Data checks for the uniqueness of the UCs.
If it finds more than one object with the same value for a UC, the default behaviour is to throw an error because the default merge policy for conflicts is NSErrorMergePolicyType. The error contains the conflicting objects in its userInfo.conflictList, so you could manually resolve the conflict.
But most of the time you probably want to use one of the other merge policies instead and let Core Data merge the conflicts automatically. These merge policies did exist before, they are used for conflicts between objects in different contexts. Maybe that's why they were not mentioned in the context of the UC feature at WWDC Session 220. Usually the right choice is NSMergeByPropertyObjectTrumpMergePolicy. It basically says "new data trumps old data", which is what you want in the common scenario when you import new data from external sources.
(Tip: First I had problems verifying this behaviour, because the duplicate objects seem to remain in the context until the save operation is finished - which in my case happened asynchronously in a background queue. So if you fetch/count your objects right after hitting the save button, you might still see the duplicates.)

I don't know the right answer, as this is a beta version, but after playing with it for a minute I found a way to make it work:
Tell the model which attributes form the unique constraint, exactly as shown in the image you have in your question.
Add a new record:
let newTag = NSEntityDescription.insertNewObjectForEntityForName("Tag", inManagedObjectContext: context) as! Tag
Assign the values to the attribues.
Save your changes:
do {
try context.save()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
context.reset()
}
The key is in the catch block. If an error happens, reset the context to the previous state. As the save operation failed, the duplicate records won't be there.
Please notice that you should check the error to see if it was caused by a duplicated record.
I hope this helps.

Related

Core Data double-inserting child records in one-to-many association

We have an iOS application that uses Core Data to persist records fetched from a private web API. One of our API requests fetches a list of Project records, each of which has multiple associated Location records. ObjectMapper is used to deserialize the JSON response, and we have a custom transformer that assigns the nested Location attributes to a Core Data association on the Project entity.
The relevant part of the code looks like this. It's executed within a PromiseKit promise (hence the seal), and we save first to a background context and then propagate to the main context that gets used on the UI thread.
WNManagedObjectController.backgroundContext.perform {
let project = Mapper<Project>().map(JSONObject: JSON(json).object)!
try! WNManagedObjectController.backgroundContext.save()
WNManagedObjectController.managedContext.performAndWait {
do {
try WNManagedObjectController.managedContext.save()
seal.fulfill(project.objectID)
} catch {
seal.reject(error)
}
}
}
The problem we're having is that this insert process is saving each Location record to the database twice. Strangely, the duplicated Location records don't have any association with their parent Project record. That is to say, if Location records are looked up with an NSFetchRequest, or if I run a query on the underlying SQLite database, I can see that there are two entries for each Location, but project.locations only returns one copy of each Location. The same (or very similar) process applied to other record types with the same structure also results in duplicates.
I've tried several things so far to narrow down the problem:
Inspected the API JSON - no duplicates.
Inspected the state of the project.locations property immediately before the Core Data write. No duplicate records are present prior to the objects being persisted, indicating that the deserializer and custom nested attributes transformer are working correctly.
Removed the block that propagates the changes to the main thread managed object context, in case this was causing the insert to occur twice. Still get duplicates with solely the write to the background context.
Run the app with com.apple.CoreData.ConcurrencyDebug 1 set. No exception is thrown in this process, confirming that it's not a thread safety issue of some kind.
Run the app with com.apple.CoreData.SQLDebug 1 set. I can see in the logs that Core Data is inserting exactly twice as many Location rows as expected into the underlying SQLite database.
Implemented a uniqueness constraint on the entity. This fixes the problem in terms of what data gets persisted, but will still throw an error unless an NSMergePolicy is set.
The last item in that list effectively solves the problem, but it's treating the symptom, not the cause. Data integrity is important for our application, and I'm looking to understand what the underlying problem might be, or other options I might pursue for investigating it further.
Thanks!
A year and eight months later, I finally got to the bottom of this bug when a similar issue occurred with a different set of records. The problem was that I was calling ObjectMapper on each Location object twice. I was using ObjectMapper's mapArray method within a custom ObjectMapper TransformType to deserialize and persist the Location records associated with each Project, which worked as follows:
let locations = Mapper<Location>().mapArray(JSONObject: value as AnyObject)
However, what I had overlooked is that I was also overriding the constructor for Location and calling ObjectMapper again there:
required public init?(map: Map) {
let entity = NSEntityDescription.entity(forEntityName: "Location", in: WNManagedObjectController.backgroundContext)
super.init(entity: entity!, insertInto: WNManagedObjectController.backgroundContext)
mapping(map: map)
}
The line mapping(map: map) was unnecessary, and proved to be the culprit. In a similar scenario with two levels of one-to-many associations, this had the somewhat amusing consequence of quadrupling (!) the records at the second level - their parents had been duplicated, each copy of which subsequently duplicated its children. This was what ultimately led me to the cause of the bug.

Change relationship of NSManagedObject to different context

This is a follow up to an earlier question: Core Data: change delete rule programmatically.
I'd like to rephrase my question, and will do that here.
Briefly, my app allows updating entries from a 3rd party database, but I'd like to keep user annotations. So my workflow is:
iterate over all entities
download external xml and parse it into a new entity
if user annotations, change their relationship from old entity to new entity
delete old entity
During the import, the old entity is in the main context, the new entity is in a temporary import context.
Number 3 gives me problems, if I just change the relationship, then they don't show if I update my UI. If I use the objectID to get the annotation and then change the relationship as follows:
NSManagedObjectID *objectId = oldAnnotation.objectID;
Annotation *newAnnotation = [importContext objectWithID: objectId];
[newEntry addAnnotationObject: newAnnotation];
It's still not working - it's not showing up.
EDIT: if I change the context in the second line to newEntry.managedObjectContext, I get an Illegal attempt to establish a relationship 'foo' between objects in different contexts error.
What am I missing?
UPDATE: After some late-night hair-pulling debugging, I found that I when I was fetching the newEntry, I was actually fetching the oldEntry, therefore none of the changes would show up. The answer below by #Mundi pointed me in the right direction.
Copying the old annotations worked using my code above, followed by copying the attributes. For some user input with relationships in itself, I had to do a "Deep Copy", which I found here: How can I duplicate, or copy a Core Data Managed Object?.
I think creating a new entity and deleting the old one is a problematic strategy. You should try to properly update the existing entities and only create new ones if they do not yet exist.
Whenever I need an object from a different context, I fetch it. That being said, your object id code should work. However, there could be all sorts of other glitches, that you should check:
Did you save the importContext?
Did you save its parent context, presumably the main context?
Was the modified object graph saved to the persistent store?
Are you checking the results after you have saved?

Can anyone give me some reference for this method "refreshAllObjects" in NSManagedObjectContext

[managedObjectContext refreshAllObjects]
Actually I am getting random error sometime during save context and when I call [managedObjectContext refreshAllObjects] after error, it allows me to save.
Could anyone please guide me about this method.
Calling refreshAllObjects calls refreshObject:mergeChanges on all objects in the context. You can see the documentation on refreshObject:mergeChanges here:
https://developer.apple.com/library/ios/documentation/Cocoa/Reference/CoreDataFramework/Classes/NSManagedObjectContext_Class/#//apple_ref/occ/instm/NSManagedObjectContext/refreshObject:mergeChanges:
It is possible that your persistent store has been modified by some other context, so you get an error when you try to save to it from your current context. If you refresh your current context first, then any modified data will be merged, and you can now save without conflicts.
When you get an error during a context save, you may have a merge conflict between the context and the persistent store. If you update your context before the save by refreshAllObjects(), refresh(_ object:mergeChanges:) is called for every object in the context, where mergeChanges: is true. This means that attributes changed in the context are kept while attributes changed in the persistent store are updated. This is exactly what is done automatically, if you set context.mergePolicy to NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType, see the docs here and here.
But this maybe not what you want. Consider a situation where an entity with an attribute updatedAt can be changed locally and remotely, and the requirement is that individual attributes may not be mixed, but only the complete entity that has been updated last should be kept. In this case, none of the predefined merge policies apply, and one has to set up a custom merge policy that checks the updatedAt attribute. How this can be done is described here.

How to add unique constraints for some fields in Core Data

I use Xcode for iOS development. I have some entity (for example, User), and I need to set unique constraint for his name, but I can't find how I can do it through visual editor. Is it possible to do it through GUI? Or it's possible through code only? I will be glad to get some screenshot.
There's a new section in the sidebar when selecting an entity in the editor for Core Data. You can set what constraint(s) you want to be unique across all instances of an entity
For automatic conflict resolution during saves, you'll need to make sure you've got a merge policy set for your managed object context or else you'll just get errors when saving (which might actually be what you want)
[managedObjectContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
The "Swift version" is exactly the same
managedObjectContext.mergePolicy = .mergeByPropertyObjectTrumpMergePolicyType
Keep in mind conflict resolution only happens during saves, and not inserts. So if you're making use of a NSFetchedResultsController you will see entities with non-unique constraints as they're inserted.
If you want to sure you have no entities with non-unique constraints in your managed object context without saving (if you're making use of a FRC), this answer is still probably the best way to go. Although, keep in mind, it's expensive if you're doing a lot of inserts, since NSFetchRequests are expensive operations.
Sample code for this demo can be found here
Swift solution:
As noted in the other answer, you can have unique constraints in Core Data for iOS9 onwards.
To do this, first add constraints to the Entity from the Core Data Editor (explaination in Zachary's answer).
Then add this line in code:
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Note: The uniqueness is checked when you do managedObjectContext.save() not while you're just adding objects to the managed object.
NSMergeByPropertyObjectTrumpMergePolicy is just one of the merge policies, which overwrites the old value with the new one. You might want to check the documentation for other options.
swift Version Is Easy
Disclaimer : If You Have Conflicting information please delete piror to implementation. Else App will not run.
Solution Delete from device and start again
steps are:
Open Core Data File (projectname.xcdatamodeld)
Click on the entity name (needs to be highlighted)
Right Side of screen (in class section) find constraints (hit the plus button)
Right click to edit info rename to Attribute.
// now to add the code in you core data container
open AppDelegate.swift file and scroll into the coredata stack (" // MARK: - Core Data Saving support ")
update the code for the static func saveContext() { let variable = persistentContainer.viewContext "
//now make this simple call that manages update process
variable".mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy"
// clear understanding
static func saveContext () {
let context = persistentContainer.viewContext
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// you need that line
if context.hasChanges {
do {
try context.save()
} catch {

What's the point of self.managedObjectContext == nil in NSManagedObject prepareForDeletion?

I have a Reminder entity that needs to update its date property whenever a certain entity B is deleted. I've spent some days coding thinking I could do some useful things in my managed object subclass on deletion time. I tried
- (void)willSave
{
if (self.isDeleted)
// use self.managedObjectContext
}
The context was nil. Relationships were also torn down there. Fair enough.
So... I started writing cumbersome code for prepareForDeletion to circumvent the fact that the object hadn't been deleted yet, but then Core Data throws self.managedObjectContext == nil in my face. The documentation says that this is where I do stuff "before relationships are torn down". So what is the point in self.managedObjectContext == nil if self.relationshipA.managedObjectContext is accessible (as the docs suggest)? And more importantly, why does my not yet deleted object not have its context?
I read a comment here regarding that problem
its not 'fault' as much as it is a 'disown', the context has disowned your object (he was deleted and save was committed to the database) and so your object was disowned. don't save in methods that are changing and object as the save should probably be committed/saved after the operation anyway. – Dan Shelly May 21 at 19:05
My code was:
[moc deleteObject:obj]
[moc save:NULL]
When I removed the save operation my self.managedObjectContext existed in prepareForDeletion. That is, until auto-save, when it was nil again. Probably because the parent context also deleted it, followed by a save by the UIManagedDocument.
I'm starting to think that my only options are to make a custom delete method (that works until Core Data cascades a deletion, in which case it won't be called), or make a new class that listens to NSManagedObjectContextDidSaveNotification.
Update:
The user wants to keep in touch with a person, and wants to be reminded after a certain interval (stored in ContactWish) if no contact has been made. What I'm trying to accomplish is that when the latest ContactOccasion for a certain person is deleted, the corresponding occasion->person->wish->reminder gets updated (using the interval).
Since this is a learning experience for me I wanted to find out the right way (one that works with cascade deletion etc.) and not just call for an update manually from every place in my code where I do [MOContext deleteObject:occasion]. Suggestions are welcome.
(the reminder entity has also been prepared for more manual use)
Would it not be much more logical to have the Reminder entity manage its date property? It could "listen" (maybe via changedValues:) to its relationship entities being deleted and perform the update.
This seems more consistent, as the B entity should not really be concerned with the logic of the Reminder entity updates.
Edit
Pursuant to the discussion below and based on my opinion that you cannot load up the database cascade delete model too much with update logic:
Rather than react to a deletion you can introduce an attribute that you set and listen to in order to do the changes.
I really do not see how relying on core data delete mechanisms is easier or more elegant than just writing your own "deleteOccasion" method that handles this logic.

Resources