I have the following fetch request block set up to deal with deletion of orphaned objects:
[objectManager addFetchRequestBlock:^NSFetchRequest *(NSURL *URL) {
RKPathMatcher *pathMatcher = [RKPathMatcher pathMatcherWithPattern:API_GET_ACTIVE_RIDES];
NSString * relativePath = [URL relativePath];
NSDictionary *argsDict = nil;
BOOL match = [pathMatcher matchesPath:relativePath tokenizeQueryStrings:NO parsedArguments:&argsDict];
if (match) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Ride"];
return fetchRequest;
}
return nil;
}];
Since I need to allow users to log out and log back in, I clear all data from core data using the following:
+ (void) clearUserData {
NSError * error = nil;
[[RKManagedObjectStore defaultStore] resetPersistentStores:&error];
if(error != nil){
[WRUtilities criticalError:error];
return;
}
}
However, if I log out and log back into my app, objects that were loaded the first time I logged in are not loaded from the server. Using RestKit logging, I can see that the request goes out and returns the correct data from the server, but mapping appears to be completely skipped, causing no objects to be (re)inserted into core data.
If I remove my fetch request block, everything works as I would expect - clearUserData removes all data, and upload login the data is re-queried from the server and reloaded into core data.
My question is two fold. What do I need to change to get the expected behavior of successfully reloading data, and why does the fetch request block, which I understand to be only for deleting orphaned objects, have an effect on this scenario?
I've seen this before and just removed the fetch request block, but I would prefer to use this feature rather than skip it because of this problem.
I had this exact problem and managed to find a solution. Essentially the code below will only return a fetch request if there are existing items held in CoreData. The problem with returning it all the time means that RestKit will delete the items you get from the server the first time. The idea here is to return nil if there are no items currently held in CoreData. Not sure if this is the way RestKit would recommend, but it worked for me.
RKObjectManager *sharedManager = [RKObjectManager sharedManager];
[sharedManager addFetchRequestBlock:^NSFetchRequest *(NSURL *URL) {
RKPathMatcher *userPathMatcher = [RKPathMatcher pathMatcherWithPattern:#"my/api/path"];
BOOL match = [userPathMatcher matchesPath:[URL relativePath] tokenizeQueryStrings:NO parsedArguments:nil];
if(match) {
// lets first check if we have anything in coredata
NSString * const entityName = NSStringFromClass([MyCoreDataEntity class]);
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName];
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = [RKManagedObjectStore defaultStore].mainQueueManagedObjectContext;
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
if(error) {
// this shouldn't happen, but if something went wrong querying CoreData
// lets just save the new ones
NSLog(#"error %#", error);
return nil;
}
if([results count] > 0) {
// if we already have something saved, lets delete 'em and save
// the new ones
return [NSFetchRequest fetchRequestWithEntityName:entityName];
}
else {
// if we don't have something saved, lets save them
// (e.g. first time after login after we cleared everything on logout)
return nil;
}
}
return nil;
}];
Hope that helps!
Related
I have two UIViewControllers in a Tab Bar
In one of the TabBar I am making an api call using AFNetworking and this api call is saving data in CoreData.
Here is my code
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < cartList.count; i++)
{
NSDictionary *dict = [cartList objectAtIndex:i];
NSFetchRequest *request = [Orders fetchRequest];
request.predicate = [NSPredicate predicateWithFormat:#"orderId = %#", [dict objectForKey:kiD]];
NSError *error = nil;
NSArray *itemsList = context executeFetchRequest:request error:&error];
if (itemsList.count == 0)
{
Orders *order = [NSEntityDescription insertNewObjectForEntityForName:#"Orders" inManagedObjectContext:appDel.persistentContainer.viewContext];
[order updateWithDictionary:dict];
order.isNew = NO;
}
else
{
Orders *order = [itemsList objectAtIndex:0];
[order updateWithDictionary:dict];
order.isNew = NO;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
[appDel saveContext];
[self refreshValues:NO];
});
});
In second VIewController I am doing something like that. If I switch the tab controllers very fast the app crashes at
[appDel saveContext];
most probably because the last time viewContext was used by other UIviewController in Background thread.
What is the work around I can adopt to fix this problem
If this is correctly implemented
[appDel.persistentContainer performBackgroundTask:^(NSManagedObjectContext * _Nonnull context)
{
NSFetchRequest *request = [Categories fetchRequest];
NSBatchDeleteRequest *deleteReq = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request];
NSError *deleteError = nil;
[appDel.persistentContainer.viewContext executeRequest:deleteReq error:&deleteError];
for (int i = 0; i < dataArr.count; i++)
{
Categories *category = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:appDel.persistentContainer.viewContext];
[category updateWithDictionary:[dataArr objectAtIndex:i]];
}
#try {
NSError *error = nil;
[context save:(&error)];
} #catch (NSException *exception)
{
}
[self getCategoryItems];
}];
Core-data is not thread-safe, neither for reading for for writing. If you violate this ever core-data can fail in unexpected ways. So even if it appears to work you can find core-data suddenly crashing for no apparent reasons. In other words, accessing core-data from the wrong thread is undefined.
There are a few possible solutions:
1) only use the main thread for reading and writing to core-data. This is an OK solution for simple apps that don't do a lot of data import or export and have relatively small data sets.
2) Wrap NSPersistentContainer's performBackgroundTask in an operation queue and only write to core-data through that method and never write to the viewContext. When you use performBackgroundTask the method gives you a context. You should use the context to fetch any objects that you need, modify them, save the context and then discard the context and the objects.
If you try to write using both performBackgroundTask and writing directly to the viewContext you can get write conflicts and lose data.
Create a child NSManagedObjectContext object with NSPrivateQueueConcurrencyType your data processing in background queue.
Read Concurrency guide for more info.
I am working on an app which makes use of Core Data. I am using parent/child contexts to access Core Data from both the main and background threads. The application makes use of Quickblox framework (don't bother if you aren't aware of this framework, you need not be aware of it to answer the question), I have to use the API of this framework which makes a call to a server, fetches data and hands the control back to me using a block. Control is always returned back on the main thread i.e the block always executes on the main thread.
Here's my code:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
{
QBResponsePage *page = [QBResponsePage responsePageWithLimit:ResponseLimit skip:0];
[QBRequest messagesWithDialogID:weakSelf.dialog.id
extendedRequest:extendedRequest
forPage:page
successBlock:^(QBResponse *response, NSArray *messages, QBResponsePage *page)
{
if(messages.count > 0)
{
NSArray *filteredArray=nil;
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:#"Dialog"];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"id == %#",weakSelf.dialog.id];
[request setPredicate:predicate];
NSArray *results=[weakAppDelegate.managedObjectContext executeFetchRequest:request error:nil];
Dialog *parentDialog=nil;
if([results count]>0){
parentDialog=[results objectAtIndex:0];
}
for(QBChatMessage *message in messages)
{
NSFetchRequest *request=[[NSFetchRequest alloc] initWithEntityName:#"Chats"];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"chat_Id == %#",message.ID];
[request setPredicate:predicate];
NSArray *results=[weakAppDelegate.managedObjectContext executeFetchRequest:request error:nil];
if([results count]==0){
if([message.dateSent compare:parentDialog.last_Message_Date]==NSOrderedDescending){
parentDialog.last_Message_Date=message.dateSent;
parentDialog.last_Message_User_Id=[NSNumber numberWithInteger:message.senderID];
parentDialog.lastMessage=message.text;
}
Chats *recievedChat=[NSEntityDescription insertNewObjectForEntityForName:#"Chats" inManagedObjectContext:weakAppDelegate.managedObjectContext];
recievedChat.sender_Id=[NSNumber numberWithInteger:message.senderID];
recievedChat.date_sent=message.dateSent;
recievedChat.chat_Id=message.ID;
recievedChat.belongs=parentDialog;
recievedChat.message_Body=message.text;
}
}
//commit main context
}
//update user profilepic
} errorBlock:^(QBResponse *response) {
//handle error
}];
});
Everything works fine, but as soon as the statement recievedChat.belongs=parentDialog; gets executed, dealloc never gets called even after popping out the view controller containing code. Commented out this statement, obviously Core Data relationships won't work but dealloc gets called as expected.
I am confused. Please help guys.
Thanks in advance.
I'm making some app and I want to provide offline functionality to it.
Problem is with getting new data from backend as temporary objects not saved in persistent store. Why I want this? Because I want to check whether data from backend is newer than offline one (by update date) If yes then update, otherwise, send it to the backend.
For now I'm trying something like this:
NSMutableURLRequest *apiEmailRequest = [[RKObjectManager sharedManager] requestWithObject:#"ApiEmail" method:RKRequestMethodGET path:pathToContent parameters:nil];
RKObjectRequestOperation *apiEmailOperation = [[RKObjectManager sharedManager] managedObjectRequestOperationWithRequest:apiEmailRequest managedObjectContext:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
*********************CHECK FOR BACKEND EMAILS AND OFFLINE ONE **********************
NSArray *backendEmails = [mappingResult array];
for (ApiEmail *backendEmail in backendEmails) {
if ([backendEmail isKindOfClass:[ApiEmail class]]) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"ApiEmail"];
NSPredicate *filterByApplication = [NSPredicate
predicateWithFormat:#"emailId == %#", backendEmail.emailId];
[fetchRequest setPredicate:filterByApplication];
NSArray *persistentEmails = [[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil];
*HERE PUT IT INTO mainQueueManagedObjectContext and
saveToPersistentStore else POST it to the backend*
}
}
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
*ERROR*
}];
return apiEmailOperation;
[[RKObjectManager sharedManager] enqueueObjectRequestOperation:apiEmailOperation];
Is there any way to do it without creating new RKObjectManager?
Best regards, Adrian.
UPDATE
-(void)willSave {
[super willSave];
NSDictionary *remoteCommits = [NSDictionary dictionaryWithDictionary:[self committedValuesForKeys:#[#"updateDate"]]];
NSDate *updateDate = [remoteCommits valueForKey:#"updateDate"];
NSComparisonResult result = [self.updateDate compare:updateDate];
if(result == NSOrderedDescending) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
} else {
[self.managedObjectContext refreshObject:self mergeChanges:YES];
}
}
After such modification I'm getting Failed to process pending changes before save. The context is still dirty after 1000 attempts.
The below is unlikely to work in your situation actually, specifically because of the way discardsInvalidObjectsOnInsert works.
You may be able to do this by following the below process but additionally implementing willSave on the managed object and checking the status there. If you decide to abandon the updates you can try using refreshObject:mergeChanges: with a merge flag of NO.
With KVC validation you have 2 options:
edit individual pieces of data as it is imported
abandon the import for a whole object
Option 2. requires that you use the Core Data validation to prevent the import. That means doing something like making the date stamp on the object non-optional (i.e. required) and in your KVC validation setting it to nil when you determine that the import should be aborted.
Note that for this to work you need to set discardsInvalidObjectsOnInsert on the entity mapping.
After big help from #Wain, I finally got it working. Without this brave men I would still be in the sandbox. Solution:
-(BOOL)validateUpdateDate:(id *)ioValue error:(NSError **)outError {
NSComparisonResult result = [self.updateDate compare:(NSDate *)*ioValue];
if (result == NSOrderedDescending) {
self.updateDate = nil;
return NO;
}
return YES;
}
-(void)willSave {
[super willSave];
if (self.updateDate == nil) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
}
}
Thank You so much for your time and help.
Best regards, Adrian.
I am struggling to come up with an elegant solution to a problem involving multiple network requests. If you need further information than what has been provided then please don't hesitate to ask.
I need to download data from a server, create core data objects from it and then use information from those objects to download the next set of data. So I am transversing a hierarchy.
So for example:
I make my first request to the server and pull down the Regions which is made up of 4 objects (North, South, East, West). I am saving these to core data.
Once that is done (not sure best way to track this) I then need to do a fetch request on the region entity to get back those 4 objects. Each region contains a number of counties which I need to request from the server. So I loop through the regions and make a network request for each region.
I loop through each returned dictionary (one for each region) to create each county.
Here is my code to download the regions and county:
+ (void)downloadRegions
{
NSString *search = #"organisation";
NetworkHandler *networkHandler = [[NetworkHandler alloc] init];
[networkHandler downloadData:search];
}
+ (void)downloadCounty
{
NSManagedObjectContext *context = [DatabaseHandler sharedHandler].managedObjectContext;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Region" inManagedObjectContext:context];
[request setEntity:entity];
//NSPredicate *searchFilter = [NSPredicate predicateWithFormat:#"attribute = %#", searchingFor];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
NSLog(#"%#", results);
NSString *search = #"organisation?code=";
for (Region *region in results) {
NSString *s = [search stringByAppendingString:[NSString stringWithFormat:#"%#", region.code]];
NetworkHandler *networkHandler = [[NetworkHandler alloc] init];
[networkHandler downloadData:s];
}
}
Both of the above methods call:
- (void)downloadData:(NSString *)searchUrl
{
NSString *apiURL = [kBaseURL stringByAppendingPathComponent:#"api"];
NSString *finalURL = [apiURL stringByAppendingPathComponent:searchUrl];
self.dataTask = [self.session dataTaskWithURL:[NSURL URLWithString:finalURL]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
//self.jsonData[searchUrl] = json;
[self createObjectInDatabase:json andSearchURL:searchUrl];
NSLog(#"This has been reached");
}];
[self.dataTask resume];
}
- (void)createObjectInDatabase:(id)data andSearchURL:(NSString *)searchURL
{
if ([searchURL isEqual:#"organisation"]) {
[Region createNewRegionWithData:data inManagedObjectContext:[DatabaseHandler sharedHandler].managedObjectContext];
}
else {
[LAT createNewLATWithData:data inManagedObjectContext:[DatabaseHandler sharedHandler].managedObjectContext];
}
}
I am not sure if I am doing this the best way. In regards to making the request, creating the object and then making the next request.
My biggest issue is knowing when to make the next request. i.e - knowing when the download has completed and all the core data objects have been created successfully before making a request for those objects and using them in the next request. I am currently making the second request manually but need it to be done automatically.
I hope that is clear. I am finding it hard to explain :-). Thanks in advance.
What if you passed a string into downloadData: that the completion handler then used to post a notification as it was finishing? When you receive each notification, you know which step of the process to go to next.
I've got an iOS app that uses restkit to handle json responses to map things into core data. Anytime I perform a request through RKObjectManager's get/post/put/delete methods, it works great, and I never run into any issues.
The app I'm developing also supports socket updates, for which I'm using SocketIO to handle. SocketIO also is working flawlessly, and every event the server sends out I receive without fail, unless the app isn't running at that time.
The issue occurs when I try to take the json data from the socket event, and map it to core data. If the socket event comes in at the same time a a response comes back from a request I made through RKObjectManager, and they are both giving me the same object for the first time, they often both end up making 2 copies of the same ManagedObject in coredata, and I get the following warning in console:
Managed object Cache returned 2 objects for the identifier configured for the [modelObjectName] entity, expected 1.
Here is the method I've made containing the code for making the RKMapperOperation:
+(void)createOrUpdateObjectWithJSONDictionary:(NSDictionary*)jsonDictionary
{
RKManagedObjectStore* managedObjectStore = [CMRAManager sharedInstance].objectManager.managedObjectStore;
NSManagedObjectContext* context = managedObjectStore.mainQueueManagedObjectContext;
[context performBlockAndWait:^{
RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:managedObjectStore];
NSDictionary* modelPropertyMappingsByDestinationKeyPath = modelEntityMapping.propertyMappingsByDestinationKeyPath;
NSString* modelMappingObjectIdSourceKey = kRUClassOrNil([modelPropertyMappingsByDestinationKeyPath objectForKey:NSStringFromSelector(#selector(object_Id))], RKPropertyMapping).sourceKeyPath;
NSString* modelObjectId = [jsonDictionary objectForKey:modelMappingObjectIdSourceKey];
CMRARemoteObject* existingObject = [self searchForObjectOfCurrentClassWithId:modelObjectId];
RKMapperOperation* mapperOperation = [[RKMapperOperation alloc]initWithRepresentation:jsonDictionary mappingsDictionary:#{ [NSNull null]: modelEntityMapping }];
[mapperOperation setTargetObject:existingObject];
RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:managedObjectStore.managedObjectCache];
[mappingOperationDataSource setOperationQueue:[NSOperationQueue new]];
[mappingOperationDataSource setParentOperation:mapperOperation];
[mappingOperationDataSource.operationQueue setMaxConcurrentOperationCount:1];
[mappingOperationDataSource.operationQueue setName:[NSString stringWithFormat:#"%# with operation '%#'", NSStringFromSelector(_cmd), mapperOperation]];
[mapperOperation setMappingOperationDataSource:mappingOperationDataSource];
NSError* mapperOperationError = nil;
BOOL mapperOperationSuccess = [mapperOperation execute:&mapperOperationError];
NSAssert((mapperOperationError == nil) && (mapperOperationSuccess == TRUE), #"Execute mapperOperation error");
if (mapperOperationError || (mapperOperationSuccess == FALSE))
{
NSLog(#"mapperOperationError: %#",mapperOperationError);
}
NSError* contextSaveError = nil;
BOOL contextSaveSuccess = [context saveToPersistentStore:&contextSaveError];
NSAssert((contextSaveError == nil) && (contextSaveSuccess == TRUE), #"Save context error");
}];
}
In the above code, I first try and fetch the existing managed object if it currently exists to set it to the mapper request's target object. The method to find the object (searchForObjectOfCurrentClassWithId:) looks like the following:
+(instancetype)searchForObjectOfCurrentClassWithId:(NSString*)objectId
{
NSManagedObjectContext* context = [CMRAManager sharedInstance].objectManager.managedObjectStore.mainQueueManagedObjectContext;
__block CMRARemoteObject* fetchedObject = nil;
[context performBlockAndWait:^{
NSFetchRequest* fetchRequest = [self fetchRequestForCurrentClassObjectWithId:objectId];
NSError* fetchError = nil;
NSArray *entries = [context executeFetchRequest:fetchRequest error:&fetchError];
if (fetchError)
{
NSLog(#"fetchError: %#",fetchError);
return;
}
if (entries.count != 1)
{
return;
}
fetchedObject = kRUClassOrNil([entries objectAtIndex:0], CMRARemoteObject);
if (fetchedObject == nil)
{
NSAssert(FALSE, #"Should be of this class");
return;
}
}];
return fetchedObject;
}
My best guess at the issue here is that it's probably due to the managed object contexts, and their threads. I don't have the best understanding of how they should necessarily be working, as I've been able to depend on Restkit's correct usage of it. I've done my best to copy how Restkit set up these mapping operations, but am assuming I've made an error somewhere in the above code.
I'm willing to post any other code if it would be helpful. I didn't post my RKEntityMapping code, because I'm pretty sure the error doesn't lie there - after all, Restkit has been successfully mapping these objects when it does the mapper operation itself, even when there's redundant JSON objects/data to map.
Another reason I think the issue must be my doing a bad implementation of the managed object contexts and their threads, is because I'm testing on an iPhone 5c, and an iPod touch, and the issue doesn't happen on the iPod touch, which I believe only has 1 core, but the iPhone 5c does sometimes encounter the issue, and I believe it has multiple cores. I should emphasize that I'm not sure of the statements I've made in this paragraph are necessarily true, so don't assume I know what I'm talking about with the device cores, it's just something I think I've read before.
try changing:
RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:managedObjectStore.managedObjectCache];
to:
RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:[RKFetchRequestManagedObjectCache new]];
And this for good measure before saving the persistent context:
// Obtain permanent objectID
[[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext obtainPermanentIDsForObjects:[NSArray arrayWithObject:mapperOperation.targetObject] error:nil];
EDIT #1
Try removing these lines:
[mappingOperationDataSource setOperationQueue:[NSOperationQueue new]];
[mappingOperationDataSource setParentOperation:mapperOperation];
[mappingOperationDataSource.operationQueue setMaxConcurrentOperationCount:1];
[mappingOperationDataSource.operationQueue setName:[NSString stringWithFormat:#"%# with operation '%#'", NSStringFromSelector(_cmd), mapperOperation]];
EDIT #2
Take a look at this unit test from RKManagedObjectMappingOperationDataSourceTest.m. Have you set identificationAttributes to prevent duplicates? It might not be necessary to find and set the targetObject, I thought RestKit tries to find an existing object if unset. Also try performing the object mapping on a private context created using [store newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO], after the context is saved, changes should be pushed to the main context.
- (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoContextsDoesNotCreateDuplicateObjects
{
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
RKInMemoryManagedObjectCache *inMemoryCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
managedObjectStore.managedObjectCache = inMemoryCache;
NSEntityDescription *humanEntity = [NSEntityDescription entityForName:#"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:#"Human" inManagedObjectStore:managedObjectStore];
mapping.identificationAttributes = #[ #"railsID" ];
[mapping addAttributeMappingsFromArray:#[ #"name", #"railsID" ]];
// Create two contexts with common parent
NSManagedObjectContext *firstContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO];
NSManagedObjectContext *secondContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO];
// Map into the first context
NSDictionary *objectRepresentation = #{ #"name": #"Blake", #"railsID": #(31337) };
// Check that the cache contains a value for our identification attributes
__block BOOL success;
__block NSError *error;
[firstContext performBlockAndWait:^{
RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:firstContext
cache:inMemoryCache];
RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:#{ [NSNull null]: mapping }];
mapperOperation.mappingOperationDataSource = dataSource;
success = [mapperOperation execute:&error];
expect(success).to.equal(YES);
expect([mapperOperation.mappingResult count]).to.equal(1);
[firstContext save:nil];
}];
// Check that there is an entry in the cache
NSSet *objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:#{ #"railsID": #(31337) } inManagedObjectContext:firstContext];
expect(objects).to.haveCountOf(1);
// Map into the second context
[secondContext performBlockAndWait:^{
RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:secondContext
cache:inMemoryCache];
RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:#{ [NSNull null]: mapping }];
mapperOperation.mappingOperationDataSource = dataSource;
success = [mapperOperation execute:&error];
expect(success).to.equal(YES);
expect([mapperOperation.mappingResult count]).to.equal(1);
[secondContext save:nil];
}];
// Now check the count
objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:#{ #"railsID": #(31337) } inManagedObjectContext:secondContext];
expect(objects).to.haveCountOf(1);
// Now pull the count back from the parent context
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Human"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"railsID == 31337"];
NSArray *fetchedObjects = [managedObjectStore.persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil];
expect(fetchedObjects).to.haveCountOf(1);
}
This is the solution we went with. Ensure identificationAttributes have been set in the mapping. Use RKMappingOperation without setting its destinationObject and RestKit will try to find an existing entity to map to by its identificationAttributes. We're also using RKFetchRequestManagedObjectCache as a precaution as we found the in-memory cache was unable to correct fetch the entities sometimes thus creating a duplicate entity..
NSManagedObjectContext *firstContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
firstContext.parentContext = [RKObjectManager sharedInstance].managedObjectStore.mainQueueManagedObjectContext;
firstContext.mergePolicy = NSOverwriteMergePolicy;
RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:[CMRAManager sharedInstance].objectManager.managedObjectStore];
RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:jsonDictionary destinationObject:nil mapping:modelEntityMapping];
// Restkit memory cache sometimes creates duplicates when mapping quickly across threads
RKManagedObjectMappingOperationDataSource *mappingDS = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:firstContext
cache:[RKFetchRequestManagedObjectCache new]];
operation.dataSource = mappingDS;
NSError *mappingError;
[operation performMapping:&mappingError];
[operation waitUntilFinished];
if (mappingError || !operation.destinationObject) {
return; // ERROR
}
[firstContext performBlockAndWait:^{
[firstContext save:nil];
}];
Please give this a try, use RKMappingOperation without setting the destination object, RestKit will try to find an existing object for you (if one exists) based on its identificationAttributes.
#pragma mark - Create or Update
+(void)createOrUpdateObjectWithJSONDictionary:(NSDictionary*)jsonDictionary
{
RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:[CMRAManager sharedInstance].objectManager.managedObjectStore];
// Map on the main MOC so that we receive the proper update notifications for anything
// observing relationships and properties on this model
RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:jsonDictionary
destinationObject:nil
mapping:modelEntityMapping];
RKManagedObjectMappingOperationDataSource *mappingDS = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:[CMRAManager sharedInstance].objectManager.managedObjectStore.mainQueueManagedObjectContext
cache:[RKFetchRequestManagedObjectCache new]];
operation.dataSource = mappingDS;
NSError *mappingError;
[operation performMapping:&mappingError];
if (mappingError || !operation.destinationObject) {
return; // ERROR
}
// Obtain permanent objectID
[[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext performBlockAndWait:^{
[[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext obtainPermanentIDsForObjects:[NSArray arrayWithObject:operation.destinationObject] error:nil];
}];
}