NSManagedObject provides access to its NSManagedObjectContext, but does it retain it?
According to "Passing Around a NSManagedObjectContext on iOS" by Marcus Zarra, "The NSManagedObject retains a reference to its NSManagedObjectContext internally and we can access it."
How does Zarra know this and is he correct?
I'm asking because I want to know if the NSManagedObjectContext will be dealloc'ed in the tearDown method below. (I'm using CocoaPlant.)
#import <SenTestingKit/SenTestingKit.h>
#import <CocoaPlant/CocoaPlant.h>
#import "AccountUser.h"
#interface AccountUserTests : SenTestCase {
AccountUser *accountUser;
}
#end
#implementation AccountUserTests
- (void)setUp {
accountUser = [AccountUser insertIntoManagedObjectContext:
[NSManagedObjectContext contextWithStoreType:NSInMemoryStoreType error:NULL]];
}
- (void)tearDown {
[accountUser delete];
}
- (void)testFetchWithLinkedAccountUserID {
// Tests go here...
}
#end
NSManagedObject DOES NOT hold strong reference to its NSManagedObjectContext. I've checked that on a test project.
Therefore, you should keep strong reference to NSManagedObjectContext as long as you use its objects.
Wow, over two years old and no accepted answer here :)
When I wrote that post I did indeed mean it keeps a reference to its associated NSManagedObjectContext. If a NSManagedObject retained the NSManagedObjectContext then it would most likely run into problems.
In either case, whether the MOC is retained by the MO is irrelevant to your application design. If you need the MOC to stay around then you need to retain it (now referred to as a strong reference) or it will go away. What the frameworks do internally is not our problem. We just need to make sure we balance our retains and releases.
Matt,
I think Marcus may have miswrote that a NSManagedObject retains its context. Every NSManagedObject maintains a link to the context. Unless individual objects have an internal retain cycle or are retained outside of their context, then, in my experience, they are all released when the context is released. If they retained the context, then this would almost certainly not be the case.
The above said, you can easily write code to test Marcus' claim. Override -dealloc and log when it is called.
IMO, it is a best practice to retain your context until you are done with it. Depending on an undocumented behavior is probably not wise.
Andrew
Related
I have a core data database. I'm performing a custom migration. I have a subclass of NSEntityMigrationPolicy. My policy's migration method buildValueFromSectionFieldManagedObject: is being called by a mapping rule : FUNCTION($entityPolicy, "buildValueFromSectionFieldManagedObject:" , $source).
This part is actually working.
However, the implementation of buildValueFromSectionFieldManagedObject: uses methods in the custom entity NSManagedObject subclass of the $source, which is Choice.
The methods of Choice do not seem to be available to the migration function, and instead it gets just a vanilla NSManagedObject.
When I try to use Choice methods, I get an exception. If I po the choice in the debugger, I get something like this:
<NSManagedObject: 0x600000281860> (entity: Choice; id: 0xd00000000038001a ; data: )
Whereas, out of a migration I would usually see something like this:
<Choice: 0x60800028bdb0> (entity: Choice; id: 0x6080002225a0 ; data: {
Is this just how it is, or is there some way that I can use the entity objects during migration?
Possibly relevant – this particular entity, Choice, is removed during this migration. It does not exist in the target managed object model, but does exist in the source managed object model. However, I don't think this is the case as other entity classes that are in the target model are also unavailable as that class during migration – they have class NSManagedObject and their entity methods are not available.
That is correct, you only have access to basic NSManagedObjects during migration.
Three-Stage Migration
The migration process itself is in three stages. It uses a copy of the source and destination models in which the validation rules are disabled and the class of all entities is changed to NSManagedObject.
From: Core Data Model Versioning and Data Migration Guide
Dave's answer clarified that during a migration, core data object are only available as NSManagedObject instances. You don't get to use their Entity classes.
Worse, if you're using a tool like mogenerator, then any handy logic that you've extended the entity classes with is inaccessible.
Poor solutions
Working with an NSManagedObject directly feels dangerous to me. Using [managedObject valueForKey:#"someKey"] is verbose, but worse, there's no compiler checking that you've got your key name correct, so you might be asking for something managedObject doesn't have. There's also no compiler checking of the returned type either – it could be anything that you can put in to a managed object.
Slightly better is [managedObject valueForKey: NSStringFromSelector(#selector(someKey))] is safer, but horribly verbose and awkward, and still isn't that safe – lots of things might implement the method someKey.
You might also declare your keys as literals:
NSString *const someKey = #"someKey";
[managedObject valueForKey: someKey];
Again – this is slightly safer, less error prone and verbose, but you've got no guarantee still that this managed object has someKey.
None of these approaches will give us access to custom logic, either.
Better solution
What I did instead of this was to define a protocol for the properties that I know my managed object has. Here's are examples for entities Choice and ChoiceType.
#protocol ChoiceProtocol
- (NSSet<id <ChoiceTypeProtocol> > *)choiceTypes;
- (NSNumber *)selected;
- (NSNumber *)order;
#end
#protocol ChoiceTypeProtocol
- (NSNumber *)selected;
- (NSString *)name;
- (NSString *)textCustom;
- (NSNumber *)order;
#end
Now in my migration code, instead of having a custom migration function similar to:
- (NSString *)migratedRepresentationOfChoice:(NSManagedObject *)choice;
I have:
- (NSString *)migratedRepresentationOfChoice:(id <ChoiceProtocol>)choice;
In the body of this function I can use choice exactly as I would any regular object. I get code completion as I type, I get the right syntax highlighting, the compiler will complain if I call a non existent method.
I also get return type checking, so the compiler will complain if I use the NSNumber property selected as an NSString or NSSet. And it'll also be helpful and suggest NSNumber methods as completions while I type.
How about the logic?
This approach doesn't provide the logic you've added to entity classes.
In Swift you might be able to use a protocol extension to achieve that.
I was retiring these entities (hence the migration), so I moved the entity logic functions I needed in to a helper functions of my custom migration. As an example choice.orderedChoiceTypes becomes [self choiceOrderedChoiceTypes:choice].
In future I will probably avoid ever adding logic to NSManagedObject entities. I think it's probably a better plan to put any such logic in domain objects that you build from your managed objects. Further, I will probably avoid defining entity classes and instead only access NSManagedObject instances through a protocol as during the migration. It seems clean, simple, removes magic, and has benefits for testing – not just for migrations.
Before I send my app to the App Store I like to check it for memory leaks and other fishy stuff with instruments. There is one Core Data issue that I can't seem to solve, so I've decided to create a small test app to illustrate the problem.
What's the problem?
When I save an entity in a (child) NSManagedObjectContext it is propagated to its parent NSManagedObjectContext. During this process Core Data creates internal instances of _NSObjectID and NSTemporaryObjectID. For some reason these instances are left behind and the only way to get rid of them is to reset the parent NSManagedObjectContext.
My app is of course a lot more complex than this little test app and resetting the NSManagedObjectContext isn't an option for me.
Test app
The test app is a standard iOS app based on the single view template with the CoreData option checked. I've used objective-c to keep it similar to my production app.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Initialize the Core Data stack
self.persistentStoreCoordinator = [self persistentStoreCoordinator];
// Create the a private context
self.rootContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.rootContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
// Create a child context
self.childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.childContext.parentContext = self.rootContext;
// Create a person
[self.childContext performBlockAndWait:^{
Person *person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.childContext];
person.name = #"John Smith";
person.age = 30;
// Save the person
[self.childContext save:nil];
// Save the root context
[self.rootContext performBlockAndWait:^{
[self.rootContext save:nil];
}];
}];
return YES;
}
When you run the code above with instruments and the allocations instrument you can see that Core Data leaves some stuff behind.
You can find the full project here: https://github.com/Zyphrax/CoreDataLeak
Things I've tried
I've tried things like [context refreshObject:... mergeChanges:YES], adding #autoreleasepool and/or [context processPendingChanges] inside the blocks, it all doesn't help. The only way to get it clean is to do a [context reset] (sledgehammer approach).
It's hard to find other people reporting this problem.
This blog post seems similar:
http://finalize.com/2013/01/04/core-data-issues-with-memory-allocation/
I hope you guys can help me with this.
Here is what I see, which is very similar to yours...
However, I don't know that I would be concerned, unless you see lots of these, and they never go away. I assume the internals of Core Data (including the row cache has) some sort of object caching going on.
On the other hand, my Core Data usage has changed a bit over the past year or two.
Unless it is a very simple app, I almost never create new objects in a child context. I will fetch and modify them, but if I end up creating a new object, I make sure that is done in a sibling context.
However, if you modify your code slightly, by adding this line (with your appropriate error handling - it returns BOOL) before the initial save...
NSArray *inserted = self.childContext.insertedObjects.allObjects;
[self.childContext obtainPermanentIDsForObjects:inserted error:&error];
you should get something like this instruments report, which shows all objects created as being transient...
Thus, I don't necessarily think it is a permanent leak, because once I force the context to convert to a permanent ID, the objects go away. However, who knows how long they keep those object ID objects cached.
In general, when I create objects in a context that contains a hierarchy, I will always obtain permanent IDs first (for many reasons). However, as I said earlier, I usually create new objects in a context that is directly created to the persistent store (because I have had to deal with other issues related to hierarchies temporary object IDs, especially when using multiple non related contexts).
I have an NSManagedObject subclass with a transient property which is basically a reformatting of one of the persistent to-many relationships. I do this by observing the relationship with KVO and registering the observer in -awakeFromFetch, -awakeFromInsert, etc.
This all works fine, however if I pass the object between threads using the object's objectID and -objectWithID: technique there is no life-cycle method into which I can hook generation of the transient property. None of the life-cycle methods are triggered, in fact, since accessing the object directly using the id isn't considered a fetch, it seems.
There are ways around this, but it would be nice to use a life-cycle based technique. Am I perhaps missing something? Is there another standard method I could be using?
Thanks
Edit: Demonstration project
https://mega.co.nz/#!UsNBTZ7S!UU1qaFuc4W6Z2EYey-9AiMyfM8203Zfrm1lfpG5QITU
When you have a NSManagedObject instance on one thread with a context and then retrieve it from a different thread and different context the -awakeFromFetch or -awakeFromInsert fires.
Are you using the contexts properly so that you are retrieving a new instance?
Have you looked at the pointers in the debugger to make sure you are talking to a new instance of the NSManagedObject?
In my experience those lifecycle methods fire per context.
Ok, so to answer my own question, the issue is caused because objectWithID: always returns an object, even though the object isn't registered in the receiver managed object context. Seems in some circumstances objectRegisteredForID: is more informative. In any case, the conclusion is that the life-cycle methods do fire, but to take care with objectWithID: since it can result in an inconsistent object.
I encountered this same issue, and solved it by creating an extension on NSManagedObjectContext that goes through the normal fetch pathway, therefore triggering all of the expected lifecycle methods:
extension NSManagedObjectContext {
func fetchObject<T: NSManagedObject>(with objectID: NSManagedObjectID) -> T? {
let request = T.fetchRequest()
request.predicate = NSPredicate(format: "SELF = %#", objectID)
guard let result = try? fetch(request) else {
return nil
}
return result.first as? T
}
}
Is there an easier way to do something like the following in Core Data:
Entry *entry = [[Entry alloc] init];
entry.name = #"An Entry";
[entry save];
I realize you don't have to allocate an NSManagedObject, have to insert directly into the context like the following:
Entry *entry = [NSEntityDescription insertNewObjectForEntityForName:#"Entry"
inManagedObjectContext:[self managedObjectContext]];
But this is a lot of code. Also I would like to save by just messaging rather than have to save the entire context:
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
Could I put these in an NSManagedObject abstract class and have my managed objects extend that abstract class? Basically, I'm just trying to encapsulate more in my models and write less code in my controllers. Any help appreciated.
You can define your own subclass of NSManagedObject, and set all of your entities to use it.
The subclass can have whatever initialiser/save patterns you define, so long as it calls the proper parent class's initialiser.
You could have a +entity method, which might link to a statically defined context (this will restrict you to a single managed object context of course, but that's not always bad as long as you can also call the more primitive initialisers when you need a second context).
You might even have a +entityWithName: method.
As for saving the context, once again you can always define a subclass and add a simple -save method which saves the context and throws an exception if the save fails. You may choose to do this with a category extending the NSManagedObject class, instead of a subclass.
Note it is impossible to save just the change you made to that one entry object. You can only save all changes to an entire managed object context. If you need to save a single record, then you need to create a temporary managed object context, make a single change in it, then save the temporary context, and then sync the temporary context change over to all other managed object context's that currently exist in the app (there is an API to do make this complicated process relatively easy).
I don't like the code you posted to save the context, for several reasons:
Don't define a managedObjectContext variable that just points to self.managedObjectContext. In almost all situations that's an extra line of code for no benefit. At best you're making your code hard to read, at worst you might be introducing bugs.
Why are you checking if it is nil? Usually you should design your code so that it cannot ever be nil. Do the nil check in the constructor of your object, and if it's nil that is a critical failure. Your whole app is completely useless for the user, and you should make it clear to the user that they can't use the app by doing something drastic, such as a crash (an error alert first would be nice, but I wouldn't bother. I'd just throw an exception).
Why are you doing a check for hasChanges? I cannot think of many situations where you would need to do this check. Perhaps your app is allowing a user to make many changes, and then saving them several minutes later? This is bad. The context should be changed milliseconds after a group of changes are made, or else you're risking data loss. Your app could crash, the phone could run out of battery, or the user might receive a phone call and your app is consuming enough RAM that the OS will terminate it instantly in order to present the "incoming call" screen. You shouldn't need to check for hasChanges because you always perform a save operation immediately after making some changes.
As I kind of mentioned before, if the save fails you should present an error to the user then throw an exception. Avoid using NSLog() in deployment code, it's really only useful for development and beta builds.
Check out
NSManagedObject+ActiveRecord.h
Inside Restkit : http://restkit.org/
It is based on :
https://github.com/magicalpanda/MagicalRecord
I am using in RestKit app, but you can adopt it quite easily.
Good luck
I am developing an application in cocoa. I am facing a critical problem.
I am deleting entries of an object named "Directory" in Core Data using the following code:
NSEnumerator *tempDirectories = [[folderArrayController arrangedObjects] objectEnumerator];
id tempDirectory;
while (tempDirectory = [tempDirectories nextObject]){
[managedObjectContext deleteObject:tempDirectory];
}
But sometimes an exception like "Core Data could not fulfill a fault.." occurs while trying to save after deletion. I am using the code [managedObjectContext save];
I am new in Core Data... Looking forward to a solution.
This is an old question, I have struggled resolving this for some time now. So, thought it would be best to document it.
As Weichsel above mentioned, the Apple documentation rightly points out reason for this exception. But it is a hectic job to identify the module due to which the NSManagedObject subclass' object is being retained (if 1st cited reason in the documentation is the root cause of the problem).
So, I started out by identifying the parts of my code which was retaining the NSManagedObject, instead I retained the NSManagedObjectID and create the managed object out of it whenever needed. The discussion in similar lines can be found in Restkit documentation:
https://github.com/RestKit/RestKit/commit/170060549f44ee5a822ac3e93668dad3b396dc39
https://github.com/RestKit/RestKit/issues/611#issuecomment-4858605
Updated my setter and getter so that the interface with rest of the modules remain same while internally we now depend upon NSManagedObjectID and avoid retaining of NSManageObject:
-(CSTaskAbstract*)task
{
CSTaskAbstract *theTask = nil;
if (self.taskObjectID)
{
NSManagedObjectContext *moc = [(CSAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
// https://github.com/RestKit/RestKit/commit/170060549f44ee5a822ac3e93668dad3b396dc39 &
// https://github.com/RestKit/RestKit/issues/611#issuecomment-4858605
NSError *theError = nil;
NSManagedObject *theObject = [moc existingObjectWithID:self.taskObjectID
error:&theError];
if ([theObject isKindOfClass:[CSTaskAbstract class]])
{
theTask = (CSTaskAbstract*)theObject;
}
}
return theTask;
}
-(void)setTask:(CSTaskAbstract *)inTask
{
if (inTask!=self.task)
{
// Consequences of retaining a MO when it is detached from its MOC
[self setTaskObjectID:[inTask objectID]];
}
}
The above is the first half of the problem solved. We need to find out dependency in suspicious parts of our app and eliminate.
There was some other problem too, instruments -> allocations is a good source to find out which modules are actually retaining the managed objects, the exception object would have details about which managed object is creating the problem, filter results for that object as shown below:
We were performing KVO on a managed object. KVO retains the observed managed object and hence the exception is thrown and it's back trace would not be from within our project. These are very hard to debug but guess work and tracking the object's allocation and retain-release cycle will surely help. I removed the KVO observation part and it all started working.