I’m trying to map users and groups to CoreData objects through RestKit, maintaining the relationship between the two.
The JSON for the users is something like
{"users":
[{"fullname": "John Doe", "_id": "1"}
...
]
}
and the JSON for the groups is something like
{"groups":
[{"name": "John's group", "_id": "a",
"members": ["1", "2"]
...
]
}
I also have corresponding Core Data objects, User and Group, with a one-to-many relationship mapping between groups and users. The relationship property in Group is called members and a separate NSArray called memberIDs holds the array of string IDs of all member users.
What I want to accomplish in RestKit is to load these objects and have the relationship mapped for me. The code for loading the users is straight forward standard stuff, and the code for loading the groups is something like
RKObjectManager* objectManager = [RKObjectManager sharedManager];
RKManagedObjectMapping* groupMapping = [RKManagedObjectMapping mappingForClass:[Group class] inManagedObjectStore:objectManager.objectStore];
groupMapping.primaryKeyAttribute = @"identifier";
[groupMapping mapKeyPath:@"_id" toAttribute:@"identifier"];
[groupMapping mapKeyPath:@"name" toAttribute:@"name"];
[groupMapping mapKeyPath:@"members" toAttribute:@"memberIDs"];
[objectManager.mappingProvider setMapping:groupMapping forKeyPath:@"groups"];
// Create an empty user mapping to handle the relationship mapping
RKManagedObjectMapping* userMapping = [RKManagedObjectMapping mappingForClass:[User class] inManagedObjectStore:objectManager.objectStore];
[groupMapping hasMany:@"members" withMapping:userMapping];
[groupMapping connectRelationship:@"members" withObjectForPrimaryKeyAttribute:@"memberIDs"];
RKObjectRouter* router = objectManager.router;
[router routeClass:[Group class] toResourcePath:@"/rest/groups/:identifier"];
[router routeClass:[Group class] toResourcePath:@"/rest/groups/" forMethod:RKRequestMethodPOST];
// Assume url is properly defined to point to the right path...
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:url usingBlock:^(RKObjectLoader *loader) {}];
When I run this code, I get the following warning spit out several times (seems to be about twice per relationship):
2012-10-24 08:55:10.170 Test[35023:18503] W restkit.core_data.cache:RKEntityByAttributeCache.m:205 Unable to add object with nil value for attribute 'identifier': <User: 0x86f7fc0> (entity: User; id: 0x8652400 <x-coredata:///User/t01A9EDBD-A523-4806-AFC2-9B06873D764E531> ; data: {
fullname = nil;
identifier = nil;
})
The strange thing is that the relationship gets set up properly (even looked at the sqlite database, and everything looks fine there) but I’m not happy with something clearly going wrong in the RestKit code, and it seems to have something to do with the bogus User mapping I create in the code above (it’s empty, since there is nothing to map in the array of IDs).
I’ve tried alternatives, for example when I add a key path mapping:
[userMapping mapKeyPath:@"" toAttribute:@"identifier"];
It complains, obviously because the key path is empty
2012-10-24 09:24:11.735 Test[35399:14003] !! Uncaught exception !!
[<__NSCFString 0xa57f460> valueForUndefinedKey:]: this class is not key value coding-compliant for the key .
And if I try to use the real User mapping
[groupMapping hasMany:@"members" withMapping:[[RKObjectManager sharedManager].mappingProvider objectMappingForKeyPath:@"users"]];
I get the following error
2012-10-24 09:52:49.973 Test[35799:18403] !! Uncaught exception !!
[<__NSCFString 0xa56e9a0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key _id.
2012-10-24 09:52:49.973 Test[35799:18403] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0xa56e9a0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key _id.'
Seems like RestKit is trying to map the ID strings themselves in the memberIDs array to User objects, which doesn’t make any sense. I’ve seen examples where the relationship array is a list of dictionaries with keys (e.g. called id) with the reference ID to the other object, for example:
{"groups":
[{"name": "John's group", "_id": "a",
"members": [
{"_id": "1"},
{"_id": "2"}
]
...
]
}
But I don’t want to change my JSON in that way (besides, I’m hoping RestKit is flexible enough to support the other way.)
Does anyone know what the proper way of doing this kind of relationship mapping in RestKit is?
Update
I ended up modifying the REST interface to send a list of dictionaries containing the user object IDs (just like the last example above), and got that to work. Still not completely happy with the setup, but the code now looks like
RKObjectManager* objectManager = [RKObjectManager sharedManager];
RKManagedObjectMapping* groupMapping = [RKManagedObjectMapping mappingForClass:[Group class] inManagedObjectStore:objectManager.objectStore];
groupMapping.primaryKeyAttribute = @"identifier";
[groupMapping mapKeyPath:@"_id" toAttribute:@"identifier"];
[groupMapping mapKeyPath:@"name" toAttribute:@"name"];
[groupMapping mapKeyPath:@"members._id" toAttribute:@"memberIDs"];
[objectManager.mappingProvider setMapping:groupMapping forKeyPath:@"groups"];
// Create an empty user mapping to handle the relationship mapping
RKManagedObjectMapping* userMapping = [RKManagedObjectMapping mappingForClass:[User class] inManagedObjectStore:objectManager.objectStore];
userMapping.primaryKeyAttribute = @"identifier";
[userMapping mapKeyPath:@"_id" toAttribute:@"identifier"];
[groupMapping hasMany:@"members" withMapping:userMapping];
[groupMapping connectRelationship:@"members" withObjectForPrimaryKeyAttribute:@"memberIDs"];
RKObjectRouter* router = objectManager.router;
[router routeClass:[Group class] toResourcePath:@"/rest/groups/:identifier"];
[router routeClass:[Group class] toResourcePath:@"/rest/groups/" forMethod:RKRequestMethodPOST];
// Assume url is properly defined to point to the right path...
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:url usingBlock:^(RKObjectLoader *loader) {}];
Just in case this helps anyone else in a similar situation. There might still be some problems with the approach I’ve taken, but so far so good (it even handles out of order loading, which is nice) – would still love to hear from anyone if there’s an answer to my original question.
Found a way to make this work, and also realized that the updated approach in my question is only supposed to be used for nested objects, not referenced objects. This thread got me on the right track: https://groups.google.com/forum/#!msg/restkit/swB1Akv2lTE/mnP2OMSqElwJ (worth reading if you’re dealing with relationships in RestKit)
Assume the groups JSON still looks like the original JSON:
Also assume that users are mapped to a keypath
users(i.e. as in something like[objectManager.mappingProvider setMapping:userMapping forKeyPath:@"users"];) the correct way to map the relationships looks like this:When I had tried this approach earlier, the thing that confused me was this line
where the first key path is referring to the key path mapping for the objects being referenced (
usersin this case), not the key path of the relationship within thegroupobject being mapped (which would have beenmembersin this case).With this change, all relationships work as expected and I’m happy.