CoreData merging inserts - ios

So, in an app we have two NSManagedObjectContext's, lets call them context1 and context2. We have a situation in which an object, with customId=1, is inserted into context2, and context2 is never saved. At some point in the future an object is added to context1, with customId=1 also. context1 is then saved and when the completion notification is received the fun begins! We try to merge the changes from the save into context2 via:
[context2 mergeChangesFromContextDidSaveNotification:notification];
This works fine, it does the merge and then there are two objects in context2 both with customId=1. However, what I want to happen is, on merge, it somehow realises that both of the objects have the same customId and so instead of doing an insert, it just updates the existing object and internally makes the two the same object (or something to that effect :/). I had thought this may be possible by overriding isEqual and hash, but this is strictly forbidden for NSManagedObjects!
Another thought was to use validateInsert: and when it tries to insert the new object tell it not to and copy over the values. This however, causes another problem. We now have a persistent store with one object and context2 has a different object. We would then have to delete the object from context1 and save that change to remove the object from the persistent store... But since we never want to save context2 (this may seem odd, but we have valid reasons... I promise !) that object would then never be saved.
We basically want to be able to tell CoreData that after two inserts have been made they are actually supposed to be the same object! If anyone has ideas on how we may be able to do this, any help at this point would be greatly appreciated!

That type of merge strategy is something you need to deal with and is outside of the scope of the framework. Basically you have a dirty sandbox and a clean sandbox. When a change is made in the clean sandbox it will get propagated to the dirty one.
It is the responsibility of the owner of the dirty sandbox to watch for changes coming in and react to them. You can listen for the NSManagedObjectContextDidSaveNotification and check for a collision. From there it is your business logic that determines what happens next.

Related

Fix uneccessary copy of NSManagedObject

I'm sorry the title may mislead you, since I'm not so good at English. Let me describe my problem as below (You may skip to the TL;DR version at the bottom of this question).
In Coredata, I design a Product entity. In app, I download products from a server. It return JSON string, I defragment it then save to CoreData.
After sometimes has passed, I search a product from that server again, having some interaction with server. Now, I call the online product XProduct. This product may not exist in CoreData, and I also don't want to save it to CoreData since it may not belong to this system (it come from other warehouse, not my current warehouse).
Assume this XProduct has the same properties as Product, but not belong to CoreData, the developer from before has designed another Object, the XProduct, and copy everything (the code) from Product. Wow. The another difference between these two is, XProduct has some method to interact with server, like: - (void)updateStock:(NSInteger)qty;
Now, I want to upgrade the Product properties, I'll have to update the XProduct also. And I have to use these two separately, like:
id product = anArrayContainsProducts[indexPath.row];
if ([product isKindOfClass:[XProduct class]] {
// Some stuff with the xproduct
}
else {
// Probably the same display to the cell.
}
TL;DR
Basically, I want to create a scenario like this:
Get data from server.
Check existed in CoreData.
2 == true => add to array (also may update some data from server).
2 == false => create object (contains same structure as NSManagedObject from JSON dictionary => add to array.
The object created in step 4 will never exist in CoreData.
Questions
How can I create an NSManagedObject without having it add to NSMangedObjectContext and make sure the app would run fine?
If 1 is not encouragement, please suggest me a better approach to this. I really don't like to duplicate so many codes like that.
Update
I was thinking about inheritance (XProduct : Product) but it still make XProduct the subclass of NSManagedObject, so I don't think that is a good approach.
There are a couple of possibilities that might work.
One is just to create the managed objects but not insert them into a context. When you create a managed object, the context argument is allowed to be nil. For example, calling insertNewObjectForEntityForName(_:inManagedObjectContext:) with no context. That gives you an instance of the managed object that's not going to be saved. They have the same lifetime as any other object.
Another is to use a second Core Data stack for these objects, with an in-memory persistent store. If you use NSInMemoryStoreType when adding the persistent store (instead of NSSQLiteStoreType), you get a complete, working Core Data stack. Except that when you save changes, they only get saved in memory. It's not really persistent, since it disappears when the app exits, but aside from that it's exactly the same as any other Core Data stack.
I'd probably use the second approach, especially if these objects have any relationships, but either should work.

CoreData - delete an object inside a managed object

I'm using CoreData to persist a list of messages in a conversation.
Conversation is a managedObject that has an array of Messages.
In one scenario, I'm trying to delete all the messages in a conversation.
for (UQMessage * message in self.tempConversation.chatMessages){
[self.tempConversation.managedObjectContext deleteObject:message];
error = nil;
[self.tempConversation.managedObjectContext.persistentStoreCoordinator lock];
if (![self.tempConversation.managedObjectContext save:&error]) {
NSLog(#"Can't Delete! %# %#", error, [error localizedDescription]);
return;
}
[self.tempConversation.managedObjectContext.persistentStoreCoordinator unlock];
}
When I check for
self.tempConversation.chatMessages.count
Nothing changes.
Everything works perfectly well when I try to add messages, and when I delete the conversation itself. But I can't seem to delete a single message.
Is it even possible to do since I'm not trying to delete the managed object itself but another object inside it?
If not, Anyway around it?
EDIT:
Messages is an NSOrderedSet inside Conversation.
I've found this works (taken from this thread):
NSMutableOrderedSet *mutableItems = (NSMutableOrderedSet *)items.mutableCopy;
[mutableItems addObject:anItem];
items = (NSOrderedSet *)mutableItems.copy;
though I'm not sure if this is the way to go.
First, about the answer by Matt S., you are not modifying self.tempConversation so you don't have to worry about mutating the array while iterating.
On the other hand, if your problem is that self.tempConversation.chatMessages.count doesn't change. That is normal. You are deleting objects from the NSManagedObjectContext. But the array is not modified. So, the array still have the managed object BUT that managed object is deleted. Is that easy. It is a zombie managed object because it has been deleted from the MOC. Nevertheless the object has not been removed from the array. So you have a managed object with the property deleted set to YES. And it is not part of the MOC any more.
You should never, ever mutate the array you're iterating over. Per the fast enumeration docs: "It is not safe to remove, replace, or add to a mutable collection’s elements while enumerating through it. If you need to modify a collection during enumeration, you can either make a copy of the collection and enumerate using the copy or collect the information you require during the enumeration and apply the changes afterwards."
The result of mutating an array during enumeration is undefined, and my guess is core data might be just tossing up its hands and not doing anything. The reason why the mutable copy works is because you're working on a copy, not the set you're enumerating over.
I would rewrite your logic to follow the guidelines laid down in the enumeration docs, and make your changes outside of the loop.
EDIT: Additional Thoughts
Why are you locking & unlocking the persistent store? It handles that itself.
You can probably call delete safely inside the for in (but I wouldn't) and then call save outside, since save is what actually does the deletion.
More Thoughts
(Transcribing from a comment) - After thinking about this for a few days and then coming back, my guess is the reason you're not outright crashing is fast enumeration is doing a deep copy on the relationship array you're working on, because calling save on a MOC is going to increment an internal version, and then should return all existing managed objected to a faulted object to be re-fetched on next access. Really this code you've got here is actually quite dangerous from an "application health" perspective.
If you look at the documentation for core data relationships, I think you'll find the easier thing to do is just set the relationship delete rule for the relationship to "Cascade". This will remove all the messages for you when you delete the conversation. Here's the reference: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdRelationships.html#//apple_ref/doc/uid/TP40001857-SW1

Creating a copy of a PFObject

I am in a situation where I allow the user to download a PFObject and modify it locally, and they can then either cancel the changes or hit Done, which will dismiss the editing interface but NOT upload the changes to Parse yet. They need to hit Save on the previous screen to write all changes to the database at once.
The problem is once the PFObject is modified, you cannot revert it to its prior state without refetching from the database. But I cannot always refetch the data from the database every time they hit Cancel because the prior state may not be uploaded to Parse yet (and that's a bad UX making them wait to discard changes that are only stored locally).
For example, imagine the user taps to edit the PFObject, they make changes then hit Done, then tap on it again and further edit the object, then hit Cancel. In this case, the object needs to be reverted to its prior state, but that state has not been uploaded to Parse yet. So I cannot refetch the data from the database to revert changes otherwise it would overwrite the changes they made the first time.
To solve this problem, I would simply fetch the PFObject and store a copy of it. I'd call that the transient object. I would have another property that stores the real object. The user would modify the transient object, and when they hit Cancel I would simply set that to nil, if they instead hit Done I would set the real object equal to the transient object, and once they finally hit Save I would save the real object to the database. That way I can be sure changes aren't being made to the real object until the user commits the changes. The problem is, PFObject does not adopt the NSCopying protocol (not sure why), therefore I cannot create a copy of the PFObject. Any change I make to it affects the real object.
How can this be resolved, without modifying the app's design that allows control over when the data is committed and later saved? Is there a way to extend PFObject and adopt NSCopying, has it been done before?
I did consider storing the attributes of the object in a dictionary and allow the user to edit that instead, then upon commit set each of those attributes on the PFObject. The problem with this solution arises with complex structures. In this app, I allow the user to modify multiple arrays that contain multiple PFObjects. It's just infeasible to try to recreate and later merge changes with complex structures like this beyond a single simple PFObject.
I ran into this same problem. I did not make any changes directly to the PFObject, but rather, saved the updates in an NSDictionary. When the user clicks the done button, I then update the PFObject and saveInBackground. I don't think there is a "discard local changes" option for PFObject. If you don't do this, the only option is to throw out the existing PFObject and fetch again.
Regarding the NSDictionary comment, perhaps NSArray would be better. The implementation really depends on your specific program, but I'll give a quick example. The NSArray we'll call instructionArray. Imagine there are 3 sections in a tableView. Also assume that the data source for each section is an NSArray of PFObjects. Now say you want to set the age property of each PFObject in Section 2 to 35.
Add an NSArray object (corresponding to an instruction to carry out) to the instructionArray. This instruction to carry out could have the form
Section to update
Property to update
Value to update to
So the object you'll add is #[#(2),#"age",#(35)];
Given that the user is probably carrying out a finite amount of instructions, it might not be that performance heavy to loop through the instructionArray in cellForRowAtIndexPath so when a cell uses its corresponding PFObject to figure out what to display, it can loop through the instructions after and change what is displayed as if the PFObject was updated.
When the save button is touched, loop through the instructions and actually edit the PFObjects themselves.
If you need the instructions to handle specific objects rather than sections, then you just have to update the structure of the instructionArray. Maybe you could include an identifier to indicate what type of instruction it is.

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.

Deleting Core Data objects from in-memory store turns them into faults but does not erase them

I have a Core Data stack based on the NSInMemoryStoreType store. And I've noticed that deleting objects doesn't really remove them or make them nil, bur rather simply turns them into faults.
For example, (MyManagedObjectEntityClass as well as the <> identifier are placeholders):
MyManagedObjectEntityClass *o = [NSEntityDescription insertNewObjectForEntityForName:#"<MyManagedObjectEntityClass Entity Name>" inManagedObjectContext:self.localContext];
NSLog(#"\n%#", o);
[self.localContext deleteObject:o];
NSLog(#"\n%#", o);
Will log that the object is still there only that it's data is a fault.
And adding [self.localContext save:nil]; after the delete doesn't change this either.
I was hoping I could at some point test the o variable for nil, in which case I'd reload the object - but it seems I can't.
Just in case, yes, I know I could instead test o for -isFault. But thing is, extrapolate this test to an NSSet and I can't just rely on [[set anyObject] isFault] to conclude that all objects in that set have been removed (Ideally the set's count would be 0, but all objects are still there as faults).
So I'm wondering if it's possible at all or what alternate approach could I take to be able to test that objects have been deleted in a way transparent to the fact that they are managed objects.
This is not actually a Core Data issue. C (and by extension Objective-C) doesn't work like that.
The deleteObject: method takes one argument, a pointer to an object. It can change the object (like setting its isDeleted flag), or it can do other things related to the object (like deleting it from its managed object context). It cannot change the value of the pointer itself. So whatever it does or should do, C says that once it's done, the pointer that you pass in still points to the same location in memory. As a result it's actually impossible for that method to force that pointer to be nil in this language. If you want it to be nil, you have to change that yourself. (As an aside, it would have been possible to implement the method to take a pointer to pointer argument, which could modify your pointer. This would have no effect on other references such as those in arrays, though, so it would be kind of pointless).
This is why the isDeleted method is public, so that if you have a pointer to this object in some other location, you can check whether it has been deleted before attempting to use it.
If that's not convenient enough (and it often isn't), Core Data also provides NSManagedObjectContextObjectsDidChangeNotification and NSManagedObjectContextDidSaveNotification. You can use these anywhere in your app to get notified of changes to the context and respond in whatever way is appropriate (updating an array, for example). These notifications both try to help you out by providing lists of inserted, updated, and deleted objects. Use those when possible to check whether you actually need to update your references.

Resources