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).
Related
I am having a major problem with my application speed in processing updates on a background thread. Instruments shows that almost all of this time is spend inside performBlockAndWait where I am fetching out the objects which need updating.
My updates may come in by the hundreds depending on the amount of time offline and the approach I am currently using is to process them individually; ie fetch request to pull out the object, update, then save.
It sounds slow and it is. The problem I have is that I don't want to load everything into memory at once, so need to fetch them individually as we go, also I save as I go to ensure that if there is an issue with a single update it won't mess up the rest.
Is there a better approach?
I hit similar slow performance when upserting a large collection of objects. In my case I'm willing to keep the full change set in memory and perform a single save so the large volume of fetch requests dominated my processing time.
I got a significant performance improvement from maintaining an in memory cache mapping my resources' primary keys to NSManagedObjectIDs. That allowed me to use existingObjectWithId:error: rather than a fetch request for an individual object.
I suspect I might do even better by collecting the primary keys for all resources of a given entity description, issuing a single fetch request for all of them at once (batching those results as necessary), and then processing the changes to each resource.
You may benefit from using NSBatchUpdateRequest assuming you're targeting iOS 8+ only.
These guys have a great example of it but the TLDR is basically:
Example: Say we want to update all unread instances of MyObject to be marked as read:
NSBatchUpdateRequest *req = [[NSBatchUpdateRequest alloc] initWithEntityName:#"MyObject"];
req.predicate = [NSPredicate predicateWithFormat:#"read == %#", #(NO)];
req.propertiesToUpdate = #{
#"read" : #(YES)
};
req.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *res = (NSBatchUpdateResult *)[context executeRequest:req error:nil];
NSLog(#"%# objects updated", res.result);
Note the above example is taken from the aforementioned blog, I didn't write the snippet.
Basically I am trying to deal with orphan objects in this case.
I have some existing objects in my core data store. And before performing the API request operation I delete all the objects for that particular entity. So when I fetch new objects from the server, my local data is in sync with the new data object received from the server.
But there can be a case, when my API request operation fail. In such a case, I cannot afford to lose the existing objects in my local database. Hence I considered using undo operation for that.
Now I tried using setWillMapDeserializedResponseBlock for performing this. (this block gets executed after the API operation is successful and just before restkit performs the mapping)
So in this block, I perform deletion of the existing objects in the entity.
But after the mapping is completed, the objects that were missing in the server response, but were present in the local store still exist. (hence the deletion of existing objects from the local store didn't affect the store). After explicitly calling the saveToPersistantStore method in the setWillMapDeserializedResponseBlock, the changes do not get reflected.
This is setWillMapDeserializedResponseBlock:
{
// start undo grouping
[[[NSManagedObjectContext MR_defaultContext] undoManager] beginUndoGrouping];
[[[NSManagedObjectContext MR_defaultContext] undoManager] setActionName:#"undo deleting objects"];
// perform deletion of objects
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"..."];
[Entity MR_deleteAllMatchingPredicate:predicate];
// end undo grouping
[[[NSManagedObjectContext MR_defaultContext] undoManager] endUndoGrouping];
}
How can I approach this problem in a different way?
Updated:
Using fetch Request Blocks is not feasible since there are a lot of entities in a tree like hierarchy.
My structure for core data is like follows:
RootEntity
- identifier
EntityB
- root_identifier
EntityC
- b_identifier
EntityD
- c_identifier
The parameter for the api call is the identifier for RootEntity.
I get all the entities in the response. FetchRequests can be written easily for RootEntity and EntityB as they have an attribute for storing RootEntity's identifier. But for EntityC and EntityD, there is no direct relation to RootEntity which makes writing a fetch request difficult.
Also, I have setup cascaded deletion in relationships. So, when rootEntity gets deleted, all children get deleted. I think this deletion would be less expensive than making those fetch requests.
You should instead us a fetch request block which is executed by RestKit on success (not failure, be careful with your response descriptors as they dictate what success is) and deletes everything which matches the fetch but isn't new in the just received response.
In my project, I have to post many data, one by one, to a remote server.
I post one object by using the well known method:
[objectManager postObject:responseMapping
path:#"remoteMethod.json"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"OK WS RK:%#",mappingResult);
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Error WS RK:%#",error.localizedDescription);
}
];
Now, after entering the success block I should start a new query, and so on.
Which is the right method to do? Or rather, I came up with a technique: when entering success block, I post a notification to self and go on.
My question is: is there other smarter or correct ways to handle it?
Notifications should be used when you're trying to notify unknown classes / instances about the occurrence of an event. That isn't what you're trying to do. It is more appropriate for you to call a method on self in the success block and have that method generate the new request / take a request off a queue.
Your queue is most easily implemented using a mutable array. Any time you want to do something, add it to the array (perhaps in the form of an NSBlockOperation). In the success block, check if there is anything in the array and, if there is, start and remove the first item.
When you add each item to the array you need to know if any operation is currently in progress. If not, you need to run the item added to (or instead of adding it) the queue. To do this, hold a BOOL #property, set it in each operation added to the queue and clear it in each success block.
RestKit (i.e. the RKObjectManager) already manages a queue of operations. Whenever you do a postObject:path:parameters:success:failure:, an RKObjectRequestOperation with a POST request for the given object is created, and enqueued to the manager’s operation queue. If you want to ensure the order of requests, set the concurrentRequestsLimit to 1.
See the documentation for further discussion.
Let's suppose I have a Core Data model using AFIncrementalStore, and I have multiple REST API endpoints for retrieving a list of objects of that model. I can override -requestForFetchRequest:withContext: in AFHTTPClient like so:
- (NSURLRequest *)requestForFetchRequest:(NSFetchRequest *)fetchRequest
withContext:(NSManagedObjectContext *)context {
NSMutableURLRequest *mutableURLRequest = nil;
if ([fetchRequest.entityName isEqualToString:#"Post"]) {
mutableURLRequest = [self requestWithMethod:#"GET" path:#"/posts/foo" parameters:nil];
}
return mutableURLRequest;
}
In this snippet, I retrieve Post objects at /posts/foo, but I also need to retrieve another set from /posts/bar.
How can I do this? The only solution I see is to make two models: one for foo and one for bar, but repeating yourself is lame, and there may be many more API endpoints that get Post objects for me that I'll need to support. Is there some other approach that I'm missing?
You need to inspect fetchRequest more closely than just looking at entityName. You can also look at fetchRequest.propertiesToFetch or possibly other things depending on your data model. You'll still need to send two requests, so just make sure your AFNetworking subclass can tell the difference.
Also: it sounds like your requestForFetchRequest:withContext: method might get really big. You might want to consider a more generic pattern in which you get your NSManagedObject subclass, and ask that to return a fetch request.
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.