I am using RestKit with Core Data and I am having trouble mapping the relationships between entities. The issue is that I have 3 different jsons (for 3 different screens) and these jsons have interrelated data. My 3 entities are Post, Conversation, and Message. The relationships are as such: Each post has many conversations (to-many relationship) and each conversation has many messages (to-many relationship).
Here is the first json for an array of posts:
{"success":true,"result":
[{"totalUsers":1,"lastMessageDate":1411612821000,"id":874,"title":"My post title"},
{"totalUsers":3,"lastMessageDate":1411536669000,"id":539,"title":"Message me"}]}
Here is the second json, which is an array of conversations for a specific post:
{"success":true,"result":
[{"badgeSize":1,"lastMessage":
{"id":1725,"text":"hey","datePublished":141153372347},"id":208,"username":"energydrink"}]}
Here is the third json for a particular conversation:
{"success":true,"result":
{"spamStatus":false,"messages":
[{"id":416,"text":"hello","datePublished":1403432789000},
{"id":380,"text":"whats up","datePublished":1403432144221}]}}
So as you can see, the data for the relationships is split up into 3 jsons. Here is how I have currently configured my relationships and response descriptor within RestKit:
[conversationMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"messages" toKeyPath:#"messages" withMapping:messageMapping]];
[postMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"conversations" toKeyPath:#"conversations" withMapping:messageMapping]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:postMapping method:RKRequestMethodAny pathPattern:nil keyPath:#"result" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
Since the response descriptor is mapped with postMapping, I am unable to correctly map data for the 2nd and 3rd jsons and then store within Core Data with my configuration. Any help is greatly appreciated.
I figured it out. Basically, I used RestKit's mapping for the first json. When tableView:didSelectRowAtIndexPath: was called on the first view controller, I passed the post object to the second view controller. For the second json response, I deserialized the data and added the conversations to that post object. Then, I simply called [[RKManagedObjectStore defaultStore].mainQueueManagedObjectContext saveToPersistentStore:&error];. Now it works just fine.
Related
I have a core data datamodel that is quite a bit different from the one my API developer expects. (Due to the way the app works I cannot deviate from this)
I have simplified my managedObjectModels into the desired format using some custom classes
A convertor class that starts it all and delivers: (simplified for readability)
a generalData class that holds the top level object
a manyData class that represents the format of the to-many relationship objects within the GeneralData
a otherData class that also holds to-many relationship objects within the generalData class but is unrelated to the manyData class.
These are the generalData attributes:
-(NSString*) ID
-(NSString*) name
-(NSString*) position
-(NSArray*) manyDataObjects
-(NSArray*) otherDataObjects
each manyData and OtherDataObject in their turn holds some basic attributes, their own ID, and the generalData ID.
I have added the following requestmapping to the generalData class:
+ (RKObjectMapping *)defineRequestMapping
{
RKObjectMapping * mapping = [RKObjectMapping mappingForClass:[generalData class]];
[mapping addAttributeMappingsFromDictionary:#{
#"generalID": #"generalID",
#"generalName": #"generalName",
#"generalPosition": #"generalPosition",
#"generalManyData": #"generalManyData",
#"generalOtherData": #"generalOtherData"
}];
return mapping;
}
and run it like so:
RKObjectMapping *requestMapping = [[generalData defineRequestMapping] inverseMapping];
[objectManager addRequestDescriptor:[RKRequestDescriptor
requestDescriptorWithMapping:requestMapping objectClass:[generalData class] rootKeyPath:nil method:RKRequestMethodPOST]];
I have also added similar defineRequestMapping Class Methods to the manyData and OtherData classes.
Everytime I try to serialize the generalData class using Restkit I get an error telling me that it cannot serialize the unkown manyData class.If I comment the "generalManyData" and "generalOtherData" line it works and serializes the other 3 attributes into the body.
I cannot seem to find a tutorial to clear my mind and am at the moment kind of mindblocked at what to do next. I tried using a relationshipMapping but that didn't bring me any further. Perhaps I forgot something?
Check out the docs for RKRelationshipMapping
You need to nest the RKRelationshipMapping object for your generalManyData within the mapping for your generalObject to establish the to-many relationship between the two classes. Simple string values won't work if there are nested objects.
RKObjectMapping *generalManyMapping = [RKObjectMapping mappingForClass:[generalData class]];];
[generalManyMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"generalManyData"
toKeyPath:#"generalManyData"
withMapping:#{#"propertyOne" : #"propertyOne", #"propertyTwo" : #"propertyTwo"}]];
I've got an iOS app that uses RestKit with Core Data persistence. It loads its data from a few different endpoints, most of which return complete object graphs as nested JSON objects. But one of the endpoints returns objects that contain foreign key references (not nested JSON) to another endpoint's data.
In other words (abbreviated):
[postMapping addAttributeMappingsFromArray:#[ #"postID", ... ]];
// ...
[commentMapping addAttributeMappingsFromArray:#[ ... ]];
[commentMapping addConnectionForRelationship:#"post" connectedBy:#"postID"];
where the relevant Post and Comment entities are set up with the appropriate relationships and so forth.
This all works exactly as it should but only if the request to fetch posts finishes before the request to fetch comments. Otherwise a comment won't be connected to its post, because the post hasn't been fetched and mapped yet.
Right now, I'm getting around it by just not fetching the comments until the posts have been fetched. Something like
- (void)fetchPosts {
[objectManager getObjectsAtPath:#"/posts.json"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self fetchComments]; // *now* we can fetch comments
// ...
But I'm wondering if there's a better way?
I figure I might be able to do something like
[objectManager.HTTPClient.operationQueue setMaxConcurrentOperationCount:1];
but most of the time, I do want concurrent requests. It's only two of the requests that must be sequential.
I guess I could set up two different object managers (one that does sequential requests and one that does concurrent ones), but I'm not sure that makes sense.
So, is there are way to specify that two specific -getObjectsAtPath:... requests should be handled sequentially, while all other can run whenever?
Look at creating another request descriptor which drills down into the comment details and processes only the postID attributes. For each one, run the postMapping to create a stub object.
Note that you might still run into race conditions with 2 different background contexts both creating the same objects at the same time, so you may need to handle merge issues (this is if you have multiple object managers as you talked about multiple endpoints).
I have successfully mapped JSON responses from Parse to my Core Data entity, Message, which has a to-one relationship with Conversation entity. When POSTing to Parse, relationships should have the format:
{
"conversation": {
"className": "Conversation",
"objectId": "MK2GbaBseP",
"__type": "Pointer"
}
}
Therefore I need to serialise the Conversation entity in Core Data to this custom JSON. Is there a way that I can add in these extra fields as part of the RestKit serialisation process?
RKObjectMapping *messageRequestMapping = [RKObjectMapping requestMapping];
... some custom serialisation code
[manager addRequestDescriptor:[RKRequestDescriptor requestDescriptorWithMapping: messageRequestMapping objectClass:PPSMessage.class rootKeyPath:nil method:RKRequestMethodAny]];
I have tried creating an RKValueTransformer subclass for converting nested objects, but it doesn't appear to be being called.
Any help appreciated.
A couple of possible approaches:
1) create a custom class that describes the format, you pass it the object and it returns a dictionary during mapping which contains all of the required information.
In a number of ways this is best because parse.com layers on top of simply posting the object and that is what this models.
2) add methods to your managed object class which you can then add to the mapping and supply the required information.
3) just create a dictionary to pass to RestKit (limitations on the path that can be automatically determined so requires alternate routing config).
4) give up and use the parse supplied SDK.
I'm developing a CoreData iOS app that's backed by a (Rails) REST API (that supports shallow routes). Because there are a lot of objects in the graph, I'd like the REST GETs to not to include a lot of nested results and, instead, just contain references that RestKit uses to establish (faulted) relationships. I'll then use shallow routes to request the individual (or groups of) objects as needed.
Assuming I have a one-to-many (<-->>) data model such as A <-->> B, I have:
RKEntityMapping *a_mapping = [RKEntityMapping mappingForEntityForName:#"A" inManagedObjectStore:managedObjectStore];
[a_mapping addAttributeMappingsFromDictionary:#{#"a_id" : #"aId", ...}];
a_mapping.identificationAttributes = #[#"aId"];
RKEntityMapping *b_mapping = [RKEntityMapping mappingForEntityForName:#"B" inManagedObjectStore:managedObjectStore];
[b_mapping addAttributeMappingsFromDictionary:#{#"b_id" : #"bId", ...}];
b_mapping.identificationAttributes = #[#"bId"];
[a_mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"bs" toKeyPath:#"bs" withMapping:b_mapping]];
I have these routes:
NSIndexSet *statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful);
RKResponseDescriptor *a_ResponseDescriptor;
a_ResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:a_mapping method:RKRequestMethodGET pathPattern:#"/A/:aId" keyPath:#"A" statusCodes:statusCodes];
RKResponseDescriptor *b_ResponseDescriptor;
NSIndexSet *statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful);
b_ResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:b_mapping method:RKRequestMethodGET pathPattern:#"/B/:bId" keyPath:#"B" statusCodes:statusCodes];
[[RKObjectManager sharedManager] addResponseDescriptor:a_ResponseDescriptor];
[[RKObjectManager sharedManager] addResponseDescriptor:b_ResponseDescriptor];
I have a couple of related questions:
How should I structure the JSON when returning an 'A' record so that RestKit will instantiate stubs for any related 'B' objects?
Similarly, if I want to request a bunch of B objects (without prior knowledge of A objects) how do I structure the JSON when returning a 'B' record so that RestKit will instantiate stubs for the owning 'A' object?
What additional setup/code do I need with RestKit?
Currently, I have one direction working (A --> B), but I can't seem to figure out how to get the reverse to work. In particular, /A/1.json returns something like:
{"a": {"a_id":1, "bs":[{"b_id": 2}, {"b_id": 3}]}}
And B/2.json returns:
{"b": {"b_id":2, "a_id": 1}}
Should I instead be using something like:
{"b": {"b_id":2, "a": {"a_id": 1}}}
? Any help/advice would be appreciated.
Faults are a Core Data concept. RestKit is involved with creating objects in the data store, but it is the act of fetching them from the data store which faults them. It sounds more like what you're interested in is having RestKit create 'stub' objects in the data store (objects with the correct id and relationship but no detailed attributes set) so they can be filled in later when required.
Your current mappings and JSON for /A/1.json are fine for creating a stub instance of A and stubs of the connected Bs.
B/2.json would only create a stub B (it wouldn't do anything with A as the mapping has no relationship information attached). It's the mapping that is at 'fault', not the JSON. Add an RKRelationshipMapping to the mapping for B and the stub would be created and connection made.
That said, you would not usually be making a request for B which required 'back-stubbing' of A, because you would usually need to have requested A in order to get B.
Finally, you don't need different mappings for stubs and detail. Just have 1 mapping which contains all the detail and id mappings and RestKit will do everything that it can based on the received JSON (so if the JSON only contains ids you will just get stub objects).
To build off of what Wain said, the solution is to:
establish bidirectional RKRelationshipMappings
use mappings with nested mappings when sending JSON for both A and B objects. Ex. {"b": {"b_id":2, "a": {"a_id": 1}}}
Also, if you're working with the iOS simulator, be sure to set your HTTP response headers to not cache things. This nailed me a bunch of times since my code was using stale data.
I'm running into an issue while trying to synchronize a list of favourited teams for a given user between my iOS app and my server. The flow of events is as follows:
User favourites a team
New favouriteTeam object is created and saved to Core Data:
NSError *error;
[[self.currentUser managedObjectContext] save:&error];
[[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext save:&error];
Array of modified favouriteTeams is POSTed to the server where they are timestamped and returned with any other modified or recently added (by another device) objects.
The problem I'm running into is that the item that is POSTed, since it is always returned is being duplicated instead of being overwritten (based on the identificationAttributes). None of the other objects returned (whether modified or newly created by another device) get duplicated... just the newly created device from the user's device.
Here's my Request/Response mapping code:
RKObjectManager *objectManager = [RKObjectManager sharedManager];
[objectManager setRequestSerializationMIMEType:RKMIMETypeJSON];
// POST Request Mapping
RKObjectMapping *favouriteTeamMapping = [RKObjectMapping requestMapping];
[favouriteTeamMapping addAttributeMappingsFromArray:#[#"uuid", #"teamName", #"displayOrder"]];
RKRequestDescriptor *favouriteTeamRequestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:favouriteTeamMapping objectClass:[FavouriteTeam class] rootKeyPath:#"favouriteTeams"];
[objectManager addRequestDescriptor:favouriteTeamRequestDescriptor];
// Response Mapping
RKEntityMapping *favouriteTeamResponseMapping = [RKEntityMapping mappingForEntityForName:#"FavouriteTeam" inManagedObjectStore:objectManager.managedObjectStore];
favouriteTeamResponseMapping.identificationAttributes = #[#"uuid"];
[favouriteTeamResponseMapping addAttributeMappingsFromArray:#[#"uuid", #"teamName", #"displayOrder", #"lastModified"]];
RKResponseDescriptor *favouriteTeamResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:favouriteTeamResponseMapping pathPattern:#"/api/favouriteteam/" keyPath:#"data.favouriteTeams" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptorsFromArray:#[favouriteTeamResponseDescriptor]];
// POST
[objectManager postObject:favTeamsArray path:#"/api/favouriteteam/" parameters:nil success:nil failure:nil];
I can't seem to figure out why this duplication is happening when the identificationAttributes (favouriteDrug "uuid" attribute) are set. The objects are identical (even the same uuid) in Core Data. What is causing this behaviour?
NOTE: This question is also posted on the RestKit Google Groups here.
Thanks!
UPDATE: It appears that this duplication doesn't occur the first time a team is favorited. Any of the following attempts to favorite a team result in this duplication.
I had a similar issue, and while I cannot really confirm if this addressed your problem, will post my answer here anyways.
My problem was that the first object would be created fine, but thereafter restkit would save duplicate objects in coredata, so this sounds like the problem you are seeing.
I was making the post call to create object by:
* creating a blank object in coredata and filling it with whatever attributes I need
* make a post call to server using restkit API and passing in the newly created object
What restkit does under the hood is to take the response, fill it into the new object I created, and save it into coredata WITHOUT checking if there is another object of the same unique id beforehand. This is why the first object creation was fine, but subsequent objects were duplicates.
The way I solved it was actually to pass in the raw values as params to the restkit post API call, and nil as the object. On the reply, restkit will then look through coredata first to see if an object of that ID exists and merge changes with that object, or create a new one.