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];
}];
}
Related
SETUP (You can read this later and skip to the scenario section first)
It's an old app, with manually setup CoreData stack like this:
+ (NSManagedObjectContext *)masterManagedObjectContext
{
if (_masterManagedObjectContext) {
return _masterManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];
if (coordinator != nil) {
_masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_masterManagedObjectContext.retainsRegisteredObjects = YES;
_masterManagedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_masterManagedObjectContext.persistentStoreCoordinator = coordinator;
}
return _masterManagedObjectContext;
}
+ (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext) {
return _managedObjectContext;
}
NSManagedObjectContext *masterContext = [self masterManagedObjectContext];
if (masterContext) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.retainsRegisteredObjects = YES;
_managedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_managedObjectContext.parentContext = masterContext;
}
return _managedObjectContext;
}
+ (NSManagedObjectContext *)newManagedObjectContext
{
__block NSManagedObjectContext *newContext = nil;
NSManagedObjectContext *parentContext = [self managedObjectContext];
if (parentContext) {
newContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
newContext.parentContext = parentContext;
}
return newContext;
}
And then save context recursively:
+ (void)saveContext:(NSManagedObjectContext *)context
{
[context performBlockAndWait:^{
if (context.hasChanges && context.persistentStoreCoordinator.persistentStores.count) {
NSError *error = nil;
if ([context save:&error]) {
NSLog(#"saved context: %#", context);
// Recursive save parent context.
if (context.parentContext) [self saveContext:context.parentContext];
}
else {
// do some real error handling
NSLog(#"Could not save master context due to %#", error);
}
}
}];
}
SCENARIO
The app load lots of data from a server, then perform update inside newContext first, then merge into mainContext -> masterContext -> persistentStore.
Because lots of data, the sync process has been divided into about 10 async threads => we have 10 newContext at a time.
Now, the data is complicated, with things like parents <-> children (same class). 1 parent can have many children, and a child can have a mother, father, god father, step mother..., so it's n-n relationship. First, we fetch parent, then perform fetch child and then set the child to parent, and so on.
The server is kinda stupid, it can't send disabled objects. However the customer would like to control the display of app's objects from the back end, so I have 2 properties to do that:
hasUpdated: At the beginning of loading process, perform a batch update, set all object's hasUpdated to NO. When got data from the server, update this property to YES.
isActive: When all loading was done, perform batch update this property to NO if hasUpdate == NO. Then, I have a filter that won't show object with isActive == NO
ISSUE
Customers complain why some objects being missing even if they're enable in the backend. I've struggle and debugging for so long after got to this strange issue:
newContext.updatedObjects : { obj1.ID = 100, hasUpdated == YES }
"saved newContext"
mainContext.updatedObjects: {obj1.ID = 100, hasUpdated == NO }
// I'll stop here. Obviously, master got updated = NO and finally isActive will set to no, which cause missing objects.
If it happened every time, then probably easier to fix (¿maybe?). However, it occurs like this:
First time running (by first time, I mean app start from where appDidFinishLaunch... got called): all correct
2nd time: missing (153 objects)
3rd time: all correct
4th time: missing (153 objects) (again? exactly those with multiple parents, I believe so!)
5th time: correct again
... so on.
Also, it looks like this happened for objects which have the same context (same newContext). Unbelievable.
QUESTIONS
Why is this happening? How do I fix this? If those objects don't have children, my life would be easier!!!!
BONUS
In case you'd like to know how the batch update is, it's below. Note:
Download requests are in async queue: _shareInstance.apiQueue = dispatch_queue_create("product_request_queue", DISPATCH_QUEUE_CONCURRENT);
Parse response and update properties are syncronous in a queue: _shareInstance.saveQueue = dispatch_queue_create("product_save_queue", DISPATCH_QUEUE_SERIAL);
Whenever parse complete, I perform save newContext and call for updateProductActiveStatus: in the same serial queue. If all requests are finished, then perform batch update status. Since request are done in concurent queue, it's always finished earlier than save (serial) queue, so it's pretty much fool proof process.
Code:
// Load Manager
- (void)resetProductUpdatedStatus
{
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.propertiesToUpdate = #{ #"hasUpdated" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update hasUpdated: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
[[CoreDataUtil managedObjectContext] performBlockAndWait:^{
[[CoreDataUtil managedObjectContext] refreshAllObjects];
}];
}];
}
- (void)updateProductActiveStatus:(SyncComplete)callback
{
if (self.apiRequestList.count) return;
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.predicate = [NSPredicate predicateWithFormat:#"hasUpdated = NO AND isActive = YES"];
request.propertiesToUpdate = #{ #"isActive" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update isActive: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
NSManagedObjectContext *maincontext = [CoreDataUtil managedObjectContext];
NSLog(#"Refreshed master");
[maincontext performBlockAndWait:^{
[maincontext refreshAllObjects];
NSLog(#"Refreshed main");
// Callback
if (callback) dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); });
}];
}];
}
mergePolicy is evil. The only correct mergePolicy is NSErrorMergePolicy any other policy is asking core-data to silently fail and not update when you expect it too.
I suspect that your problem is that you are writing simultaneously to core-data with the background contexts. (I know that you say you have a serial queue - but if you call performBlock inside the queue then each block is executed simultaneously). When there is a conflict stuff gets overwritten. You should only write to core-data in one synchronous way.
I wrote an answer on how to accomplish this with a NSPersistentContainer:
NSPersistentContainer concurrency for saving to core data and I would suggest that you migrate your code to it. It really should not be that hard.
If you want to keep the code as close to what is currently is as possible that also is not that hard.
Make a serial operation queue:
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
And do all writing using this queue:
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = [CoreDataUtil newManagedObjectContext];
[context performBlockAndWait:^{
blockCopy(context);
[CoreDataUtil saveContext:context];
}];
}]];
}
Also it could be that the objects ARE updated, but you aren't seeing it because you are relying on a fetchedResultsController to be updated. And fetchedResultsController don't update from batch update requests.
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 trying to move my process to the background thread, but i believe the managedObjectContext is not being passed correctly. Initialize it in the background queue of the location manager, as so:
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.managedObjectContext.parentContext = moc;
locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
if([self.locationManager respondsToSelector:#selector(requestAlwaysAuthorization)]){
[self.locationManager requestAlwaysAuthorization];
}
geoCoder = [[CLGeocoder alloc] init];
NSNumber *nsInterval = [[NSUserDefaults standardUserDefaults] objectForKey:#"location_interval"];
interval = (nsInterval == nil) ? -1 : [nsInterval intValue];
bgBackupHandler = [[BackgroundBackupHandler alloc]init];
bgBackupHandler.managedObjectContext = self.managedObjectContext;
I then call it in another method in the location handler class as so:
[bgBackupHandler OnSyncComplete:[NSNumber numberWithInt:0] message:#"BG-Backup Called"];
Then in the bgBackupHandler class everything works up until i try to fetch. The array is always null. I think im not passing my managedObjectContext correctly. Ive tried using self.managedObjectContext but the code either crashes or, the array is still null. What is the difference between self.managedObjectContext and just simply managedObjectContext. Ive also read some where it is strictly forbidden to pass a managedObject from one MOC to the other.
The work is all being done in the background.
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"bdir_0001" inManagedObjectContext:managedObjectContext];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name = %#", strDirName];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *array = [managedObjectContext executeFetchRequest:request error:&error];
Core Data allows for two different concurrency models: thread confinement, which is deprecated, and queue confinement. When an NSManagedObjectContext is created with the initializer initWithConcurrencyType: it uses queue confinement.
Queue confinement requires the application to follow a set of simple rules. Access to any of the non thread safe methods or properties of the context and it's objects must be done through the performBlock: or performBlockAndWait: methods. The only safe methods are the read-only accessors of the context and the context's property setters. All other methods of the NSManagedObject context and the NSManagedObjects it owns must be accessed through these methods.
In your case, the fetch you are performing must be done in a block passed to one of these methods. For example:
[managedObjectContext performBlock:^{
NSError *error = nil;
NSArray *array = nil;
array = [managedObjectContext executeFetchRequest:request error:&error];
}];
It a good idea while developing to enable Core Data concurrency debugging assertions to catch these and other issues. When an application is passed the com.apple.CoreData.ConcurrencyDebug argument on launch Core Data will halt execution when it runs into a concurrency issue like violating the rules described above. This helps to catch issues early in your development cycle.
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!
I'm trying to use RestKit just to map local JSON to my Core Data entity, as I'm dealing with authenticating to an API and already am using another library to do that, so I'd just be passing RestKit the array of JSON data (which I've verified I'm receiving correctly). I already have all the basic necessary components set up (data model/entity, NSManagedObject class, managed object context, etc.), as I'm adapting an existing app I wrote that used RestKit to access a RSS feed and map it into Core Data.
From researching, it seems that I should use RKManagedObjectMappingOperationDataSource to accomplish this, and have adapted an example I found in the unit tests for this:
-(void)performMapping
{
FoodTruck *foodTruck = FoodTruck.new;
RKEntityMapping *mapping = [ObjectMappings FoodTruckArticleMapping];
RKManagedObjectStore *store = [[FoodTruckDataModel sharedDataModel] objectStore];
RKManagedObjectMappingOperationDataSource *mappingDS = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:store.mainQueueManagedObjectContext cache:store.managedObjectCache];
mappingDS.operationQueue = [NSOperationQueue new];
RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:self.moreStatuses destinationObject:foodTruck mapping:mapping];
operation.dataSource = mappingDS;
NSError *error = nil;
[operation performMapping:&error];
[mappingDS.operationQueue waitUntilAllOperationsAreFinished];
}
My object mappings are done in a separate mappings class and look like this:
+(RKEntityMapping *)FoodTruckArticleMapping
{
RKEntityMapping *jsonMapping = [RKEntityMapping mappingForEntityForName:#"FoodTruck" inManagedObjectStore:[[FoodTruckDataModel sharedDataModel] objectStore]];
jsonMapping.identificationAttributes = #[#"tweetID"];
[jsonMapping addAttributeMappingsFromDictionary:#{
#"text": #"tweet", #"user.screen_name": #"foodTruckName", #"id": #"tweetID", #"created_at": #"timeStamp"}];
return jsonMapping;
}
My FoodTruckDataModel class looks like this:
+(id)sharedDataModel {
static FoodTruckDataModel *__sharedDataModel = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__sharedDataModel = [[FoodTruckDataModel alloc] init];
});
return __sharedDataModel;
}
- (NSManagedObjectModel *)managedObjectModel
{
return [NSManagedObjectModel mergedModelFromBundles:nil];
}
-(id)optionsForSQLiteStore
{
return #{
NSInferMappingModelAutomaticallyOption: #YES,
NSMigratePersistentStoresAutomaticallyOption: #YES
};
}
-(void)setup
{
self.objectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:[self managedObjectModel]];
[self.objectStore createPersistentStoreCoordinator];
NSString *path = [RKApplicationDataDirectory() stringByAppendingPathComponent:#"FoodTruckxxx.sqlite"];
NSLog(#"Setting up store at %#", path);
[self.objectStore addSQLitePersistentStoreAtPath:path
fromSeedDatabaseAtPath:nil
withConfiguration:nil
options:self.optionsForSQLiteStore
error:nil];
[self.objectStore createManagedObjectContexts];
self.objectStore.managedObjectCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:self.objectStore.persistentStoreManagedObjectContext];
}
However, something isn't being done correctly, as I'm getting this Core Data error:
2013-05-25 19:38:17.867 FoodTrucks[37340:c07] CoreData: error: Failed to call designated initializer on NSManagedObject class 'FoodTruck'
and I'm also getting a crash on this:
2013-05-25 19:38:17.887 FoodTrucks[37340:4903] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** setObjectForKey: key cannot be nil'
It appears as if it's performing mapping operations on my data, but that it's not being passed my entity and mapping info, as can be seen from my log (JSON data erased). I'm not sure what I'm doing wrong, as it was working fine before when I was using RKManagedObjectRequestOperation.
If anyone has any suggestions or help, it'd be greatly appreciated!
The cause of the first problem (and probably the second too) is caused by:
FoodTruck *foodTruck = FoodTruck.new;
As you are not permitted to instantiate managed objects like that. Instead you must use NSEntityDescription. The code you have at the moment doesn't create a valid instance so when you try and use it bad things happen.