Relationship Mapping with primary key (vs. nested object)

2,523 views
Skip to first unread message

Taylor Romero

unread,
Feb 1, 2012, 2:49:28 AM2/1/12
to RestKit
Here is my error (I have looked over every post and even the spec
classes and tried every combination of mapping the relationship I have
seen, they all give me the same results):

'[<__NSCFString 0x189ee0> valueForUndefinedKey:]: this class is not
key value coding-compliant for the key _id.'

json: { items: [

{_id: "itemId", name: "Item 1", auction: "auctionId"},
{_id: "itemId", name: "Item 2", auction: "auctionId"}

] }

My auctions are already stored locally (NSManagedObjects) and
available by id. We use MongoDB as are database, so the IDs are
strings, not numbers. Perhaps this as breaking things?

Item.h
@property (nonatomic, strong) NSString * id;
@property (nonatomic, strong) NSString * name;
@property (nonatomic, strong) Auction * auction;

//item mapping
[mapping mapKeyPathsToAttributes:
@"_id", @"id",
@"name", @"name",
nil];

//relationship mapping attempt 1
[itemMapping mapKeyPath: @"auction" toRelationship: @"auction"
withMapping: auctionMapping];

//relationship mapping attempt 2
[itemMapping hasOne:@"auction" withMapping: auctionMapping];
[itemMapping connectRelationship:@"auction"
withObjectForPrimaryKeyAttribute:@"auction"];

//relationship mapping attempt 3
[itemMapping mapKeyPath: @"auction" toRelationship: @"auction"
withMapping: auctionMapping];
[itemMapping connectRelationship:@"auction"
withObjectForPrimaryKeyAttribute:@"auction"];

//relationship mapping attempt 4
[itemMapping mapKeyPath: @"auction" toRelationship: @"auction"
withMapping: auctionMapping];
[itemMapping connectRelationship:@"auction"
withObjectForPrimaryKeyAttribute:@"_id"];


I have tried every combination you can imagine! In every case, the
RKManagedObjectMapping object expects the item.auction to be a
dictionary, not a string.

I am refraining from modifying the RestKit source, but just for fun, I
dropped a little [value isKindOfClass:[NSString class]] check in the
RKManageObjectMapping and was able to make some progress. I could make
it work pretty easily adding some checks in a few places, but that
kinda screws me for later updates.

3 side notes:

1. You could probably, wherever applicable, check if the value of a
relationship field is key value coding-compliant, and if not, always
assume it's the id (or hell, I've even toyed with the idea of dropping
it onto an [NSDictionary dictionaryWithObject: value forKey:
primaryKeyAttributeMapping.sourceKeyPath] and allowing the code to
continue to run as if i I had received a nested array in the first
place. Doing this basic check would give the library a more "it just
works" feel vs. having to guess at what configuration options to use
to tell it to expect the ID, not a nested object.

2. I have also run into some ARC (automatic reference counting) issues
that are causing some delegate methods to fail entirely (with
EXEC_BAD_ACCESS) . I attempted to use - (void) objectLoader:
(RKObjectLoader *)loader willMapData:(inout id *)mappableData to
manually modify the dictionary to turn { auction: id } into { auction:
{ _id: id } } but mappableData is nil immediately (you make a mutable
copy before passing a pointer ref). I imagine this will be addressed
in a later release (as ARC becomes more commonplace) so I'm not really
concerned.

3. This would end all your "it doesn't handle the my JSON/XML schema
issues" because it would be powerful yet mega easy to use.

RKManagedObjectMapping* mapping = [RKManagedObjectMapping
mappingForClass: [Item class]];
mapping.primaryKeyAttribute = @"id";

//first param is parsed data from json, 2nd param is an instance of
Item (whatever is passed to mappingForClass)
[mapping mapUsingBlock^(NSDictionary d, id item) {

item._id = [d valueForKey: @"_id"];
item.auction = [Auction findByAttribute: [d valueForKey: @"_id"]]

}];

If something like this existed, I'm sure it would be the end of all
mapping/relationship issues. Sure, it's a tad more typing, but having
to configure all the mapping is a ton of typing anyway, plus it's less
clear, easier to mess up, and leads to long posts like this =) And you
can easily see what I'm doing by reading the code without having to
dive into documentation or class headers.

Anyway, thank you for the great library and keep up the great work!

Jeff Arena

unread,
Feb 1, 2012, 8:09:51 AM2/1/12
to res...@googlegroups.com
For some background, these methods should only be used when you have nested JSON objects:

[itemMapping mapKeyPath:@"auction" toRelationship:@"auction" withMapping:auctionMapping];
[itemMapping hasOne:@"auction" withMapping: auctionMapping];

For either of these to work properly, instead of an auctionId associated with the auction keyPath in your JSON, you need a nested object. That why your mapping is always failing expecting a dictionary.

Instead, since you do not have a nested object, you should be using only this:

[itemMapping connectRelationship:@"auction" withObjectForPrimaryKeyAttribute:@"auction"];

In this case, you don't have a nested auction JSON element, but instead simply have a primaryKey reference in your Item JSON that points to an Auction you care about. In this situation, it will always be impossible to actually map the Auction class itself. All you're really trying to do with this mapping is connect these new Items with Auctions that already exist on the device, likely via a call to another API endpoint.

Hope this helps.

Jeff

Taylor Romero

unread,
Feb 1, 2012, 11:48:34 AM2/1/12
to RestKit
Hey Jeff,

Thanks for the feedback. Only using the
connectRelationship:withObjcetForPrimaryKeyAttribute fails here:

NSAssert(mapping, @"Attempted to connect relationship for keyPath
'%@' without a relationship mapping defined.");

I followed a suggested emailed to my by tiga which was to create a new
field to house the destination object and that appears to have worked.

But, now I have an "auction" field with an NSSring that is the ID and
an "auctionObj" field with my related model instance. Yuck! I will
use the 2 field approach if I am not able to simply use my "auction"
field to do all I need, but of course, housing the relationship twice
(once as the id, once as the actual related model) is not optimal (and
probably not necessary once I get this worked out =)

Thank you again!

Tay

Kamil

unread,
Feb 1, 2012, 1:52:57 PM2/1/12
to res...@googlegroups.com
I have a similar problem. Part of my JSON lookes like:

promotions: [{
id: 2
promotionType_id: 1,
startDateTime: "2011-09-26 17:40:22",
},...

and I have generated NSManaged Object:

@interface Promotion : NSManagedObject
@property (nonatomic) int64_t identifier;
@property (nonatomic) NSTimeInterval startDateTime;
@property (nonatomic, retain) PromotionType *promotionType;
@end

*promotionType should be assigned to apriopriate Object with ID=1.

Due to your advice ('use only connectRelationship') my mapping lookes like:

RKManagedObjectMapping *promotionMapping = [RKManagedObjectMapping mappingForClass:[Promotion class]];
[promotionMapping mapKeyPathsToAttributes:@"id", @"identifier", nil];
[promotionMapping connectRelationship:@"promotionType_id" withObjectForPrimaryKeyAttribute:@"promotionType"];
promotionMapping.primaryKeyAttribute = @"identifier";
[objectManager.mappingProvider setMapping:promotionMapping forKeyPath:@"Promotion"];

When I run my app I get an exception:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to connect relationship for keyPath '(null)' without a relationship mapping defined.'

What am I doing wrong?

Blake Watters

unread,
Feb 2, 2012, 10:31:40 AM2/2/12
to res...@googlegroups.com
Kamil -

Your arguments are reversed in the connectRelationship call. The first argument is the relationship to hydrate (promotionType) and the second is the primary key. If this still doesn't work, please open an issue for me on the GH 0.9.4 milestone.

--
Blake Watters
VP Engineering, GateGuru
Mobile: 919.260.3783
Get GateGuru for iOS: bit.ly/ggitunes
Get GateGuru for Android: bit.ly/ggandroid

Interested in creating the mobile travel experience of the future? We're hiring!

Kamil

unread,
Feb 2, 2012, 12:27:44 PM2/2/12
to res...@googlegroups.com
If I do as you say:

RKManagedObjectMapping *promotionMapping = [RKManagedObjectMapping mappingForClass:[Promotion class]];
[promotionMapping mapKeyPathsToAttributes:@"id", @"identifier", nil];
[promotionMapping connectRelationship:@"promotionType" withObjectForPrimaryKeyAttribute:@"promotionType_id"];
promotionMapping.primaryKeyAttribute = @"identifier";
[objectManager.mappingProvider setMapping:promotionMapping forKeyPath:@"Promotion"];

I still have an exception:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to connect relationship for keyPath '(null)' without a relationship mapping defined.'

Should I declare a relationship like:

[promotionMapping mapKeyPath:@"promotionType_id" toRelationship:@"promotionType" withMapping:promotionTypeMapping];

before calling connectToRelationship?

Or does anyone have a working example with mapping by primary key? I don't belive anybody did it before :/

Jeff Arena

unread,
Feb 2, 2012, 12:29:53 PM2/2/12
to res...@googlegroups.com
Yes, you'll need to both define the relationship mapping and then call connectRelationship.

Taylor Romero

unread,
Feb 2, 2012, 2:43:04 PM2/2/12
to RestKit
Would that match "Attempt 3" from my original post?

//relationship mapping attempt 3
[itemMapping mapKeyPath: @"auction" toRelationship: @"auction"
withMapping: auctionMapping];
[itemMapping connectRelationship:@"auction"
withObjectForPrimaryKeyAttribute:@"auction"];

I think my issue is using 1 field for everything, rather than having a
separate auctionId field.

Kamil

unread,
Feb 2, 2012, 4:02:19 PM2/2/12
to res...@googlegroups.com
Yes, my problem lookes like your "Attempt 3", but i have explicitly int as an ID.

@Jeff when I try something you suggest:

RKManagedObjectMapping *promotionMapping = [RKManagedObjectMapping mappingForClass:[Promotion class]];
[promotionMapping mapKeyPathsToAttributes:@"id", @"identifier", nil];
[promotionMapping mapKeyPath:@"promotionType_id" toRelationship:@"promotionType" withMapping:promotionTypeMapping];
[promotionMapping connectRelationship:@"promotionType" withObjectForPrimaryKeyAttribute:@"promotionType_id"];
promotionMapping.primaryKeyAttribute = @"identifier";
[objectManager.mappingProvider setMapping:promotionMapping forKeyPath:@"Promotion"];

it means I map relationship and then call connectRelationship: I get:

Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFNumber 0x7118090> valueForUndefinedKey:]: this class is not key value coding-compliant for the key id.'

I have really no idea what other options are...

Jeff Arena

unread,
Feb 2, 2012, 4:07:19 PM2/2/12
to res...@googlegroups.com
You're relationship mapping is incorrect. Should look like this:

[promotionMapping mapKeyPath:@"promotionType" toRelationship:@"promotionType" withMapping:promotionTypeMapping serialize:NO]; 
[promotionMapping connectRelationship:@"promotionType" withObjectForPrimaryKeyAttribute:@"promotionType_id"]; 

Kamil

unread,
Feb 2, 2012, 5:12:11 PM2/2/12
to res...@googlegroups.com
Well, this didn't work either, but you put me in the right direction. 
I think i've found the solution.

First of all I expanded my Promotion model class to have both promotionType and promotionType_id fields like this (most important lines are bolded):

@interface Promotion : NSManagedObject
@property (nonatomic) int64_t identifier;
@property (nonatomic) NSTimeInterval startDateTime;
@property (nonatomic) int64_t promotionType_id;
@property (nonatomic, retain) PromotionType *promotionType;
@end

Then I configured mapping like this:

RKManagedObjectMapping *promotionMapping = [RKManagedObjectMapping mappingForClass:[Promotion class]];

[promotionMapping mapKeyPathsToAttributes:@"id", @"identifier", @"promotionType_id", @"promotionType_id", nil];

[promotionMapping mapKeyPath:@"promotionType" toRelationship:@"promotionType" withMapping:promotionTypeMapping serialize:NO];
[promotionMapping connectRelationship:@"promotionType" withObjectForPrimaryKeyAttribute:@"promotionType_id"];

promotionMapping.primaryKeyAttribute = @"identifier";
[objectManager.mappingProvider setMapping:promotionMapping forKeyPath:@"Promotion"];

 

And it seems to work! In SQLite I have something like duplicated column (the same values in both promotionType_id and promotionType), but in CoreData after synchronization I have beautiful Promotions with PromotionTypes within! :)

Thanks to all of you for help.

Taylor Romero

unread,
Feb 3, 2012, 1:24:10 AM2/3/12
to RestKit
I had to do it by having 2 fields as well, one for the ID, one for the
actual related model =(

Of course, this is icky, but easy enough to work around.

Thank you Jeff for your feedback!

On Feb 2, 3:12 pm, Kamil <kamil.burc...@gmail.com> wrote:
> Well, this didn't work either, but you put me in the right direction.
> I think i've found the solution.
>
> First of all I expanded my Promotion model class to have both promotionType
> and promotionType_id fields like this (most important lines are bolded):
>
> @interface Promotion : NSManagedObject
>
> @property (nonatomic) int64_t identifier;
> @property (nonatomic) NSTimeInterval startDateTime;
> *@property (nonatomic) int64_t promotionType_id;
> @property (nonatomic, retain) PromotionType *promotionType; *
>
> @end
>
> Then I configured mapping like this:
>
> RKManagedObjectMapping *promotionMapping = [RKManagedObjectMapping
> mappingForClass:[Promotion class]];
> [promotionMapping mapKeyPathsToAttributes:@"id", @"identifier", *@"promotionType_id",
> @"promotionType_id"*, nil];
> * [promotionMapping mapKeyPath:@"promotionType"
> toRelationship:@"promotionType" withMapping:promotionTypeMapping
> serialize:NO];
> [promotionMapping connectRelationship:@"promotionType"
> withObjectForPrimaryKeyAttribute:@"promotionType_id"];*

Tony

unread,
Jun 28, 2012, 3:26:41 PM6/28/12
to res...@googlegroups.com
Are there any updated solutions to this? It seems awfully hacky to have to use two fields to handle a relationship. Also, does anyone have an example of connecting objects with manyToMany relationships with each other?

Tony

unread,
Jun 29, 2012, 3:38:24 AM6/29/12
to res...@googlegroups.com
Just in case anybody might find this useful, I ended up writing a subclass of RKManagedObjectMapping along with a bit of method swizzling magic and arrived at a solution that works very well for me around RestKit's limitations. Here it is attached.(fyi, I use the conciseKit library)

  1. #import "CLManagedObjectMapping.h"
  2.  
  3. /*
  4.  * Make it possible to map related objects by just primary key rather than nested JSON or two fields
  5.  */
  6. @implementation CLManagedObjectMapping
  7.  
  8. - (id)mappableObjectForData:(id)mappableData {
  9.     if ([mappableData isKindOfClass:[NSString class]]) {
  10.         RKObjectAttributeMapping* pMapping = [[self.attributeMappings $select:^BOOL(id object) {
  11.             return [[object destinationKeyPath] isEqualToString:self.primaryKeyAttribute];
  12.         }] lastObject];
  13.         return [super mappableObjectForData:$dict(mappableData, pMapping.sourceKeyPath)];
  14.     }
  15.     return [super mappableObjectForData:mappableData];
  16. }
  17.  
  18. @end
  19.  
  20. @implementation RKManagedObjectMappingOperation (CLSupport)
  21.  
  22. - (BOOL)_performMapping:(NSError**)error {
  23.     if ([self.sourceObject isKindOfClass:[NSString class]]
  24.         && [self.objectMapping isKindOfClass:[CLManagedObjectMapping class]])
  25.         return YES;
  26.     return [self _performMapping:error];
  27. }
  28.  
  29. + (void)load {
  30.     [$ swizzleMethod:@selector(performMapping:)
  31.                 with:@selector(_performMapping:)
  32.                   in:[RKManagedObjectMappingOperation class]];
  33. }
  34.  
  35. @end

donack

unread,
Jul 18, 2012, 10:09:16 AM7/18/12
to res...@googlegroups.com
Ho do you use this? Thank you.

Tony Xiao

unread,
Jul 18, 2012, 2:51:49 PM7/18/12
to res...@googlegroups.com
Hi there,

here's the interface

//
//  CLManagedObjectMapping.h
//

#import "RKManagedObjectMapping.h"

@interface CLManagedObjectMapping : RKManagedObjectMapping

@end


It really doesn't contain much at all. Here's how you would use it (we've got a self-referential many-to-many relationship btw)

+ (CLManagedObjectMapping *)objectMapping:(BOOL)isList {
    CLManagedObjectMapping* mapping = [CLManagedObjectMapping mappingForClass:[self class]
                                                         inManagedObjectStore:[[NSApp delegate] managedObjectStore]];
    mapping.primaryKeyAttribute = @"resourceURI";
    mapping.rootKeyPath = isList ? @"objects" : @"";
    [mapping mapAttributes:@"title", @"behavior", @"type", @"info", nil];
    [mapping mapKeyPath:@"resource_uri" toAttribute:@"resourceURI"];
    [mapping mapKeyPath:@"parents" toRelationship:@"parents" withMapping:mapping serialize:NO];
serialize:NO];

    

    return mapping;
}

MountainMath

unread,
Jul 19, 2012, 7:31:00 PM7/19/12
to res...@googlegroups.com
Thanks Tony,

played around with your fix and it works great. Needed to make two small modifications though to get it to work on my end:
1) i changed your [NSString class] into [NSNumber class] since my foreign keys are numbers, and
2) the swizzle magic did not work for me, i inserted the (un-concise) code from the swizzle method

           if ([value isKindOfClass:[NSNumber class]]) {

                RKObjectAttributeMapping *mapping=nil;

                for (RKObjectAttributeMapping *object in objectMapping.attributeMappings) {

                    if ([[object destinationKeyPath] isEqualToString:((RKManagedObjectMapping*)objectMapping).primaryKeyAttribute]) {

                        mapping=object;

                    }

                }

                value=[NSDictionary dictionaryWithObjectsAndKeys:value, mapping.sourceKeyPath,nil];

            }


code right before the 

            destinationObject = [objectMapping mappableObjectForData:value];

line in the method of RKObjectMappingOperation (version 0.10.1). That changes "value" for the rest of the method, otherwise i ran into trouble...


then things work without artificial (transient) foreign key variables in my core data.

cheers,
--jens

pidge

unread,
Sep 7, 2012, 6:46:20 AM9/7/12
to res...@googlegroups.com
i have two entities named venue and venue timing. i relate both together.
But it works sometime. not working some times..
Can any one please give the solution for this?

code:

RKManagedObjectMapping *venuesMapping = [[SRObjectManager entryObjectMappings] objectForKey:@"Venues"];

    RKManagedObjectMapping *venueTimingsMapping = [[SRObjectManager entryObjectMappings] objectForKey:@"VenueTimings"];

    [venuesMapping mapKeyPath:@"dummy" toRelationship:@"venueInverse" withMapping:venueTimingsMapping];

    [venuesMapping connectRelationship:@"venueInverse" withObjectForPrimaryKeyAttribute:@"mUrl"];

    [venueTimingsMapping mapKeyPath:@"dummy" toRelationship:@"venues" withMapping:venuesMapping]; // To avoid the mapping attempt while parsing JSON and creating dummy row in Venue table

    [venueTimingsMapping connectRelationship:@"venues" withObjectForPrimaryKeyAttribute:@"venueUrl"];

Reply all
Reply to author
Forward
0 new messages