I have two MOCs: the first is the root context. When I save this context, the changes are saved to the persistent store coordinator. The second MOC has the first MOC as the parent. When I save the second MOC, I also have to save the first MOC in order to save the changes in the second MOC to the persistent store coordinator.
I use the second MOC to let the user edit an object. He can save or cancel the changes. When he saves the changes, all MOCs are saved. When he cancels the changes, I call rollback() of the second MOC.
Unfortunately, the object comes from the first MOC. This means, I execute an NSFetchRequest to fetch the object on the first MOC. Then I create the second MOC in which the user can edit the object. But there is a problem: when the second MOC should change something, for example delete an object that is contained in an array of the original object the user wants to edit, this is not possible, because a MOC can only delete objects that have this MOC as the context. But the object was fetched in the first MOC.
That's why I need to "transfer" somehow the object from the first MOC to the second MOC before the user edits the object. I don't want to fetch the object again with a NSFetchRequest or something, there must be a better way…
Is this possible? Or do you recommend to do this completely different, maybe without parent contexts?
This is where the objectID property of NSManagedObject will come in handy.
Ask the object for its ID
let objectID = myManagedObject.objectID
Ask the child context for a managed object with that ID
do {
let childManagedObject = try childContext.existingObjectWithID(objectID)
print("\(newObject)")
} catch {
}
I think you might be complicating your time with this. Unless it is completely necessary for proposed changes to also be saved there should be no reason to have two contexts for this. There are multiple ways for you to handle a temporary data which you can use to compare your actual record to without having it stored twice.
Why don't you just create a copy of the NSManagedObject and handle the information correction by comparison or simply replacing the original NSManagedObject with the data of the copy and then save it? I personally like this setup a bit more since all I have to do is either compare individual properties when update is desired.
When a full update is required than you can simply work directly on the one NSManagedObject without worrying about copies since you will probably be replacing the entire thing anyway. Like I said, there are other ways to handle, but if it is absolutely necessary for you to have both contexts then looking for a comparison of each property value to the replaced value and then simply save the one in the parent context.
Related
I have a NSManagedObjectContext where two NSManagedObject are saved.
I'm calling a method in another thread and I need to access those two NSManagedObject so I created a child context like the following:
let childManagedContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
childManagedContext.parentContext = self.managedContext
When I do:
let myNSManagedObject1 = childManagedContext.objectWithID(self.myNSManagedObject1.objectID) as! MyNSManagedObject
let myNSManagedObject2 = childManagedContext.objectWithID(self.myNSManagedObject2.objectID) as! MyNSManagedObject
myNSManagedObject1 and myNSManagedObject2 are not the same objects as self.myNSManagedObject1 and self.myNSManagedObject2. Can someone explain me why?
Plus if I use existingObjectWithID instead of objectWithID, it seems I still have a fault object for my relationship in myNSManagedObject1 and myNSManagedObject2:
relationShipObject = "<relationship fault: 0x170468a40 'relationShipObject'>"
Understand that they are the "same" in the sense that they refer to the same object in your object graph. If you compare all attributes, you will find that they are equal.
However, because they are in different contexts, they will be two separate instances of this object. So the machine address you see will be different. I hope that clears up the confusion.
As for the "fault", that only means that the underlying object (or attribute) has not yet been fetched into memory. This is simply an optimization mechanism to minimize memory footprint. If you were to log the object or attribute explicitly, it would be fetched from the store and displayed as expected. See "Faulting and Uniquing" in the Core Data Programming Guide.
You have one object, that is the version that's in Core Data. When you use objectWithID: you create an instance of that object. So, if you do it twice you get two instances of the same object. (Much in the same way that you can create two objects of the same class.)
Of course, if you try to save your context, having changed one but not the other, weird things might happen.
A common pattern is where you create a new "editing" managed object context and create a new instance there. Then if the user pressed Cancel, you can just delete the context and not have to worry about rolling back any changes. I can't think where having two instances on the same context would be useful.
I have created two context like this:
// create writer MOC
_privateWriterContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateWriterContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
// create main thread MOC
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.parentContext = _privateWriterContext;
I have a NSFetchResultedController initiated with _managedObjectContext.
I know it is strange, but I am adding a record to the parent to _privateWriterContext, I am saving it.
What is surprising is that child context and so FRC gets notified about this event. Why? I have not reset-ed child, or anything else. I thought they are independent entities as long as child context will not get saved.
In #pteofil article I found this line:
When a change is made in a context, but not saved, it is visible to all of its’ descendants but not to its’ ancestors.
.. it is pushed to the persistent store (via the persistent store coordinator) and becomes visible to all contexts connected to the store.
This is not supposed to happen. Adding an NSManagedObject ('record' ) to the parentContext, will not make the child aware of this object automatically. Only when you make the childContext execute a fetch, then it will fetch from the parentContext.
To figure out what is going on in your case, here are some suggestions:
figure out when a fetch is executed on the childContext (this is done by the fetchedRestultsController when you set it up. Check if that fetch happens before or after you add the managedObject to the parentContext).
set breakpoints in all four delegate callbacks of the fetchedResultsController to find out for which object it is calling the methods (and see if it is the object you just added to the parentContext).
make absolutely sure you know which context you are sending messages too.
I have a used a similar approach, but different: the childContext is the context is used to parse in new data (on a private queue), and when this parsing is done, the chid calls save:. This will save the changes up to the parent, which is in my case the mainQueueContext. This call to save: will cause the mainQueueContext to receive all the newly parsed objects, and any fetchedResultsController using that mainQueueContext will then call it's delegate methods for the new/changed/updated/delete objects.
You could try inverting your child/parent relationship too and see if it works as described in the docs, just to find out whats going on.
I strongly recommend avoiding parent-child context setups. Our book goes into detail why they often lead to strange behaviour: https://www.objc.io/books/core-data/
Short story: They're not as independent as you might think.
Use multiple context that share a single persistent store coordinator if you need more than a single context.
Say I have a CoreData entity type called Player and it has a to-one relationship (purpose) with an entity type called PlayerPurpose. For completeness, say we have an inverse relationship in PlayerPurpose called parentPlayer. Consider the following swift code:
// Assume we already have a player object in a NSManagedObjectContext called context:
player.purpose = NSEntityDescription.insertNewObjectForEntityForName("PlayerPurpose",
inManagedObjectContext: context) as PlayerPurpose;
// Later in the code, we set the value to nil (or we could have replaced
// it with another call to insertNewObjectForEntityForName)
player.purpose = nil;
// What happens to the previous playerPurpose object within the Managed Object Context?
My question: what happens to the original playerPurpose object within the Managed Object Context when the only reference it has in the data is set to nil (or replaced with another object)?
This is not really related to relationship deletion rules because I'm not explicitly deleting any object -- I'm removing it from any meaningful relationships, making it an orphan.
From an ARC perspective (if the PlayerPurpose was just a normal, non-managed object), the original PlayerPurpose instance now has no references, so it can be cleared from memory -- but what happens in the Managed Object Context? Does CoreData recognize this as an orphaned object and delete it via the context?
If not, then I assume I have to be careful to delete any managed object created via a context if I'm going to get rid of all references to it. Assuming that's the case, is there a good pattern go use for making sure that orphaned objects get cleared from the NSManagedObjectContext and that they are no longer stored in the persistent store?
Thanks!
Core Data does not automatically delete objects in this scenario, because "orphaned" is a concept that your code has but not one that Core Data recognizes. There's no reason for it to delete a PlayerPurpose object just because one of its relationships is nil.
The most reliable way to ensure that PlayerPurpose instances are deleted would be to
Create custom NSManagedObject subclasses for your entities (if you don't have them already).
Override the setter method for purpose on the Player subclass. If the new value is nil, delete the old one.
You can also handle this by just making sure to call deleteObject: at the appropriate times. Or you could run a clean-up step where you fetch every PlayerPurpose with a nil value for parentPlayer and delete them.
I have a CodeData model [Phone]<<--->>[Forwarding]. So the Phone object has a Forwardings set, and vice versa.
I have a list of Phones and want to add one of them to a new Forwarding.
In the ForwardingViewController I do:
// Create a new managed object context; set its parent to the fetched results controller's context.
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[managedObjectContext setParentContext:[fetchedResultsController managedObjectContext]];
self.forwarding = (ForwardingData*)[NSEntityDescription insertNewObjectForEntityForName:#"Forwarding"
So this creates a child MOC and a now temporary Forwarding.
Then I pass self->forwarding to my PhonesViewController which shows all Phones (in a table). This view controller is simply navigation-pushed.
When the user taps on one of the Phones in the table I do:
[self.forwarding addPhonesObject:phone];
The addPhonesObject is a CoreData generated accessor.
Now, when the user is back at the ForwardingViewController and taps the Cancel button (because he decides he does not want to create a new Forwarding after all), it is dismissed, which cleans up this child managedObjectContext and also self.forwarding.
After doing the above, I get a database error (Cocoa error 1550). When trying to understand the console output, my guess is that the Forwarding was indeed deleted, but that the Phone object (which of course is still there), now has a null reference to this deleted Forwarding.
My question. How should I handle this case correctly: Having a temporary object created on a child MOC, link it to another object (on the parent MOC), and then delete this temporary object again.
What is the actual error you are getting?
From your description, I am guessing that your PhonesViewController is listing phones from a different NSManagedObjectContext than the one that you created the ForwardingData entity from. This violates the relationship rule with Core Data. The rule is simple, to create a relationship between two entities they must both be from the same NSManagedObjectContext instance.
I question why you are creating a temporary NSManagedObjectContext in this situation. Since you are retaining the ForwardingData entity and you know when you are being cancelled, it seems cleaner to just delete the temporary entity when cancel is pressed instead of standing up another NSManagedObjectContext.
Update
If you need to use the child (per your comment), then you should change your PhonesViewController to accept a NSManagedObjectContext via dependency injection. Then you can send it the same NSManagedObjectContext instance as the one you used to create the new entity. With that change everything will work as you expect it to.
I'm confused on how the a child MOC (NSPrivateQueueConcurrencyType) works with a parent MOC (NSMainQueueConcurrencyType) with respect to the following scenario. I need to has a background thread check a web server for new or updated data. When something is received, I need to save or update the parent MOC. Sounds simple enough, and I see various examples. However, I'm still confused on a couple of things.
When processing data in the background thread, I can easily save the object in the child MOC. However, in my data model, I have relationships set up as recommended. So, for example, a ConversationThread will contain an array of Messages. Each Message will have Message.parentConvoThread set in the relationship. So, whenever I'm getting new messages from the server...
How do I associate the new Message object, which is created in the child privateMOC, with the ConversationThread (currently in the parent mainMOC)?
Now, say that I'm getting updated personal info for the person who wrote the message. I see they have updated data on the server, so I need to update their data in app. In the privateMOC...
How do I get the actual object (say it's MyContact) from the mainMOC to explicitly update? or...
If I create a new MyContact in the privateMOC, how do I merge that with the currently existing MyContact in the mainMOC? ...or does it automagically happen somehow? (<- I've read a lot of older threads that say you to use NSManagedObjectDidChangeNotification and manually merge but that this isn't necessary anymore...but how/why?)
Finally, a couple of questions about searching...
Can a search against the child privateMOC return results from the parent mainMOC (say if an entity exists in the parent but not the child)?
If the answer to #1 is true, what happens if the entity exists in both but hasn't been merged?
I'm quite confused on how they work together. Any help is greatly appreciated.
NSManagedObjectContexts are in memory caches of the data from an NSPersistentStore. A fetch on a child context will be executed through the parent context on the NSPersistentStore, and the data from the objects will be retrieved from either the cache in the child context, the parent context, or all the way from the persistent store (wherever it can first find the data).
If you are fetching from a child context, the results will be retrieved through the parent context, and you can expect this fetch request to return objects as though you fetched from the parent context.
Going the other direction, as long as all the changes you have made to your child context have been saved, those changes will be reflected in the parent context, because core data automatically handles the merge from child to parent.
The only trick is if you have references to objects in the child context, and changes are saved in the background to the parent context, you will either need to re-fetch those objects on the child context to get changes from the parent, or you can manually merge the changes on the parent's NSManagedObjectContextDidSaveNotification. See this post for more information: How to keep a child NSManagedObjectContext up to date when using bindings.