I am struggling to map the foreign key relationships returned by my JSON API with RestKit.
Specifically, I use Loopback to generate an API with Entities like Team
and User
. There are two Endpoints by default that return the following JSON:
/Teams/
[
{
"title": "Team Title",
"id": 1,
}
]
/Users/
[
{
"name": "Alice",
"id": 1,
"teamId": 1,
},
{
"name": "Bob",
"id": 2,
"teamId": 1,
}, ...
]
Now this is a pretty simple API, right? It should be easy to map with RestKit, but even after reading all those other questions about the topic (e.g. Seeking recommendations for best RestKit/CoreData mapping and JSON structure for shallow routes, Foreign key relationship mapping with RestKit, Restkit: GET remote linked object when foreign key refers to missing local object in Core Data) I just can't figure out how to load the entire object graph into core data.
If it makes things easier, I can alter the API as well, of course. For obvious reasons, I don't want to embed the entire Team
object in every User
but use a foreign key instead. Also, I want to keep the Endpoints separate, mostly because this is the way Loopback generates the API by default.
So what I tried is calling both Endpoints (in an order that should not matter) with the following mapping:
Team
Mapping
RKEntityMapping *teamMapping = [RKEntityMapping mappingForEntityForName:@"Team" inManagedObjectStore:objectManager.managedObjectStore];
[teamMapping addAttributeMappingsFromArray:@[ @"title", @"id" ]];
teamMapping.identificationAttributes = @[ @"id" ];
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:teamMapping method:RKRequestMethodAny pathPattern:@"Teams" keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
User
Mapping
RKEntityMapping *userMapping = [RKEntityMapping mappingForEntityForName:@"User" inManagedObjectStore:objectManager.managedObjectStore];
[userMapping addAttributeMappingsFromArray:@[ @"name", @"id", @"teamId" ]];
userMapping.identificationAttributes = @[ @"id" ];
[userMapping addConnectionForRelationship:@"team" connectedBy:@{ @"teamId": @"id" }];
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:userMapping method:RKRequestMethodGET pathPattern:@"Users" keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
As suggested in other questions (like the ones linked above), I use an RKConnectionDescription
to establish the relationship via a transient teamId
property. The mapping works fine with a JSON payload that includes both Team
and User
objects as arrays (with a keyPath instead of a pathPattern in the response descriptor), but fails with separate Endpoints that I call independantly from each other. In that case, the object the connection relationship refers to may not have been created yet. So to solve this, a Stub Object should be created with only the id
property populated in that situation.
How can I achieve this with a RestKit mapping?
Edit: Would it help if I changed the JSON structure from "teamId": 1
to "team": { "id": 1 }
and add another response descriptor with the keyPath team
? Not sure if that's easy to do with Loopback.
What's the best way to do this?
Edit 2: So I added the following stub mapping:
RKEntityMapping *teamStubMapping = [RKEntityMapping mappingForEntityForName:@"Team inManagedObjectStore:objectManager.managedObjectStore];
[teamStubMapping addAttributeMappingsFromDictionary:@{@"teamId": @"id"}];
teamStubMapping.identificationAttributes = @[ @"id" ];
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:teamStubMapping method:RKRequestMethodAny pathPattern:@"Users" keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
When I turn on trace logging, I don't see this mapping executed, though. What am I doing wrong? Is this mapping skipped because there is another one already matching the same pattern? When I comment out the User
mapping, it does its job.
Edit 3: So RestKit does actually use only the last response descriptor added to the object manager of those matching a given path and with equal key paths, as discussed here on GitHub. Thanks to the answer below, using a nil
key path mapping solves the issue:
RKEntityMapping *teamStubMapping = [RKEntityMapping mappingForEntityForName:@"Team inManagedObjectStore:objectManager.managedObjectStore];
[teamStubMapping addPropertyMapping:[RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@"id"]];
teamStubMapping.identificationAttributes = @[ @"id" ];
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:teamStubMapping method:RKRequestMethodAny pathPattern:@"Users" keyPath:@"teamId" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];