Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Restkit-loaded nested Core Data entities cause NSObjectInaccessibleException

I'm using RestKit to grab objects from my RoR service and using CoreData to persist some of the objects (more static-type lookup table objects). TasteTag is one of those persisted objects:

#ifdef RESTKIT_GENERATE_SEED_DB
    NSString *seedDatabaseName = nil;
    NSString *databaseName = RKDefaultSeedDatabaseFileName;
#else
    NSString *seedDatabaseName = RKDefaultSeedDatabaseFileName;
    NSString *databaseName = @"Model.sqlite";
#endif

RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:kServerURL];  
manager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:databaseName usingSeedDatabaseName:seedDatabaseName managedObjectModel:nil delegate:self];

.. lots of fun object mapping ..

 RKManagedObjectMapping* tasteTagMapping = [RKManagedObjectMapping mappingForClass:[TasteTag class]];
[tasteTagMapping mapKeyPath:@"id" toAttribute:@"tasteTagID"];
[tasteTagMapping mapKeyPath:@"name" toAttribute:@"name"];
tasteTagMapping.primaryKeyAttribute = @"tasteTagID";
[[RKObjectManager sharedManager].mappingProvider setMapping:tasteTagMapping forKeyPath:@"taste_tags"]; 
[[RKObjectManager sharedManager].mappingProvider addObjectMapping:tasteTagMapping];

.. some more mapping ..

I have the data coming back from the RoR server and it's getting mapped to objects as expected. The Core Data entity also seems mapped fine after RestKit gets the request back:

"<TasteTag: 0x6e87170> (entity: TasteTag; id: 0x6e85d60 <x-coredata://03E4A20A-21F2-4A2D-92B4-C4424893D559/TasteTag/p5> ; data: <fault>)"

The issue is when I try to access properties on the objects the fault can't seem to be fire. At first I was just calling the properties, which always came back as nil (even though that should fire the fault):

for (TasteTag *tag in self.vintage.tasteTags) {
    [tagNames addObject:tag.name]; //get error of trying to add nil to array   
}

After looking into manually triggering faults (http://www.mlsite.net/blog/?p=518) I tried calling [tag willAccessValueForKey:nil] which results in:

Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x6e7b060 <x-coredata://03E4A20A-21F2-4A2D-92B4-C4424893D559/TasteTag/p5>''

Looking up the entity in the .sqlite based on the key (TasteTag/p5) does show it mapped to the one I'd expect.

Other posts relating to RestKit recommend disabling the object cache (which I'm not using) since this is usually caused by an entity being deleted. But at this stage I'm just reading, not deleting, and I have no cache in place.

If I just call [TasteTag allObjects] I'm able to get all the objects back fine and they load without issue. It's just in the case when they are faulted it seems.

like image 287
Parrots Avatar asked Jan 12 '12 16:01

Parrots


2 Answers

I found a solution that worked for me (I am unsure of how applicable it will be to your situation, but I'm adding it as an answer since it solved this (or very similar) issue for me):

A couple days back, I ran the RKTwitterCoreData example and noticed it worked perfectly while mine, with very simple code at this point and doing nearly the same thing, didn't. I got a lot of unfulfilled faults. So I decided to modify all of my code dealing with RestKit to reflect how the RKTwitterCoreData example does it.

I'll split this into chunks to try and help you follow my line of thinking at the time (since I don't think our problems are identical).

My Original Implementation Assumption

Since RestKit can back objects to Core Data, I assumed that those managed objects could be used interchangeably. For example, I could use the objects from Core Data in the exact same way as the ones retrieved from a remote web service. I could even merge them together to get all the data.

I Was Wrong

I noticed that RKTwitterCoreData's code did not flow this way in the least. A decent chunk of my code matched up with theirs, but the largest difference was that they didn't treat these objects as interchangeable. In fact, they never used the objects they got from remote data stores. Instead, they just let that "fall through the cracks". I can only assume that means they're added to Core Data's data store since it works for them and, now, for me.

Details

My app worked after modifying my code to utilize this flow. I can only then surmise that the unfulfillable faults we are seeing are related to using the Core Data backed objects that we get back from the web service. If instead you just ignore those and then do a fetch, you will get everything back (including the most recent request) and you shouldn't get any unfulfillable faults.

To elaborate, if you look at RKTwitterViewController you will notice that lines 45-61 handle loading of objects:

- (void)loadObjectsFromDataStore {
    [_statuses release];
    NSFetchRequest* request = [RKTStatus fetchRequest];
    NSSortDescriptor* descriptor = [NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:NO];
    [request setSortDescriptors:[NSArray arrayWithObject:descriptor]];
    _statuses = [[RKTStatus objectsWithFetchRequest:request] retain];
}

- (void)loadData {
    // Load the object model via RestKit    
    RKObjectManager* objectManager = [RKObjectManager sharedManager];
    [objectManager loadObjectsAtResourcePath:@"/status/user_timeline/RestKit" delegate:self block:^(RKObjectLoader* loader) {
        // Twitter returns statuses as a naked array in JSON, so we instruct the loader
        // to user the appropriate object mapping
        loader.objectMapping = [objectManager.mappingProvider objectMappingForClass:[RKTStatus class]];
    }];
}

Everything looks normal (at least compared to how I was doing this loading initially). But take a look at the objectLoader:didLoadObjects: delegate method:

- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
    [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:@"LastUpdatedAt"];
    [[NSUserDefaults standardUserDefaults] synchronize];
    NSLog(@"Loaded statuses: %@", objects);
    [self loadObjectsFromDataStore];
    [_tableView reloadData];
}

The sample doesn't even touch the objects parameter! (Aside from the NSLog of course...)

Conclusion/tl;dr

Don't use the managed objects you get back in objectLoader:didLoadObjects: as if they are fully backed by Core Data. Instead, ignore them and re-fetch from Core Data. All objects, including the ones from the last request are there. Otherwise, you will get unfulfillable faults (at least I did).

like image 172
Ryan Wersal Avatar answered Oct 02 '22 23:10

Ryan Wersal


Documenting my fix (read:hack) per Ryan's suggestion.

The error seems to be in how RestKit assumed you'll be using the objects returned from their objectLoader:didLoadObjects: method. They seem to assume it will all be Core Data backed (and follow the flow similar to what Ryan talked about - let it sync to Core Data, then re-query) or that you'll be using all non Core Data backed objects and just keep those results around.

In my case I had a mix - a root array of non Core Data backed objects which each then contained an array of Core Data backed entities. The top-level objects are ones I don't mind querying the server for and have no reason to persist locally beyond the view they're shown in. It seems once objectLoader:didLoadObjects: is complete the managed object context backing the Core Data entities within the objects param is disposed of (under the assumption you'll be re-querying for them), causing any future calls to the entities to result in being treated as faults, even though you can't trigger the fault and load the data (results in NSObjectInaccessibleException).

I got around it with an ugly hack - within objectLoader:didLoadObjects: I access one of the Core Data entity's managed object context and copy it to a property within the view (self.context = [tag managedObjectContext];). This prevents the context it being released after objectLoader:didLoadObjects: is complete, allowing me to access the entities without issue later in the view.

Another solution would be to manually re-query for each entity using a new context and copy that back to the stored return objects. One could do this when one goes to display them, or possibly some post-processing in objectLoader:didLoadObjects:, using a new context. The entity ID is still around on the faulted object so one could use that to re-query without issue even after the original RestKit context disappears. But it seems silly to have to re-query for every entity in the object graph like that.

like image 28
Parrots Avatar answered Oct 02 '22 23:10

Parrots