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"];
Related
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"
I would like to use the item that has been just been saved in the completion block of Magical Record saveWithBlock method. For example:
//Get the ID of an existing NSManagedObject to use in the save block (if it exists)
NSManagedObjectID *objectRef = [self.object objectID];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
//This method either loads an existing object and makes changes or creates a new entity in localContext
NSManagedObject *itemToSave = [self prepareItemInContext:localContext WithID: objectRef];
} completion:^(BOOL success, NSError *error) {
if (success) {
//here I want to get at the object 'itemToSave' that was either created in the save block (with a new objectID) or updated (with the ID objectRef)
Well, you need to have a reference to your external context to load the object with that ID:
NSManagedObjectContext *outsideContext = //...
NSManagedObjectID *objectID = //...
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
} completion:^(BOOL success, NSError *error) {
NSManagedObject *newlySavedObject = [outsideContext existingObjectWithID:objectID];
//...do stuff here
}];
Generally, however, I would discourage this usage. I would instead recommend keeping any predicates or means of reloading your data set handy, and dump and refetch fresh data from the store. This will give you proper object references. Another, more precise way of updating objects in other contexts is to listen to the NSManagedObjectContextDidSaveNotification and merge this updates into your context. From there, your data will be "refreshed" and as long as you're KVO'ing a property, or using a NSFetchedResultsController with a delegate, your updates will propagate to the UI (or other destination).
Either just use self.object or, if you create a new object and insert it (presumably because objectRef is nil) then you should get the corresponding new object from the main thread context and use that.
How you do that shuffle is the interesting part. It isn't exactly clear why you're using a background context at the moment so you can also consider changing that, which removes all of the complexity.
If you need to keep the background context then you need to decide on how to get that data back to the main thread. Generally, you could use performBlockAndWait: inside your current block to get the new object from the main context and then store it into a property on your class so you can use it in the completion block. This would be setting the self.object property.
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]];
What are the delete semantics for RestKit when using it with Core Data?
For example, assume I correctly set a primaryKeyAttribute in RestKit for Organization entities. If I do a GET on, say, /organizations/ I get entries for /organizations/1/, /organizations/2/, and /organizations/3/ back. Let's say I do a GET on /organizations/ a bit later and only get entries for /organizations/1/ and /organizations/3/ back. `/organizations/2/ has been deleted on the server.
I would expect RestKit to delete my Core Data record for /organizations/2/. Is this what RestKit does or do I have to implement this behavior? Does this change in any way if I am using the reboot-networking-layer branch? Are there any settings in RestKit I should be aware of that affect this behavior?
You need to implement FetchRequests in order for those objects to be deleted. Here is a simple example:
[objectManager addFetchRequestBlock:^NSFetchRequest *(NSURL *URL) {
RKPathMatcher *pathMatcher = [RKPathMatcher pathMatcherWithPattern:#"/organizations"];
NSDictionary *argsDict = nil;
BOOL match = [pathMatcher matchesPath:[URL relativePath] tokenizeQueryStrings:NO parsedArguments:&argsDict];
if (match) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Organization"];
return fetchRequest;
}
return nil;
}];
I do this in the ApplicationDelegate(the same place I setup RestKit). After you do it there, all other calls will automatically delete orphaned objects.
I have an entity called Practice and I use a View Controller called SelectorViewController to select one of the practices, selectedPractice. I then return selectedPractice to a view Controller called RegularViewController where I display some of the selectedPractice attributes. All of this works fine. However the app has a number of other View Controllers which can be reached by modal segues from instances of RegularViewController. As a result, if I leave and then come back to RegularViewController, selectedPractice is reset as null. I would also like to save selectedPractice so that it is available at app initialisation if it has previously been set in SelectorViewController. How do I achieve this by making selectedPractice persistent across the app, and available at runtime?
Regards
Thanks to the post above, which was great, I managed to sort it. Here is my code, which may be very clumsy, but it works.
Firstly, as I loaded the fetchedObjects into a PickerView in SelectorView Controller, I set an attribute "isSelectedPractice" to "NO" with the following code:
for (Practice *fetchedPractice in [self.fetchedResultsController fetchedObjects]) {
[fetchedPractice setValue:#"NO" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
I then identified for the selected Practice:
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
Practice *practice = [[self.fetchedResultsController fetchedObjects] objectAtIndex:row];
self.selectedPractice = practice;
NSLog(#"The '%#' practice was selected using the picker", self.selectedPractice.name);
}
as the view Segue'd back to RegularViewController I set the isSelectedPractice attribute for selectedPractice to YES. I kept it this late as I didn't want more than one selection in the PickerView to result in multiple objects with isSelectedPractice YES.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"SavedPractice Segue"])
{
[self.selectedPractice setValue:#"YES" forKey:#"isSelectedPractice"];
[self.managedObjectContext save:nil];
NSLog(#"Setting SelectedPractice as '%#' in RegularViewController with isSelectedPractice as '%#'",self.selectedPractice.name,self.selectedPractice.isSelectedPractice );
RegularViewController *rvc= segue.destinationViewController;
rvc.delegate = self;
rvc.selectedPractice = self.selectedPractice;
}
else {
NSLog(#"Unidentified Segue Attempted!");
}
}
I then set the following Predicate in the setupFetchedResultsController method of RegularViewController:
request.predicate = [NSPredicate predicateWithFormat:#"isSelectedPractice = %#", #"YES"];
Many thanks for the help
Without seeing your actual project, one way I know will work but might be a little too round a bout would be to add an attribute "isSelectedPractice" to your entity. You could make it a BOOL, but I've had mixed results with BOOL's in Core Data, I prefer to just leave it as a NSString and set it to "yes" or "no". Then when you pull it down, modify it or add it to core Data as a entity with isSelectedPractice set to "yes". Then in your other controllers, do a
if (self.managedObjectContext == nil) {
self.managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
then do a fetch request to get entities with a predicate which is looking for isSelectedPractice equaling "yes". If you need actual code samples on how to do this let me know and I'll edit them in.