I'm experiencing strange CoreData issue.
First of all, in my project i use lot of frameworks, so there are many sources of problem - so i considered to create minimal project which repeats my issue. You can clone Test project on Github and repeat my test step-by-step.
So, the problem:
NSManagedObject is tied to it's NSManagedObjectID which doesn't let object to be deleted from NSManagedObjectContext properly
So, steps to reproduce:
In my AppDelegate, i setup CoreData stack as usual. AppDelegate has managedObjectContext property, which can be accessed to obtain NSManagedObjectContext for main thread. Application's object graph consists of one entity Message with body, from, timestamp attributes.
Application has only one viewController with only method viewDidLoad. It looks so:
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSEntityDescription *messageEntity = [NSEntityDescription entityForName:NSStringFromClass([Message class]) inManagedObjectContext:context];
// Here we create message object and fill it
Message *message = [[Message alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:context];
message.body = #"Hello world!";
message.from = #"Petro Korienev";
NSDate *now = [NSDate date];
message.timestamp = now;
// Now imagine that we send message to some server. Server processes it, and sends back new timestamp which we should assign to message object.
// Because working with managed objects asynchronously is not safe, we save context, than we get it's objectId and refetch object in completion block
NSError *error;
[context save:&error];
if (error)
{
NSLog(#"Error saving");
return;
}
NSManagedObjectID *objectId = message.objectID;
// Now simulate server delay
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
// Refetch object
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
Message *message = (Message*)[context objectWithID:objectId]; // here i suppose message to be nil because object is already deleted from context and context is already saved.
message.timestamp = [NSDate date]; // However, message is not nil. It's valid object with data fault. App crashes here with "Could not fulfill a fault"
NSError *error;
[context save:&error];
if (error)
{
NSLog(#"Error updating");
return;
}
});
// Accidentaly user deletes message before response from server is returned
delayInSeconds = 2.0;
popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
// Fetch desired managed object
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"timestamp == %#", now];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])];
request.predicate = predicate;
NSError *error;
NSArray *results = [context executeFetchRequest:request error:&error];
if (error)
{
NSLog(#"Error fetching");
return;
}
Message *message = [results lastObject];
[context deleteObject:message];
[context save:&error];
if (error)
{
NSLog(#"Error deleting");
return;
}
});
}
Well, i detected app crash so i try to fetch message another way. I changed fetch code:
...
// Now simulate server delay
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
// Refetch object
NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"timestamp == %#", now];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])];
request.predicate = predicate;
NSError *error;
NSArray *results = [context executeFetchRequest:request error:&error];
if (error)
{
NSLog(#"Error fetching in update");
return;
}
Message *message = [results lastObject];
NSLog(#"message %#", message);
message.timestamp = [NSDate date];
[context save:&error];
if (error)
{
NSLog(#"Error updating");
return;
}
});
...
Which NSLog'ed message (null)
So, it shows:
1) Message is actually not existent in DB. It cannot be fetched.
2) First version of code someway kept deleted message object in context (Probably cause it's object id was retained for block call).
But why i could obtain deleted object by its id? I need to know.
Obviously, first of all, i changed objectId to __weak. Got crash even before blocks:)
So CoreData is built without ARC? Hmm interesting.
Well, i considered to copy NSManagedObjectID. What i've gotten?
(lldb) po objectId
0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4>
(lldb) po message.objectID
0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4>
See what's wrong? NSCopying's -copy is implemented like return self on NSManagedObjectID
Last try was __unsafe_unretained for objectId. Here we go:
...
__unsafe_unretained NSManagedObjectID *objectId = message.objectID;
Class objectIdClass = [objectId class];
// Now simulate server delay
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
if (![NSObject safeObject:objectId isMemberOfClass:objectIdClass])
{
NSLog(#"Object for update already deleted");
return;
}
...
safeObject:isMemberOfClass: implementation:
#ifndef __has_feature
#define __has_feature(x) 0
#endif
#if __has_feature(objc_arc)
#error ARC must be disabled for this file! use -fno-objc-arc flag for compile this source
#endif
#import "NSObject+SafePointer.h"
#implementation NSObject (SafePointer)
+ (BOOL)safeObject:(id)object isMemberOfClass:(__unsafe_unretained Class)aClass
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage"
return ((NSUInteger*)object->isa == (NSUInteger*)aClass);
#pragma clang diagnostic pop
}
#end
Brief explanation - we use __unsafe_unretained variable, so at time of block call it can be freed, so we have to check whether it's valid object. So we save it's class before block (it's not retain, it's assign) and check it in block via safePointer:isMemberOfClass:
So for now, refetching object by it's managedObjectId is UNTRUSTED pattern for me.
Does anybody have any suggestions how i should do in this situation? To use __unsafe_unretained and check? However, this managedObjectId can be also retained by another code, so it will cause could not fulfill crash on property access. Or to fetch object everytime by predicate? (and what to do if object is uniquely defined by 3-4 attributes? Retain them all for completion block?). What is the best pattern for working with managed objects asynchronously?
Sorry for long research, thanks in advance.
P.S. You still can repeat my steps or make your own experiments with Test project
Don't use objectWithID:. Use existingObjectWithID:error:. Per the documentation, the former:
... always returns an object. The data in the persistent store
represented by objectID is assumed to exist—if it does not, the
returned object throws an exception when you access any property (that
is, when the fault is fired). The benefit of this behavior is that it
allows you to create and use faults, then create the underlying data
later or in a separate context.
Which is exactly what you're seeing. You get an object back because Core Data thinks you must want one with that ID even though it doesn't have one. When you try to store to it, without having created an actual object in the interim, it doesn't know what to do and you get the exception.
existingObject... will return an object only if one exists.
Related
I have flow where I should create object save it in CoreData. But by the flow I need to update object every X seconds and update it with saving context. And because there is possibility of terminating the app in coredata should be "last updated version" of the object.
Problem is that after saving context, core data not saving anymore.
Example with double saving not working:
dispatch_semaphore_t waitTodoA = dispatch_semaphore_create(0);
NSManagedObjectContext *contextA = [CoreDataManager backgroundObjectContext];
[contextA performBlock:^{
PlaceObject* placeObject = [NSEntityDescription insertNewObjectForEntityForName:#"Places" inManagedObjectContext:contextA];
placeObject.type = #"Flat";
placeObject.timestamp = [[NSDate date] timeIntervalSince1970];
[CoreDataManager saveContext:contextA];
placeObject.address = #"Sunny beach ave. 1";
placeObject.coordinates = #"0.0,0.0";
[CoreDataManager saveContext:contextA];
dispatch_semaphore_signal(waitTodoA);
}];
dispatch_semaphore_wait(waitTodoA, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)));
OK, I understand what you are trying to test now. The answer is that it works perfectly for me. I pasted the following code into the tail end of -readFromURL::: in the document subclass of my document-based Core Data app:
[self.managedObjectContext performBlock:^{
Stark* stark = [NSEntityDescription insertNewObjectForEntityForName:#"Stark_entity"
inManagedObjectContext:self.managedObjectContext];
BOOL ok ;
NSError* error = nil;
NSLog(#"Testing two saves in %#", self);
stark.name = #"David";
stark.rating = #(3);
ok = [self.managedObjectContext save:&error];
NSLog(#"First Save ok=%hhd error = %#", ok, error);
stark.url = #"http://example.com";
stark.comments = #"Did it work?";
ok = [self.managedObjectContext save:&error];
NSLog(#"Second Save ok=%hhd error = %#", ok, error);
}];
Upon running this code, the NSLogs printed:
Testing two saves in BkmxDoc 0x100d308f0 "Test.bmco"
First Save ok=1 error = (null)
Second Save ok=1 error = (null)
And, upon examining the SQLite file, I found that indeed the new object had been inserted and had all four properties values assigned by the above code.
I agree with #vadian that using the dispatch_semaphore in there is strange, although I don't see any reason it would cause saving to fail. Just to prove that, in a subsequent test I added those three lines using dispatch_semaphore, retested, and it still worked.
The most likely source of the trouble is in your use of CoreDataManager. Notice that in my code, I simply used the raw `-[NSManagedObjectContext save:].
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.
For the purpose of ensuring I have chosen the correct NSMergePolicy I am curious as to whether a value being set to its current value is capable of causing a merge conflict across multiple contexts.
Specifically, in my case I want to ensure that a modified flag will conflict and be preserved if set at an inopportune moment.
Example:
//...
//on background thread, doing some work to an object because it's status was
//set to Status_Modified
[managedObjectContext performBlockAndWait:^{
object.status = Status_NotModified;
[managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[managedObjectContext save:&error];
}];
What if while this is going on, on the main thread, the status is set to Status_Modified and saved? Will the objects status stay as Status_Modified? I.e. will the 'status' property be considered to be changed and so cause a conflict and therefore trump our in memory change (according to the policy)?
So, I cannot find any decent documentation to answer this question, but I have done some tests and it seems that the property is considered to be changed. This was my suspicion and seems to agree with various references to key-value setting being wrapped by will/didSetValueForKey:
My test:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSManagedObjectContext * uiCtx = [self contextForUIWork];
NSEntityDescription * entityDesc = [NSEntityDescription entityForName:#"Entity" inManagedObjectContext:uiCtx];
Entity * entity = [[Entity alloc] initWithEntity:entityDesc insertIntoManagedObjectContext:uiCtx];
entity.testproperty = #(1);
NSError * error = nil;
[uiCtx save:&error];
if (error)
{
assert(0);
}
NSManagedObjectID * objID = entity.objectID;
[self doStuffToObjectWithIDOnAnotherThreadAndAnotherContext:objID];
entity.testproperty = #(2);
[uiCtx setMergePolicy:NSErrorMergePolicy];
error = nil;
[uiCtx save:&error];
if (!error)
{
//We do not hit this! Success!
assert(0);
}
}
-(void)doStuffToObjectWithIDOnAnotherThreadAndAnotherContext:(NSManagedObjectID*)objID
{
dispatch_barrier_sync(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSManagedObjectContext * bgCtx = [self contextForBackgroundWork];
Entity * bgEntity = (Entity*)[bgCtx objectWithID:objID];
[bgCtx performBlockAndWait:^{
//set to same value
bgEntity.testproperty = bgEntity.testproperty;
NSError * bgError = nil;
[bgCtx save:&bgError];
if (bgError)
{
assert(0);
}
}];
});
}
(Full test code uploaded here: https://github.com/samskiter/CoreDataValueChangingTest )
A citation from the docs confirming this would be far better than just some test that shows it works on this particular version of iOS.
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];
}];
}