Pointers to NSManagedObject after object delete / context save? - ios

Can someone please explain what happens to the pointers to the NSManagedObjects after the object is deleted and the context is saved? How should I set them up so that they get set to nil automatically?

Well, it's quite simple.
[managedObjectContext deleteObject:managedObject];
[managedObjectContext save:error];
managedObject = nil;
If you are afraid of memory leaks when deleting lots of objects, just use fast enumeration. This is pretty much guaranteed to clean up behind itself:
for (NSManagedObject *obj in fetchedObjects) {
[managedObjectContext deleteObject:obj];
}
[managedObjectContext save:error];

After you delete an object, the isDeleted property will be true. After saving the context, the isDeleted will be false if you still have a reference to the managed object.
You can safely make weak references to managed objects. The weak reference will nil out automatically for you under ARC when Core Data is done with them.
Here are the three relevant paragraphs from the Core Data Programming Guide:
Core Data “owns” the life-cycle of managed objects. With faulting and
undo, you cannot make the same assumptions about the life-cycle of a
managed object as you would of a standard Cocoa object—managed objects
can be instantiated, destroyed, and resurrected by the framework as it
requires.
When a managed object is created, it is initialized with the default
values given for its entity in the managed object model. In many cases
the default values set in the model may be sufficient. Sometimes,
however, you may wish to perform additional initialization—perhaps
using dynamic values (such as the current date and time) that cannot
be represented in the model.
You should typically not override dealloc to clear transient
properties and other variables. Instead, you should override
didTurnIntoFault. didTurnIntoFault is invoked automatically by Core
Data when an object is turned into a fault and immediately prior to
actual deallocation. You might turn a managed object into a fault
specifically to reduce memory overhead (see “Reducing Memory
Overhead”), so it is important to ensure that you properly perform
clean-up operations in didTurnIntoFault.

I check for (managedObject.managedContext == nil).
If it is nil then it was deleted.
Although this is not guaranteed by Apple Documentation, it seems to be working fine with me.
If you use it in different contexts or it is not saved, it will not work.
See How can I tell whether an `NSManagedObject` has been deleted? for details.

Related

What is the best practice to pass object references between two queues with CoreData?

I am facing a decision to be made for an applications architecture design.
The Application uses CoreData to persist user information, the same information is also stored on a remote server accessible by a REST-Interface. When the Application starts I provide the cached information from CoreData to be displayed, while I fetch updates from the server. The fetched information is persisted automatically as well.
All of these tasks are performed in background queues as to not block the main thread. I am keeping a strong reference to my persistenContainer and my NSManagedObject called User.
#property (nonatomic, retain, readwrite) User *fetchedLoggedInUser;
As I said the User is populated performing a fetch request via
[_coreDataManager.persistentContainer performBackgroundTask:^(NSManagedObjectContext * _Nonnull context) {
(...)
NSArray <User*>*fetchedUsers = [context executeFetchRequest:fetchLocalUserRequest error:&fetchError];
(...)
self.fetchedLoggedInUser = fetchedUsers.firstObject;
//load updates from server
Api.update(){
//update the self.fetchedLoggedInUser properties with data from the server
(...)
//persist the updated data
if (context.hasChanges) {
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
NSError *saveError = nil;
BOOL saveSucceeded = [context save:&saveError];
if (saveSucceeded) {
//notify the App about the updates
//**here is the problem**
}
};
}];
So the obvious thing about this is, that after performing the backgroundTask, my self.fetchedLoggedInUser is not in memory anymore, because of its weak reference to the NSManagedObjectContext provided by the performBackgroundTask() of my PersistentContainer.
Therefore, if I try to access the information from another Model, the values are nil.
What would be the best practice to keep the fetched ManagedObject in Memory and not have to fetch it again, every time I want to access its values?
A) In the Documentation, Apple suggests using the objectID of an ManagedObject, to pass objects between queues
Passing References Between Queues
NSManagedObject instances are not intended to be passed between
queues. Doing so can result in corruption of the data and termination
of the application. When it is necessary to hand off a managed object
reference from one queue to another, it must be done through
NSManagedObjectID instances.
You retrieve the managed object ID of a managed object by calling the
objectID method on the NSManagedObject instance.
The perfectly working code for that situation would be to replace the if(saveSucceeded) Check with this Code:
if (saveSucceeded) {
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = nil;
self.fetchedLoggedInUser = [self.coreDataManager.persistentContainer.viewContext existingObjectWithID:_fetchedLoggedInUser.objectID error:&error];
(...)
//notify the App about the updates
});
}
But I think this may not be the best solution here, as this needs to access the mainContext (in this case the persistentContainer's viewContext) on the mainQueue. This is likely contradictory to what I am trying to do here (performing on the background, to achieve best performance).
My other options (well, these, that I came up with) would be
B) to either store the user information in a Singleton and update it every time the information is fetched from and saved to CoreData. In this scenario I wouldn't need to worry about keeping the NSManagedObject context alive. I could perform any updates on a private background context provided by my persistentContainer's performBackgroundTask and whenever I'd need to persist new / edited user information I could refetch the NSManagedObject from the database, set the properties, save my context and then update the Singleton. I don't know if this is elegant though.
C) edit the getter Method of my self.fetchedLoggedInUser to contain a fetch request and fetch the needed information (this is probably the worst, because of the overhead when accessing the database) and I am not even sure if this would work at all.
I hope that one of these solutions is actually best practice, but I'd like to hear your suggestions why/how or why/how not to handle the passing of the information.
TL:DR; Whats the best practice to keep user information available throughout the whole app, when loading and storing new information is mostly done from backgroundQueues?
PS: I don't want to fetch the information every time I need to access it in one of my ViewControllers, I want to store the data on a central knot, so that it is accessible from every ViewController with ease. Currently the self.fetchedLoggedInUser is a property of a singleton used throughout the application. I find that this saves a lot of redundant code, using the Singleton makes loading and storing the information clearer and reduces the access count to the database. If this is considered bad practice I'd be happy to discuss about that with you.
Use a NSFetchedResultsController - they are very efficient and you can use them even for one object. A FetchedResultsController does a fetch once and then monitors core data for changes. When it changes you have a callback that it has changed. It also works perfectly for ANY core-data setup. So long as the changes are propagated to the main context (either with newBackgroundContext or performBackgroundTask or child contexts or whatever) the fetchedResultsController will update. So you are free to change your core-data stack without changes your monitoring code.
In general I don't like keeping pointers to ManagedObjects. If the entry is deleted from database then the managedObject will crash when you try to access it. A fetchedResultsController is always safe to read fetchedObjects as it tracks deletions for you.
Obviously attach the NSFetchedResultsController to the viewContext and only read it from the main thread.
I came up with a very elegant solution in my opinion.
From the beginning I was using a Singleton called sharedCoreDataManager, I added a property backgroundContext that is initialized like so
self.backgroundContext = _persistentContainer.newBackgroundContext;
_backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
_backgroundContext.retainsRegisteredObjects = YES;
and is retained by sharedCoreDataManager. I am using this context to perform any tasks. Through calling _backgroundContext.retainsRegisteredObjects my NSManagedObject is retained by the backgroundContext, which is itself (like I said) retained by my Singleton sharedCoreDataManager.
I think this is an elegant solution as I can access the ManagedObject threadsafe from the background anytime. I also won't need any extra class that holds the user information on top. And on top of that I can easily edit the user information at anytime and then call save() on my backgroundContext if needed.
Maybe I am going to add it as a child to my viewContext in the future, I'll evaluate the performance and eventually update this answer.
You are still welcome to propose a better solution or discuss this topic.

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

Orphaned objects in iOS CoreData

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.

About the benefit of objectWithID:

The doc says:
If the object is not registered in the context, it may be fetched or
returned as a fault. This method always returns an object. The data in
the persistent store represented by objectID is assumed to exist—if it
does not, the returned object throws an exception when you access any
property (that is, when the fault is fired). The benefit of this
behavior is that it allows you to create and use faults, then create
the underlying data later or in a separate context.
I'm thinking about the last sentence:
The benefit of this behavior is that it allows you to create and use faults, then create the underlying data later or in a separate context.
Does it mean I can use objectWithID: with an arbitrary ID to get a fault handle of an non-existing object first then later create the object with ID? But how can I assign an arbitrary ID to the new object?
In general, Yes you can get a handle to a non existing item an later create that item.
But, since you don't know what ID will be assigned to the item these is not very useful in that case.
You could use obtainPermanentIDsForObjects:error: to obtain the object final ID, but, this is a trip to the store, and will have a performance penalty.
You can use objectWithID: to "warm up" the coordinator cache. in this manner you may fetch objects in the background, and use this method in another context then access these items without hitting the store (much better performance).
Since every NSManagedObjectID must initially come from a fulfilled NSManagedObject and there is no way to create one from scratch, the only possible way to "create the underlying data later" is meaningless, as follows:
NSManagedObjectID *objID = object.objectID;
[moc deleteObject:object];
…
object = [moc objectWithID:objID]; // Deleted so non-existing
[moc insertObject:object]; // Kinda of resurrecting the deleted object, but not really since the data are gone only ID is left. So it is creating a new object with the old ID. But what's the point?
// Fill data into object
…
[moc save:NULL];
If you use -objectWithID:, it will return a fault if the object is not already registered in the managed object context (ie. only if the object hasn't already been fetched and hasn't been faulted in). In the case that it does return a fault, you do not need to do anything to "create the object". Simply accessing the attributes of the object will automatically fire the fault and let you access its data. There is no additional work needed on your part to create additional objects.

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