I am having some difficulties POSTing an NSManagedObject with nested NSManagedObjects with RestKit. I appear to be getting duplicate records for the sub-NSManagedObjects inserted into CoreData when the POST returns. Here is a snapshot of the model:
Here is the JSON that I am POSTing:
{
"actions": [],
"application": "Identify",
"createBy": "welcomed",
"createDt": "2014-04-11T16:26:15Z",
"description": null,
"externalId": null,
"groupId": "5",
"id": 0,
"images": [
{
"format": "JPEG",
"height": 200,
"id": 0,
"image": "/9j/4A..../Pv5n/9k=",
"status": "C",
"type": "MUGSHOT",
"width": 200
}
],
"locked": null,
"modifyBy": null,
"modifyDt": null,
"priv": null
}
Here is the JSON that is returned from the service after the POST:
{
"actions": [],
"application": "Identify",
"createBy": "welcomed",
"createDt": 1397233575000,
"description": null,
"externalId": null,
"groupId": "5",
"id": 11,
"images": [
{
"captureDevice": null,
"createBy": null,
"createDt": null,
"format": "JPEG",
"height": 200,
"id": 11,
"image": "/9j/4AAQSkZJR.../Pv5n/9k=",
"recordId": 11,
"status": "C",
"type": "MUGSHOT",
"width": 200
}
],
"locked": false,
"modifyBy": null,
"modifyDt": null,
"priv": false
}
EDIT (I suppose this is important): Here is the mapping for the WTSImage and WTSRecord:
RKEntityMapping *recordMapping = [RKEntityMapping mappingForEntityForName:#"WTSRecord" inManagedObjectStore:self.managedObjectStore];
[recordMapping addAttributeMappingsFromDictionary:#{
#"id":#"dbId",
#"externalId":#"extId",
#"groupId":#"groupId",
#"application": #"application",
#"description": #"desc",
#"priv": #"priv",
#"locked": #"locked",
#"createBy": #"createBy",
#"createDt": #"createDt",
#"modifyBy": #"modifyBy",
#"modifyDt": #"modifyDt",
}];
recordMapping.identificationAttributes = #[#"dbId"];
//image mapping
RKEntityMapping *imageMapping = [RKEntityMapping mappingForEntityForName:#"WTSImage" inManagedObjectStore:self.managedObjectStore];
[imageMapping addAttributeMappingsFromDictionary:#{
#"id": #"dbId",
#"status": #"status",
#"type": #"type",
#"format": #"format",
#"width": #"width",
#"height": #"height",
#"image": #"base64Image"
}];
imageMapping.identificationAttributes = #[#"dbId"];
[recordMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"images" toKeyPath:#"images" withMapping:imageMapping]];
The following code is where I create the NSManagedObjects and call [RKObjectManager postObject:path:parameters:success:failure::
WTSRecord *record = [NSEntityDescription insertNewObjectForEntityForName:#"WTSRecord" inManagedObjectContext:self.managedObjectContext];
record.createBy = #"welcomed";
record.createDt = [NSDate date];
record.application = kWTSApplicationIdentify;
record.groupId = #"5";
WTSImage *image = [NSEntityDescription insertNewObjectForEntityForName:#"WTSImage" inManagedObjectContext:self.managedObjectContext];
image.height = [NSNumber numberWithFloat:mugshot.size.height];
image.width = [NSNumber numberWithFloat:mugshot.size.width];
image.imageData = UIImageJPEGRepresentation(imageData, 1.0);
image.type = kWTSCaptureTypeMugshot;
image.format = kWTSCaptureFormatJpeg;
image.status = kWTSCaptureStatusCaptured;
image.record = record;
[record addImagesObject:image];
RKObjectManager *manager = [RKObjectManager sharedManager];
[manager postObject:record path:#"records" parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error Sending Record" message:error.localizedDescription delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
}];
When the success block is invoked, I check the sqlite database and there is 1 WTSRecord inserted and 2 WTSImages. One of the WTSImages has the correct FK to the WTSRecord and the PK from the database, while the other one appears to be orphaned (dbId and the FK to WTSRecord are not set).
Here is a link to the RestKit and Core Data trace logs: https://dl.dropboxusercontent.com/u/466390/restkit2.txt
Hopefully someone can help! Thanks!
EDIT After some more searching, I found this page: https://github.com/RestKit/RestKit/issues/1228
Do I have to create identification elements on the client side using UUIDs before POSTing them to a REST service? Will RestKit not be able to map request objects back to objects that were already created in the object store without first setting an identification attribute on the client side?
For the posted object, RestKit understands how to update that item with the response data, but that doesn't apply to the relationship contents. Technically it could be coded, but it isn't currently.
If you need the object in the relationship after the mapping to be the same object that you create then you have an issue. If you don't mind it being a different object then the issue is just to delete the duplicate...
Duplicate deletion:
Fetch request blocks aren't used when processing a POST response so you'd need to fetch the dupe and delete it manually. I'm going to assume that any image with a nil relationship to a record is a dupe so it's a relatively simple fetch to execute.
Related
I am implementing a simple iOS app that consumes reddit feeds and I need to show the feed comments. I am using RESTKit to do the ORM part and I am having an annoyance with mapping the comments.
Reddit comments can have replies and the replies can have replies and so on. It's a tree of comments. This is no big deal. The problem is that among the children nodes there might exist one that's different (used to sign that there are more comments to load on that particular thread).
Here's a JSON example:
"replies":{
"data":
{
"after": null,
"before": null,
"children":
[
{
"data":
{
"approved_by": null,
"author": "InstaRamen",
"author_flair_css_class": null,
"author_flair_text": null,
"banned_by": null,
"body": "that or he counted it all wrong",
"body_html":"<div class=\"md\"><p>that or he counted it all wrong</p>\n</div>",
"controversiality": 0,
"created": 1405093511,
"created_utc": 1405064711,
"distinguished": null,
"downs": 0,
"edited": false,
"gilded": 0
"id": "ciubfdk",
"likes": null,
"link_id": "t3_2ae8oa",
"name": "t1_ciubfdk",
"num_reports": null,
"parent_id": "t1_ciuao19",
"replies":
{
"data":
{
"after": null,
"before": null,
"children":
[
{
"data":
{
"approved_by": null,
"author": "Everlasting",
"author_flair_css_class": null,
"author_flair_text": null,
"banned_by": null,
"body": "3, sir.",
"body_html":"<div class=\"md\"><p>3, sir.</p>\n</div>",
"controversiality": 0,
"created": 1405093986,
"created_utc": 1405065186,
"distinguished": null,
"downs": 0,
"edited": false,
"gilded": 0,
"id": "ciubj8h",
"likes": null,
"link_id": "t3_2ae8oa",
"name": "t1_ciubj8h",
"num_reports": null,
"parent_id": "t1_ciubfdk",
"replies":
{
"data":
{
"after": null,
"before": null,
"children":
[
{
"data":
{
"children":
[
"ciublgq"
],
"count": 0,
"id":"ciublgq",
"name":"t1_ciublgq",
"parent_id": "t1_ciubj8h"
},
"kind": "more"
}
],
"modhash": ""
},
"kind": "Listing"
},...
So, you can see that the typical comment replies has a structure of
replies
|-data
|-children
|-data
|- #comment key/values
But the "Load more" item has a structure of:
replies
|-data
|-children
|-data
|-children
|-more loading info...
My DataObject is:
#interface RedditComment : NSObject
#property (nonatomic, copy) NSString *author;
#property (nonatomic, readwrite) NSInteger createdUTC;
#property (nonatomic, readwrite) NSInteger score;
#property (nonatomic, copy) NSString *subreddit;
#property (nonatomic, copy) NSString *subredditId;
#property (nonatomic, readwrite) NSInteger ups;
#property (nonatomic, copy) NSString *body;
#property (nonatomic, copy) NSArray *replies;
+ (RKObjectMapping*)restKitMapping;
#end
And my mapping implementation is:
#implementation RedditComment
+ (RKObjectMapping*)restKitMapping;
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[self class]];
[mapping addAttributeMappingsFromDictionary:#{
#"author": #"author",
#"created_utc": #"createdUTC",
#"score": #"score",
#"subreddit": #"subreddit",
#"subreddit_id": #"subredditId",
#"body": #"body",
#"ups": #"ups",
}];
[mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"replies" toKeyPath:#"replies" withMapping:mapping]];
return mapping;
}
#end
This yields an error:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFConstantString 0x1daa0d0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key body.'
This is because the "Load more" node doesn't have a body key (it's missing other keys as well, but this is the first one that RESTKit complains about).
I've tried to use a dynamic mapping block to change the mapping in runtime but couldn't figure out how to isolate the problematic node (i.e. the "Load More").
My question is how can I distinguish between both possible values in "replies" JSON nodes?
Can I have two mappings for the same partial key?
That is a mapping for replies > data > children > data and another to replies > data > children > data > children. If so how to do it?
Or is there anything wrong in my line of thinking?
Thanks in advance to everyone that is able to help or that simply take the time to read... ;-)
EDIT
including response descriptor
As requested on comments here's the RKResponseDescriptor:
RKResponseDescriptor *commentResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:[PPRedditComment restKitMapping]
method:RKRequestMethodAny
pathPattern:nil
keyPath:#"data"
statusCodes:nil];
EDIT #2
including a "good comment" node to show what I am interested to map to my DO
"data":
{
"approved_by": null,
"author": "PerimeterBlue",
"author_flair_css_class":null,
"author_flair_text":null,
"banned_by":null,
"body": "This actually looks really cool.",
"body_html": "<div class=\"md\"><p>This actually looks really cool.</p>\n</div>",
"controversiality":0,
"created": 1405095679,
"created_utc": 1405066879,
"distinguished":null,
"downs": 0,
"edited": false,
"gilded": 0,
"id": "ciubwld",
"likes":null,
"link_id": "t3_2aelr4",
"name": "t1_ciubwld",
"num_reports": null,
"parent_id": "t3_2aelr4",
"replies": {...},
"saved": false,
"score": 48,
"score_hidden": false,
"subreddit": "gaming",
"subreddit_id": "t5_2qh03",
"ups": 48
}
Ok, apparently got the issue. Wain was right: it was a path related issue.
There were certain nodes which was supposed to contain dictionaries and sometimes contained strings. In the way the paths were defined this caused an non-existent selector (the property from DO) to be called on the NSString class. Since it didn't contain it, it went BOOM!
Mapping was corrected to this:
+ (RKObjectMapping*)restKitMapping;
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[self class]];
[mapping addAttributeMappingsFromDictionary:#{
#"author": #"author",
#"created_utc": #"createdUTC",
#"score": #"score",
#"subreddit": #"subreddit",
#"subreddit_id": #"subredditId",
#"body": #"body",
#"ups": #"ups",
}];
RKDynamicMapping *dynamicMapping = [RKDynamicMapping new];
[dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) {
RKObjectMapping *result = mapping;
// When node is empty string don't map...
if([representation isKindOfClass:[NSString class]] && [representation isEqualToString:#""])
{
result = nil;
}
return result;
}];
[mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"replies"
toKeyPath:#"replies"
withMapping:dynamicMapping]];
return mapping;
}
Response descriptor corrected to:
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:[PPRedditFeedCollection restKitMapping]
method:RKRequestMethodAny
pathPattern:nil
keyPath:#"data"
statusCodes:nil];
Hope this helps someone! :-)
I'm looking to get all of my Foursquare Lists into Core Data. I'd like to use Restkit to accomplish this. The structure of the /v2/users/self/lists response is:
"response": {
"lists": {
"count": 8,
"groups": [
{
"type": "created",
"name": "Lists You've Created",
"count": 6,
"items": [
{
"id": "13250/todos",
"name": "My to-do list", ...
}
{
"id": "13251/something",
"name": "Some List", ...
},
{
"id": "13252/somethingelse",
"name": "Some Other List", ...
}
]
},
{
"type": "followed",
"name": "Lists You've Saved",
"count": 1,
"items": [
{
"id": "5105e3cae4b0e721ca7b400a",
"name": "Portland's Best Coffee - 2012", ...
}
{
...
}
]
}
]
}
As you can see there are 2 lists under the keyPath response.lists.groups. Ultimately I'd like to merge those 2 lists into 1, but I'd be happy with getting 2 separate lists.
I've set up my mappings as follows:
RKEntityMapping* listMapping = [RKEntityMapping mappingForEntityForName:[FOFSList entityName]
inManagedObjectStore:objectManager.managedObjectStore];
[listMapping addAttributeMappingsFromDictionary:#{
#"id": #"listID",
#"title": #"name",
#"description": #"desc",
#"user": #"user",
#"following": #"following",
#"collaborative": #"collaborative",
#"canonicalUrl": #"canonicalUrl",
#"venueCount": #"venueCount",
#"visitedCount": #"visitedCount"
}];
RKDynamicMapping *dynamicMapping = [RKDynamicMapping new];
[listMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:nil
toKeyPath:#"items"
withMapping:dynamicMapping]];
RKResponseDescriptor *listResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:listMapping
method:RKRequestMethodGET
pathPattern:nil
keyPath:#"response.lists.groups"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:listResponseDescriptor];
[dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) {
if ([[representation valueForKey:#"type"] isEqualToString:#"created"]) {
return listMapping;
} else if ([[representation valueForKey:#"type"] isEqualToString:#"followed"]) {
return listMapping;
}
return nil;
}];
listMapping.identificationAttributes = #[ #"listID" ];
I end up with an error:
* Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key propertyMappings.'
Am I supposed to be using RKDynamicMappings? Is there some trick that I'm missing for parsing a response that is styled like this?
For those that are interested, I got a little bit creative with the RKResponseDescriptor
RKResponseDescriptor *listResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:listMapping
method:RKRequestMethodGET
pathPattern:nil
keyPath:#"response.lists.groups.#distinctUnionOfArrays.items"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
See the collection operation #distinctUinionOfArrays was ultimately what got me what I needed. It makes a union of the 2 groups arrays, then I grab the items key from each of the objects in the union of the arrays.
At the moment the dynamic mapping is serving as a filter on the type. That's fine if it's what you want and is the correct way to achieve the filter. But, you're applying that mapping in the wrong way.
The dynamic mapping should be supplies as the mapping for the response descriptor. It analyses the incoming object and returns the appropriate mapping to apply.
You need a new, non-dynamic, mapping to handle the nested items.
Your other question about merging can't be handled during the mapping, but it could be done by adding a method to the destination class which is called with the mapped array and it merges with an existing array and pushes the merged result into the true instance variable. The mapping destination would be the method instead of the instance variable.
I have two entities: Contact and Listing. A User can mark a specific Listing as its Favourite.
I set up the Relationships correctly and I'm able to add a Listing as a Favourite for the User.
The problem for me is the API side. It gives me only the ids of the relationship: contact_id and the listing_id.
How can I set up RestKit to map the relationships defined in the Favourites object I get from the server which only gives me the two object ids of a contact and a listing?
Restkit Version: 0.20.3
JSON for Favorite:
{
"contact_id": "1",
"created_at": "2013-11-06T15:02:21.056Z",
"id": "2",
"listing_id": "3",
"updated_at": "2013-11-06T15:02:21.056Z"
}
JSON for Contact:
{
"id": 1,
"favorites": [
{
"contact_id": "1",
"created_at": "2013-11-06T15:02:21.056Z",
"id": "2",
"listing_id": "3",
"updated_at": "2013-11-06T15:02:21.056Z"
}
],
"first_name": Max,
}
//////////
// Contact
//////////
RKEntityMapping *contactMapping = [RKEntityMapping mappingForEntityForName:#"PBContact" inManagedObjectStore:managedObjectStore];
contactMapping.identificationAttributes = #[ #"object_id" ];
[contactMapping addAttributeMappingsFromDictionary:#{ #"id": #"object_id" }];
[contactMapping addAttributeMappingsFromArray:#[ #"given_name", #"address", #"birthday",
#"city", #"company_name", #"country",
#"email", #"family_name", #"gender",
#"mobile_number", #"note", #"phone_number",
#"state", #"zip_code", #"created_at", #"updated_at" ]];
//////////
// Listing
//////////
RKEntityMapping *listingMapping = [RKEntityMapping mappingForEntityForName:#"PBListing" inManagedObjectStore:managedObjectStore];
listingMapping.identificationAttributes = #[ #"object_id" ];
[listingMapping addAttributeMappingsFromDictionary:#{ #"id": #"object_id", #"description": #"descriptions" }];
[listingMapping addAttributeMappingsFromArray:#[ #"address", #"name", #"bathrooms", #"bedrooms",
#"city", #"country", #"price", #"title",
#"zip_code", #"latitute", #"longitude", #"status",
#"total_area", #"year_built", #"property_type", #"listing_type",
#"size", #"lot_size", #"parking_spaces", #"view",
#"state", #"note", #"monthly_rent", #"created_at", #"updated_at" ]];
//////////
// Relationships
//////////
[contactMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"favorites" toKeyPath:#"favoriteListings" withMapping:listingMapping]];
[listingMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"favorites" toKeyPath:#"prospects" withMapping:contactMapping]];
Response Descriptor
RKResponseDescriptor *contactResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:contactMapping
method:RKRequestMethodAny
pathPattern:#"api/v1/contacts"
keyPath:nil
statusCodes:statusCodes];
You don't need (and should delete):
[listingMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"favorites" toKeyPath:#"prospects" withMapping:contactMapping]];
because the relationship is already being configured from the other side on the contactMapping. There shouldn't need to be any other changes by the look of it.
It isn't entirely clear why there are 3 ids:
"contact_id": "1",
"id": "2",
"listing_id": "3",
I'm assuming that contact_id always matches the outer contact that the favourites are nested in and isn't required. And that id is the useful piece of information (so your mapping is correct).
In your Core Data model, favoriteListings and prospects should be the inverse of each other.
Based on your latest comments, I think you need to create a new and different listing mapping. This mapping only contains #"listing_id" : #"object_id". This will either link to an existing listing entity or create a placeholder that can be filled in later. This is the mapping that you should use as the relationship on contactMapping.
I have to map this structure, but I don't know how to identify the "br.myservice.com" part, because it changes and I cannot build a regular RKObjectMapping, as it uses fixed strings.
{ "objects": {
"br.myservice.com": {
"url": "http://br.myservice.com",
"name": "Brazil",
"flag": "br",
"countries": ["br"]
},
"us.myservice.com": {
"url": "http://us.myservice.com",
"name": "United States",
"flag": "us",
"countries": ["us"]
}
}
You need to use addAttributeMappingFromKeyOfRepresentationToAttribute: like so:
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[MyObject class]];
mapping.forceCollectionMapping = YES;
[mapping addAttributeMappingFromKeyOfRepresentationToAttribute:#"host"];
[mapping addAttributeMappingsFromDictionary:#{
#"(host).url": #"url",
#"(host).name": #"name",
... and so on ....
}];
Assuming that the keys are unknown to you at the time of writing the code:
You would need to create a dynamic mapping (RKDynamicMapping) with a block which inspects the received JSON dictionary and creates (and returns) a custom object mapping on the fly.
If you know the keys, just add them all and any that are appropriate will be used at runtime.
I am trying to incorporate RestKit v.20 into my iOS App. I get the proper mapping result from RKObjectRequestOperation but I can't figure out a good way to put it into a NSDictionary. I am returned an array of objects of hex values. I can get the values of all the specific items I want by using objectAtIndex: but I need a place that has all the values in its string format, not hex. I can only change from hex to string by setting the associated class equal to a single array object. I want a general list so I can predicate it for use in search.
Here is the JSON Data:
{
"carsInShop": [
{
"car": {
"id": "1",
"name": "Mercedes",
"year": "2000"
}
},
{
"car": {
"id": "2",
"name": "BMW",
"year": "2004"
}
},
{
"car": {
"id": "3",
"name": "Audi",
"year": "2001"
}
},
{
"car": {
"id": "4",
"name": "Lexus",
"year": "2011"
}
},
{
"car": {
"id": "5",
"name": "Toyota",
"year": "2006"
}
},
],
"count": 5
}
Here is where I am having code problems:
NSURL *URL = [NSURL URLWithString: #"http://localhost:8888/testphpread/index.php"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
RKObjectRequestOperation *objectRequestOperation = [[RKObjectRequestOperation alloc] initWithRequest:request
responseDescriptors:#[responseDescriptor]];
[objectRequestOperation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
RKLogInfo(#"CarsInShopArray: %#", mappingResult.array);
carsInShopArray = [mappingResult array];
CarsInShop* carsInShop = [carsInShopArray objectAtIndex:0];
NSLog(#"Loaded Car ID #%# -> Name: %#, Year: %#", carsInShop.car.id, carsInShop.car.name, carsInShop.car.year);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
RKLogError(#"Operation failed with error: %#", error);
}];
This is what I get from NSLog:
CarsInShopArray: (
"CarsInShop: 0xa26b0f0",
"CarsInShop: 0xa46b2f0",
"CarsInShop: 0xa46af20",
"CarsInShop: 0xa46b7a0",
"CarsInShop: 0xa46bd90",
)
Loaded Car ID #1 -> Name: Mercedes, Year: 2000
So you see I can get individual items, but I need to access the dictionary as a whole, for use in searching.
You have a custom class CarsInShop, I guess it inherits from NSObject. You don't override the description method so you get the default description when you log the instance. The default is the class name and its pointer. That's why you get "CarsInShop: 0xa26b0f0". It says nothing about the contents of the instance.
When you say: "I need to access the dictionary as a whole", you don't have a dictionary. You have an array of custom objects. You can still use KVC to search through the items in the array (based on key path by the look of your log statement).
You can get an array of the cars by doing:
NSArray *cars = [carsInShopArray valueForKey:#"car"];