Restkit: including field from request into object mapping - core-data

I am making rest call as this
GET /entities?parent=123
[
{id:1, name:"name 1"},
{id:2, name:"name 2"}
]
My entity defination have following fields
id
name
parent
The problem is that parent field is not coming in the response, It is in the request. How can I save parent field from request to coredata?
I have tried to search these things with no luck.
I have tried looking for some transformers which could transform response before being processed by restkit but could not find anything.
I have seen Metadata Mapping in RKObjectRequestOperation but could not figure out if/how this can be used.
thanks
EDIT
Answer provided below works only if that is used with object manager. So the following works
RKDynamicMapping *learningObjectMapping = [MTAbstractLearningObject map];//this contains metadata mapping for #metadata.routing.parameters.entityId
RKResponseDescriptor* learningObjRd = [RKResponseDescriptor responseDescriptorWithMapping:learningObjectMapping
method:RKRequestMethodGET pathPattern:#"entity/:entityId" keyPath:#"objects"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[self.objectManager addResponseDescriptor:learningObjRd];
[self.objectManager.router.routeSet addRoute:[RKRoute routeWithName:#"learningObjects" pathPattern:#"entity/:id" method:RKRequestMethodGET]];
If object manager is constructed as shown above, and requests are made as shown below.
[self.objectManager getObjectsAtPathForRouteNamed:#"learningObjects" object:entity parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"API: Success got learning objects");
}failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"API: Failed to get learning objects");
}];
Then it will work.

You can use metadata, it includes a dictionary of query parameters that you can access. Your mapping will contain
#"#metadata.query.parameters.parent": #"parent"

Related

Need to Create Multiple Identical Entities using RestKit

I'm downloading often identical data on multiple calls to different URLs in RESTKit. The first call maps fine, but the subsequent calls then replace the first call's entities.
The behaviour that I want is to have the objects unique to within their parent entity, so even if the data looks the same I still want a new object to be created, but at the moment it looks like RESTKit wants them to be unique across the entire database. There's no unique key to do this in the data I'm downloading, the objects are literally identical. Below is the code I'm using to create the operation. How do I set this up to allow duplicates?
NSMutableURLRequest *request = [self requestWithURL:URL];
[request setHTTPMethod:#"GET"];
RKHTTPRequestOperation *requestOperation = [[RKHTTPRequestOperation alloc]initWithRequest:request];
RKResponseDescriptor *responseDescriptor = [INKResponseDescriptorFactory journeySegmentDescriptorForPath:URL.path inStore:[RKObjectManager sharedManager].managedObjectStore];
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:requestOperation
responseDescriptors:#[responseDescriptor]];
operation.managedObjectContext = [self.objectManager context];
operation.managedObjectCache = self.objectManager.managedObjectStore.managedObjectCache;
operation.savesToPersistentStore = NO;
[operation setCompletionBlockWithSuccess: ^(RKObjectRequestOperation *requestOperation, RKMappingResult *mappingResult)
{
success();
} failure: ^(RKObjectRequestOperation *requestOperation, NSError *error) {
failure(error);
}];
[self.objectManager enqueueObjectRequestOperation:operation];
Figured out a way to do this.
Each time I'm updating the data, even though the entities I get contain identical data, I'm retrieving them from a different url.
RESTKit allows you to retrieve metadata from you calls and map this into your managed objects properties.
So what I've done is map the URL I used for the request into a property and used that as well as an identifier, which is unique within this call's returned objects, to make objects which will be unique throughout the database.
So my mapping now looks like this:
RKEntityMapping *seatMapping = [RKEntityMapping mappingForEntityForName:#"Seat" inManagedObjectStore:store];
[seatMapping addAttributeMappingsFromDictionary:#{ #"designator" : #"designator",
#"status" : #"status",
#"#metadata.HTTP.request.URL" : #"requestURL"}];
seatMapping.identificationAttributes = #[#"requestURL", #"designator"];

RESKIt: No Response Descriptor

I'm using the code below to post a managedObject. From the server, I'm not getting anything back, except 200, so there is no mapping for response object.
[self.objectManager postObject:invitation path:#"/path" parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"success");
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failure");
}];
Is there a dummy format I can use for RKResponseDescriptor ?
RKResponseDescriptor *responseDescriptor = nil; //causes a crash
There is no 'dummy' RKResponseDescriptor instance, you must explicitly define what you want RestKit to do with the response data.
Your response descriptor can be partial, so it just maps one field to prove things are working.
Or, you could use a dynamic mapping to read the keys in the incoming data and create a mapping to NSMutableDictionary which includes all of the keys.
Both of these things are only really useful for testing, but may assist you.
If the response actually has no body data and you just want to monitor for success then you can either use AFNetworking instead, or create the response descriptor with a mapping to NSMutableDictionary and with no keys. RestKit should pick that up and it should act as a 'dummy' for your purposes. It will create a dictionary instance that is never used, but you can just ignore that.
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[NSMutableDictionary class]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping
method:RKRequestMethodPOST
pathPattern:#"/path"
keyPath:nil
statusCodes:[self statusCodeSuccess]];

Importing with MagicalRecord + AFNetworking

I'm using AFNetworking and MagicalRecord (the current develop branch) and I'm trying to figure out how to import a lot of objects which are dependent on each other. Each resource/entity has multiple pages worth of downloads. I have a class managing the downloads for a given entity and saving them using MagicalDataImport (which has been amazing).
I believe my issue is that the imports aren't happening on the same thread. So I think what is happening is:
In one thread, EntityA is getting saved properly and propagated to the parent entity.
Then in another thread, EntityB is being saved, and along with it it's relationship to EntityA is built. That means a blank (fault?) object is being created. Then when it gets propagated to the parent entity, I believe EntityA is overwriting the EntityA that is there. Thus I'm left with some objects that don't have all of the attributes.
At least, I think that is what is happening. What I'm seeing via the UI is actually that the relationships between entities aren't always built correctly.
My end goal is to get the entire download/import process to be done in the background, not effecting the UI at all.
Here is my AFJSONRequest:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self saveResources:[JSON objectForKey:#"data"]];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON)
{
DLog(#"%#",error.userInfo);
[self.webService command:self didFail:error.localizedDescription];
}];
[operation setQueuePriority:self.priority];
And it calls saveResources::
- (void)saveResources:(NSArray*)resources {
BOOL stopDownloads = [self stopDownloadsBasedOnDate:resources];
if ([resources count] > 0 && !stopDownloads){
self.offset = #([offset intValue] + [resources count]);
[self send];
}
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *blockLocalContext) {
[self.classRef MR_importFromArray:resources inContext:blockLocalContext];
} completion:^(BOOL success, NSError *error) {
if (error){
// ... handle errors
}
else {
// ... handle callbacks
}
}];
}
This kicks off another download ([self send]) and then saves the objects.
I know by default AFNetworking calls the callback in the main queue, and I've tried setting the SuccessCallbackQueue/FailureCallbackQueue to my background thread, but that doesn't seem to solve all the issues, I still have some relationships going to faulted objects, though I think I do need to do that to keep everything going in a background thread.
Is there anything else I need to call in order to properly propagate these changes to the main context? Or is there a different way I need to set this up in order to make sure that all the objects are saved correctly and the relationships are properly built?
Update
I've rewritten the issue to try to give more clarification to the issues.
Update
If you need more code I created a gist with (I believe) everything.
I ended up having this exact same issue a few days ago. My issue was I had received a customer record from my API with AFNetworking. That customer could have pets, but at this point I didn't have the petTypes to correspond to the customers pet record.
What I did to resolve this was create a transformable attribute with an NSArray which would temporarly store my pets until my petTypes were imported. Upon the importation of petTypes I then triggered an NSNotificationCenter postNotification (or you can just do the pet import in the completion).
I enumerated through the temporary transformable attribute that stored my pet records and then associated the with the petType
Also I see you are doing your import inside of a save handler. This is not needed. Doing your MR_importFromArray will save automatically. If you are not using an MR_import method then you would use the saveToPersistentStore methods.
One thing is I don't see where you are associating the relationships. Is EntityB's relationship to EntityA being sent over via JSON with the EntityA objecting being in EntityB?
If so then this is where the relationship is getting messed up as it is creating / overwriting the existing EntityA for the one provided in EntityB. My recommendation would be to do something like this.
NSArray *petFactors = [responseObject valueForKeyPath:#"details.items"];
NSManagedObjectContext *currentContext = [NSManagedObjectContext MR_context];
Pets *pet = [Pets MR_findFirstByAttribute:#"id" withValue:petId inContext:currentContext];
pet.petFactors = nil;
for (id factor in petFactors) {
[pet addPetFactorsObject:[PetFactors MR_findFirstByAttribute:#"id" withValue:[factor valueForKey:#"factorId"]]];
}
[currentContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"SAVED PET FACTORS");
[[NSNotificationCenter defaultCenter] postNotificationName:kPetFactorsSavedSuccessfully object:nil];
} else {
NSLog(#"Error: %#", error);
}
}];
I'm putting this as an answer, though I'm not 100% sure if this is your issue or not. I think the issue stems from your localContext. Here is a sample web request method from an app we wrote that uses data importing, you may be able to use it as an example to get yours working.
Note that the AFNetworking performs its completion block on the main thread, then the MagicalRecord saveInBackground method switches back to a background thread to do the importing and processing, then the final MR completion block performs the handler block on the main thread again. The localContext that's used to import is created/managed by the saveInBackground method. Once that method is complete the context is saved and merged with the app's main context and all the data can then be accessed.
- (void)listWithCompletionHandler:(void (^)(BOOL success))handler{
[[MyAPIClient sharedClient] getPath:#"list.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject){
NSString *statusString = [responseObject objectForKey:#"status"];
// Handle an error response
if(![statusString isKindOfClass:[NSString class]] || ![statusString isEqualToString:#"success"]){
// Request failure
NSLog(#"List Request Error: %#", statusString);
NSLog(#"%#", [responseObject objectForKey:#"message"]);
if(handler)
handler(NO);
return;
}
NSArray *itemsArray = [responseObject objectForKey:#"items"];
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext){
// Load into internal database
NSArray *fetchedItems = [Item importFromArray:itemsArray inContext:localContext];
NSLog(#"Loaded %d Items", [fetchedItems count]);
} completion:^{
if(handler)
handler(YES);
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
NSLog(#"Fail: %#", error);
if(handler)
handler(NO);
}];
}

Restkit-Core data: Duplicate objects in ObjectStore and Backend when post

I have a problem when using Restkit (0.10.3)+core data on an iOS 5.0 app. I have a entity ToDo mapped as follow:
RKManagedObjectMapping* map = [self mappingInObjectStore];
[map mapKeyPathsToAttributes:
#"id" , #"todoID" ,
#"message" , #"detail" ,
#"created_at" , #"createdAt" ,
#"updated_at" , #"updatedAt" ,
#"public_location",#"publicLocation",
nil];
map.primaryKeyAttribute = #"todoID";
[map mapKeyPath:#"location" toRelationship:#"location" withMapping:[TDLocation mapping]];
TDLocation is a simple entity with long,lat, ...
I created an TODO object using [TDTodo object] method and set all needed data, with the exception of todoID (because it will filled by the backend). Later I do the post:
[RKObjectManager sharedManager] postObject:todo usingBlock:^(RKObjectLoader *loader) {
loader.onDidLoadResponse= ^(RKResponse *response){
if (response.isOK || response.isCreated) {
id parsedData = [response parsedBody:nil];
todo.todoID = [NSNumber numberWithInt: [[parsedData valueForKeyPath:#"acceptedTodo.id"] intValue]]; //just get the assigned id from backend
}
[loader cancel];
};
}];
Up to here, everything is ok. If I do [TDTodo allObjects], I get the posted todo with the assigned id. But later I made a get of todo list and I retrieve two more objects TDTodo containing the same info that the posted todo, so I do [TDTodo allObjects]
{todoID: 1, ...}//posted todo
{todoID: 1, ...} // from the backend with the same info and id that the posted todo
{todoID: 2, ...} // from the backend with the same info that the posted todo, but diff id.
In conclusion, I have 3 (three) duplicated instance of the same object in the object Store. I searched for this problem, but I couldn't find a working solution.
Thanks in advance
UPDATE 1
I solved one part of the problem: "duplicate creation of entities in the back end". This was produce by trying to cancel the response/loader, in concrete this instruction:
[loader cancel];
Also I tried:
[response.request cancel];
[response.request.delegate = nil;
but this produces also the duplicates. So removing this instructions fix that part of the problem.
However, there is still the duplicates in the objectStore:
{todoID: 1, ...}//posted todo`
{todoID: 1, ...} // from the backend with the same info and id that the posted todo`\
How I can solve this? I tried all solution that I found in the web :-(
Cheers.

Restkit.Delete on server sided changes

I use RestKit to receive data from my web server and storing it in Core Data.
Everything works fine, also the object mapping.
But now I have a question/problem.
I want to sync my device with the data from the web server.
The way with new or updated objects is clear, because RestKit controls it for me. But how to delete objects in Core Data? There is a way using the DELETE method. But what is, if some objects were deleted on the server?
I want to make a GET request with the timestamp of the last sync to receive all data changes (delta).
new objects (OK)
modified objects (OK)
deleted objects (How?)
How to delete objects in Core Data with JSON payload from the server.
Is there a possibility to prevent the save when a delete attribute of an object in JSON is set? Is there a way to delete this object from Core Data.
That works for me. I'm using RestKit 0.10.2
#pragma Mark - RKObjectLoaderDelegate
- (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects {
//Select all users in Core Data
NSArray* allUsers = [User findAll];
for (User *userRecord in allUsers) {
// check each user into objects loaded now. If not contains, delete him
if (NO == [objects containsObject:userRecord]) {
[[[[RKObjectManager sharedManager] objectStore] managedObjectContextForCurrentThread] deleteObject:userRecord];
}
}
//Saving changes in MOC
NSError* error = nil;
[[[[RKObjectManager sharedManager] objectStore] managedObjectContextForCurrentThread] save:&error];
if (nil != error) {
NSLog(#"################# Error %#", error);
}
}
Solution n°1: The removed attribute
Imho it wouldn't be a good solution to remove every local objects that doesn't appear in the request reponse. So I just add an attribute removed (not deleted because it's reserved by Core Data) and only display Object[removed=FALSE].
In a fetch request, it would be:
removed == FALSE
Solution n°2: what-should-I-delete?
You could make a simple page in your API that would indicate what to delete on your application. For instance:
[
{'type': 'post', 'id': 5},
{'type': 'comment', 'id': 10},
]
Then, you would fetch objects by postID or commentID and delete them from the store. You'll have to find a way to confirm the deletion to the server, as the list will grow.

Resources