Skip to content

Create and Inject Custom Objects For Complex Core Data Relations

dave-thompson edited this page Jun 13, 2012 · 4 revisions

Create and Inject Custom Objects For Complex Core Data Relationships

Purpose: This recipe is used to create a glue object (with attributes) that completes a many-to-many relationship in Core Data.

For example, a person may belong to a variety of social organizations, which in turn have many members. So you have a Core Data model for Person and another model for Organization with a many-to-many relationship between them. But what happens when you want to stick some attributes in there to describe each of those relationships. An example might be an attribute for a person's title or role within an organization (i.e. "President", "Social Coordinator"). These attributes don't really belong in the Organization model, nor on the Person model. In such cases, I'll use a third entity, Position. Position will have one Person and one Organization relationship defined. Modeling that in Core Data is easy enough, but how do you handle this in RestKit's mapping system?

In the following recipe, we will show how to map a SLFCommitteePosition glue object that connects a SLFLegislator to a SLFCommittee. A legislator will sit on many committees, which in turn have many other members who have differing roles and responsibilities.

The JSON for the committee looks like this (truncated for legibility):

{
  "id": "MDC000065",
  "committee": "HOUSE FACILITIES COMMITTEE",
  "updated_at": "2011-03-01 00:44:07",
  "state": "md",
  "chamber": "lower",
  "members": [
	{
	  "leg_id": "MDL000319",
	  "role": "member",
	  "name": "Mary Ann Love"
	},
	{
	  "leg_id": "MDL000334",
	  "role": "member",
	  "name": "LeRoy E. Myers, Jr."
	},
	{
	  "leg_id": "MDL000342",
	  "role": "member",
	  "name": "Shane E. Pendergrass"
	}
  ]
}

And the JSON for the legislator looks like this (truncated for legibility):

{
  "id" : "MDL000319",
  "leg_id" : "MDL000319",
  "full_name" : "Mary Ann Love",
  "district" : "32",
  "state" : "md",
  "party" : "Democratic",
  "chamber" : "lower",
  "updated_at" : "2011-07-18 02:31:25",
  "roles" : [
	{
	  "term" : "2011-2014",
	  "level" : "state",
	  "chamber" : "lower",
	  "type" : "member"
	},
	{
	  "term" : "2011-2014",
	  "committee_id" : "MDC000043",
	  "chamber" : "lower",
	  "committee" : "ECONOMIC MATTERS COMMITTEE",
	  "type" : "committee member"
	},
	{
	  "term" : "2011-2014",
	  "committee_id" : "MDC000065",
	  "chamber" : "lower",
	  "committee" : "HOUSE FACILITIES COMMITTEE",
	  "type" : "committee member"
	}
  ]
}

So we'll be inferring a committee position from the committee's "members" and from the legislator's "roles". Notice that a primary key does not exist for our CommitteePosition object. So, while mapping, we will compound two other keys (legislator and committee IDs) to create the position's primary key.

The following code block goes in your object loader delegate. (This case, that's the view controller that displays the details for an individual legislator. A similar code block also sits in my CommitteeViewController to catch the mappings coming from the other direction.

- (void)objectLoader:(RKObjectLoader*)loader willMapData:(inout id *)mappableData {
	
	if (loader.objectMapping.objectClass == [SLFLegislator class]) {
		
		NSArray* origRolesArray = [*mappableData valueForKeyPath:@"roles"];	// array of dictionaries
		NSString *legID = [*mappableData objectForKey:@"leg_id"];		// this legislator's id
		NSString *legName = [*mappableData objectForKey:@"full_name"];		// ... etc.
		
		if (!legID)
			legID = self.legislator.legID;
		
		if (!legName)
			legName = self.legislator.fullName;

		NSMutableArray* newRolesArray =  [[NSMutableArray alloc] initWithCapacity:[origRolesArray count]];
		
		int roleIndex = 0;
		for (NSDictionary* origRole in origRolesArray) {
			
			//NSString *term = [origRole objectForKey:@"term"];  // our legislative session/year
			NSString *comID = [origRole objectForKey:@"committee_id"];

			// we use these to create a unique committee position object id 
			if (!comID || !legID) // include term ??
				continue;

			NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@ AND %K == %@", 
                                                                                  @"committeeID", comID, @"legID", legID];

			SLFCommitteePosition *position = [SLFCommitteePosition objectWithPredicate:predicate];

			if (!position) {	// we didn't find one already in core data, create one.
				position = [SLFCommitteePosition object];
				position.committeeID = comID;
				position.legID = legID;
			}
			
			// It doesn't hurt to just update these properties we know about anyway, I guess.
			position.legislatorName = legName;
			position.committeeName = [origRole objectForKey:@"committee"];	// committee's name
			position.positionType =	[origRole objectForKey:@"type"];	// member, or chairperson, etc.			

				//  This seems like a klunky way to generate a unique primary key ID, but the
				//  aggregation of these three attributes *should* be unique across everything.

			position.posID = [NSString stringWithFormat:@"%@|%@", comID, legID];	// include term?
											
			[newRolesArray addObject:position];

			roleIndex++;
		}
		
		[*mappableData removeObjectForKey:@"roles"];		 //remove the old roles array from the legislator
		[*mappableData setObject:newRolesArray forKey:@"roles"]; // inject our modified roles array.
		[newRolesArray release];
	}
}

Now, back in the app delegate (or wherever you normally set up your initial mappings:

RKManagedObjectMapping* posMapping = [RKManagedObjectMapping mappingForClass:[SLFCommitteePosition class]];
posMapping.primaryKeyAttribute = @"posID";
[posMapping mapAttributes:@"posID", @"positionType",@"legID",@"legislatorName",@"committeeID",@"committeeName",nil];

[comMapping mapKeyPath:@"members" toRelationship:@"positions" withObjectMapping:posMapping];
[legMapping mapKeyPath:@"roles" toRelationship:@"positions" withObjectMapping:posMapping];

[objectManager.mappingProvider setObjectMapping:comMapping forKeyPath:@"committee"];
[objectManager.mappingProvider setObjectMapping:legMapping forKeyPath:@"legislator"];
[objectManager.mappingProvider setObjectMapping:posMapping forKeyPath:@"position"];