CoreData - delete an object inside a managed object - ios

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

Related

obtainPermanentIDsForObjects doesn't make passed in object's IDs permanent immediately

I've been wrestling with temporary core data objects within my iOS app for a fair few months now. I use UIManagedDocument which may or may not complicate things a little. The problem I have is when views are trying save URIs for objects during state encoding for restoration I hit problems whenever newly created objects have objectID's that are temporaryIDs.
Previously I'd tried to force save the UIManagedDocument with the following
NSError *saveError=nil;
BOOL bSuccess=[document.managedObjectContext save:&saveError];
[document updateChangeCount:UIDocumentChangeDone];
[document savePresentedItemChangesWithCompletionHandler:^(NSError *errorOrNil)
I thought this was helping fix the temporary objectIDs, it was definitely forcing the saving to store/disk (which shouldn't be necessary when using the more automated UIManagedDocument), but I since discovered that newly created object id's on the document.managedObjectContext were still left with temporary ObjectIDs even after this.
Last night I discovered that the following brute force addition done after the save has occurred in the savePresentedItemChangesWithCompletionHandler's completion handler block seemed to be able to fix up the temporary ObjectIDs that I was still experiencing.
[document.managedObjectContext reset];
This presumably discards the entire context and forces everything to be refreshed with the new permanent ids following the save having completed. I presume this would require at least some form of SQL db being reloaded from disk and so wasn't really an ideal solution.
Finally I discovered that there may be another solution, one that doesn't require brute force saving on the UIManagedDocument, and that's to instead do the following on any newly created NSManagedObject instead
NSError *obtainError=nil;
BOOL bObtainSuccess = [object.managedObjectContext obtainPermanentIDsForObjects:#[object] error:&obtainError];
I do think that this seems to do what's written on the tin. If I test for object's being temporary even just a second or so later then it seems to clear up and find ALL object's processed as permanent. However if I try to test whether they're permanent immediately after calling obtainPermanentIDsForObjects as follows
NSError *obtainError=nil;
BOOL bObtainSuccess = [object.managedObjectContext obtainPermanentIDsForObjects:#[object] error:&obtainError];
assert(![[object objectID] isTemporaryID]);
Then the assert fires, ie. the object still has a temporaryID even though the obtainPermanentIDsForObjects method returned YES, and left obtainError as nil.
This is all done on the main thread, without any context performBlock.. Given the configuration of UIManagedDocument though this should be correct I think.
Has anyone got any thoughts on this? For now hopefully it seems to be ok if i don't check immediately, almost like there's some threading to the operation, which does make me wonder if it should be done on some different thread...
Thanks for your time

CoreData merging inserts

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.

Remove all references to ManagedObjects belonging to a ManagedObjectContext

I'm looking to integrate iCloud with a Core-Data-managed SQLite database (only on iOS 7 and later). I've been reading Apple's guide on using Core Data with iCloud (https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/UsingCoreDataWithiCloudPG.pdf).
To quote from the guide, "Core Data posts an NSPersistentStoreCoordinatorStoresWillChangeNotification notification. In your notification handler, you reset your managed object context and drop any references to existing managed objects."
Calling -reset on the MOC to reset it isn't the problem, the problem is the part where they say all references to managed objects need to be dropped. I understand why this needs to be done (because the persistent store is changing), what I don't know is how to do it.
All my Core Data work is handled by a singleton and I had originally thought of posting a notification, and listening classes could set all their managed objects to nil. First, this doesn't sound like a particularly good way of doing it. Secondly, I have a FetchedResultsController managing a tableView, the FetchedResultsController manages it's own managed objects, therefore, as far as I know, I can't set them to nil.
I'd be really grateful for any advice on what to do here.
Thanks in advance.
The way I handle situations like this is to post two notifications in my app: just before resetting, and just after resetting.
For example, I might post MYMainContextWillResetNotification, then reset the context, then post MYMainContextDidResetNotification.
Any controller receiving the will-reset notification should release its managed objects, but also store any information it will need to recover after the reset. Usually this will be one or more NSManagedObjectID objects. In some cases, you may not need to store anything, simply performing a fetch after the reset instead.
A typical method might look like this:
- (void)mainContextWillReset:(NSNotification *)notif
{
self->noteID = note.objectID;
}
This code supposes there is a controller for a single note object. When the reset is about to take place, the note's object identifier is stored in an instance variable.
The did-reset notification method retrieves the note.
- (void)mainContextDidReset:(NSNotification *)notif
{
note = [context existingObjectWithID:noteID error:NULL];
[self refreshViews];
}
This code uses existingObjectWithID:error:, but you could equally do a fetch.
With an NSFetchedResultsController, you would need to call performFetch: in the did-reset method, to refresh the objects.

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