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.
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 just wanted to be clear about core data private context. I am trying to insert 20k record using private context (NSPrivateQueueConcurrencyType). But as soon as 'Insert 20k records' button is tapped UI thread hangs.
- (IBAction)insertRecords:(id)sender {
[[CoreDataStore privateContext] performBlock:^{
NSLog(#"Starting to insert 20k records...");
for (int i = 0 ; i < 20000; i++)
{
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:[CoreDataStore privateContext]];
[newManagedObject setValue:[NSDate date] forKey:#"time"];
}
NSError * error = nil;
NSLog(#"Inserted 20k records to managed object context");
[[CoreDataStore privateContext] save:&error];
NSLog(#"Save context command fired");
}];
NSLog(#"returning from insert method");
}
changes from private context are being merged to main context here
- (void)contextDidSavePrivateQueueContext:(NSNotification *)notification
{
#synchronized(self) {
[self.mainContext performBlock:^{
NSLog(#"merging changes to main context.....");
[self.mainContext mergeChangesFromContextDidSaveNotification:notification];
NSLog(#"merged changes to main context");
}];
}
}
Below is log window output when I hit insert button:
2014-08-04 14:54:12.431 CoreDataDrillDown[11323:90b] returning from insert method
2014-08-04 14:54:12.431 CoreDataDrillDown[11323:1403] Starting to insert 20k records...
2014-08-04 14:54:12.506 CoreDataDrillDown[11323:1403] Inserted 20k records to managed object context
2014-08-04 14:54:12.785 CoreDataDrillDown[11323:90b] merging changes to main context.....
2014-08-04 14:54:12.786 CoreDataDrillDown[11323:1403] Save context command fired
2014-08-04 14:54:27.019 CoreDataDrillDown[11323:90b] merged changes to main context
For more than 15 sec UI was irresponsive. I want to know the reason why?
With below approach suggested by https://stackoverflow.com/users/817182/thom-ek and https://stackoverflow.com/users/2128900/micha%c5%82-ciuba, insertion is quick and does not freeze UI but changes are not getting saved to disk, not sure why.
//writerContext has persistent store coordinator, so it should write data to disk but it
//is not writing any changes to disk.???????
-(NSManagedObjectContext *)writerContext
{
if (!_writerContext)
{
_writerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_writerContext.persistentStoreCoordinator = [(AppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator];
}
return _writerContext;
}
//mainContext is being used with NSFetchedResultController
-(NSManagedObjectContext *)mainContext
{
if (!_mainContext)
{
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.parentContext = [self writerContext];
}
return _mainContext;
}
//performBlock is being called on privateContext
-(NSManagedObjectContext *)privateContext
{
if (!_privateContext)
{
_privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_privateContext.parentContext = [self mainContext];
}
return _privateContext;
}
Main problem is that mainContext is still tied to mainQueue, so it will wait for this huge merge.
There are good articles about asynchronous saves: on Cocoanetics and ObjC.io.
Other solution is just to reset mainContext and reload all tableViews (you don’t show all of 20k records to user at once).
I'm trying to separate my application work when there is a bigger work to do to optimize performance. My problem is about a NSManagedObjectContext used in another thread than the main one.
I'm calling:
[NSThread detachNewThreadSelector:#selector(test:) toTarget:self withObject:myObject];
On the test method there are some stuff to do and I have a problem here:
NSArray *fetchResults = [moc
executeFetchRequest:request
error:&error];
Here is my test method:
-(void) test:(MyObject *)myObject{
#autoreleasepool {
//Mycode
}
}
The second time I call the test method, my new thread is blocked when the executeFetchRequest is called.
This problem arrived when my test method is called more than one time in succession. I think the problem comes from the moc but I can't really understand why.
Edit:
With #Charlie's method it's almost working. Here is my code to save my NSManagedObjectContext (object created on my new thread).
- (void) saveContext:(NSManagedObjectContext *) moc{
NSError *error = nil;
if ([moc hasChanges] && ![moc save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
This method is called on the new thread. My problem now is that with this save, I have a deadlock and I don't really understand why. Without it's perfectly working.
Edit2
I'm working on this issue but I still can't fix it. I changed my code about the detachNewThreadSelector. Here is my new code:
NSManagedObjectContext* context = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = self.persistentStoreCoordinator;
context.undoManager = nil;
[context performBlock:^
{
CCImages* cachedImage;
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childContext.parentContext = context;
cachedImage=[CCImages getCCImageForKey:path inManagedObjectContext:childContext];
UIImage *image = [self getImageFromCacheWithPath:path andCachedImage:cachedImage atDate:now];
if (image != nil){
if(![weakSelf.delegate respondsToSelector:#selector(CacheCacheDidLoadImageFromCache:)])
[weakSelf setDelegate:appDelegate.callbacksCollector];
//[weakSelf useCallbackCollectorForDelegate:weakSelf inMethod:#"initPaginatorForListMoments"];
[weakSelf.delegate CacheCacheDidLoadImageFromCache:image];
}
}
- (UIImage*) getImageFromCacheWithPath:(NSString*) path andCachedImage:(CCImages *) cachedImage atDate: (NSDate *) now{
NSURL* localURL=[NSURL URLWithString:cachedImage.path relativeToURL:[self imageCacheDirectory]];
UIImage * image;
//restore uiimage from local file system
if (localURL) {
image=[UIImage imageWithContentsOfFile:[localURL path]];
//update cache
[cachedImage setLastAccessedAt:now];
[self saveContext];
if(image)
return image;
}
return nil;
}
Just after that, I'm saving my contexts (manually for now)
[childContext performBlock:^{
NSError *error = nil;
if (![childContext save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
else{
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
}];
}
}];
There is a strange problem. My call back method is called without any problem on my controller (which implements the CacheCacheDidLoadImageFromCache: method). On this method I attest the reception of the image (DDLogInfo) and say that I want my spinner to stop. It does not directly but only 15secondes after the callback method was called.
My main problem is that my context (I guess) is still loading my image from the cache while it was already found. I said 'already' because the callback method has been called and the image was present. There is no suspicious activity of the CPU or of the memory. Instruments didn't find any leak.
I'm pretty sure that I'm using wrongly the NSManagedObjectContext but I can't find where.
You are using the old concurrency model of thread confinement, and violating it's rules (as described in the Core Data Concurrency Guide, which has not been updated yet for queue confinement). Specifically, you are trying to use an NSManagedObjectContext or NSManagedObject between multiple threads.
This is bad.
Thread confinement should not be used for new code, only to maintain the compatibility of old code while it's being migrated to queue confinement. This does not seem to apply to you.
To use queue confinement to solve your problem, first you should create a context attached to your persistent store coordinator. This will serve as the parent for all other contexts:
+ (NSManagedObjectContent *) parentContextWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setPersistentStoreCoordinator:coordinator];
return result;
}
Next, you want the ability to create child managed object contexts. You will use these to perform work on the data, wether reading or writing. An NSManagedObjectContext is a scratchpad of the work you are doing. You can think of it as a transaction. For example, if you're updating the store from a detail view controller you would create a new child context. Or if you were performing a multi-step import of a large data set, you would create a child for each step.
This will create a new child context from a parent:
+ (NSManagedObjectContext *) childContextWithParent:(NSManagedObjectContext *)parent {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setParent:parent];
return result;
}
Now you have a parent context, and you can create child contexts to perform work. To perform work on a context, you must wrap that work in performBlock: to execute it on the context's queue. I do not recommend using performBlockAndWait:. That is intended only for re-rentrant methods, and does not provide an autorelease pool or processing of user events (user events are what drives nearly all of Core Data, so they're important. performBlockAndWait: is an easy way to introduce bugs).
Instead of performBlockAndWait: for your example above, create a method that takes a block to process the results of your fetch. The fetch, and the block, will run from the context's queue - the threading is done for you by Core Data:
- (void) doThingWithFetchResults:(void (^)(NSArray *results, NSError *error))resultsHandler{
if (resultsHandler != nil){
[[self context] performBlock:^{
NSArray *fetchResults = [[self context] executeFetchRequest:request error:&error];
resultsHandler(fetchResults, error);
}];
}
}
Which you would call like this:
[self doThingsWithFetchResults:^(NSArray *something, NSError *error){
if ([something count] > 0){
// Do stuff with your array of managed objects
} else {
// Handle the error
}
}];
That said, always prefer using an NSFetchedResultsController over using executeFetch:. There seems to be a belief that NSFetchedResultsController is for powering table views or that it can only be used from the main thread or queue. This is not true. A fetched results controller can be used with a private queue context as shown above, it does not require a main queue context. The delegate callbacks the fetched results controller emits will come from whatever queue it's context is using, so UIKit calls need to be made on the main queue inside your delegate method implementations. The one issue with using a fetched results controller this way is that caching does not work due to a bug.
Again, always prefer the higher level NSFetchedResultsController to executeFetch:.
When you save a context using queue confinement you are only saving that context, and the save will push the changes in that context to it's parent. To save to the store you must recursively save all the way. This is easy to do. Save the current context, then call save on the parent as well. Doing this recursively will save all the way to the store - the context that has no parent context.
Example:
- (void) saveContextAllTheWayBaby:(NSManagedObjectContext *)context {
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]){
// Handle the error appropriately.
} else {
[self saveContextAllTheWayBaby:[context parentContext]];
}
}];
}
You do not, and should not, use merge notifications and mergeChangesFromContextDidSaveNotification: with queue confinement. mergeChangesFromContextDidSaveNotification: is a mechanism for the thread confinement model that is replaced by the parent-child context model. Using it can cause a whole slew of problems.
Following the examples above you should be able to abandon thread confinement and all of the issues that come with it. The problems you are seeing with your current implementation are only the tip of the iceberg.
There are a number of Core Data sessions from the past several years of WWDC that may also be of help. The 2012 WWDC Session "Core Data Best Practices" should be of particular interest.
if you want to use managed object context in background thread, there are two approaches,
1 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set the parentContext to main thread context
2 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set persistentStoreCoordinator to main thread persistentStoreCoordinator
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
NSManagedObjectContext *moc = mainManagedObjectContext;
if (note.object != moc) {
[moc mergeChangesFromContextDidSaveNotification:note];
}
}];
// do work here
// remember managed object is not thread save, so you need to reload the object in private context
});
before exist the thread, make sure remove the observer, bad thing can happen if you don't
for more details read http://www.objc.io/issue-2/common-background-practices.html
in a core data app with a one-to-many relationship (one "test", many "measures"), I used to have this code :
In AppDelegate.m :
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m :
- (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *context = nil;
id contextDelegate = [[UIApplication sharedApplication] delegate];
if ([contextDelegate performSelector:#selector(managedObjectContext)])
context = [contextDelegate managedObjectContext];
return context;
}
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:context];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
}
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
It works great, but of course, the [NSKeyedArchiver archivedDataWithRootObject:plotDataArray]; can take a few seconds and block the UI so I would like to do it in background.
I spent a few hours to read everything about the concurrency in core data (and I am quite new at it), but I didn't find anything regarding my problem : how to deal with a one-to-many relationship background save ?
What I've tried so far :
In AppDelegate.m
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
//_managedObjectContext = [[NSManagedObjectContext alloc] init];
//[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
In TableViewController.m
- (void)saveEntryButton:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
{
// Update existing test
self.test.number = self.numberTextField.text;
}
else // Create new test
{
self.test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
{
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
if (isSaving)
{
NSManagedObjectContext *context = [test managedObjectContext];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = context;
[temporaryContext performBlock:^{
self.measure = [NSEntityDescription insertNewObjectForEntityForName:#"Measure" inManagedObjectContext:temporaryContext];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
// push to parent
NSError *error;
if (![temporaryContext save:&error])
{
// handle error
NSLog(#"error");
}
// save parent to disk asynchronously
[context performBlock:^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSError *error;
if (![context save:&error])
{
// handle error
NSLog(#"error");
}
}];
}];
}
}
Of course, I receive a SIGABRT error as "test" and "measure" are not in the same context...I've tried a LOT of different things, but I'm really lost.
Thanks in advance for any help.
I see two questions here: background asynchronous saving and what to do with objects in different contexts.
First about saving. Are you sure that it is saving itself that blocks your UI thread and not call to archivedDataWithRootObject? If saving itself is relatively fast, you can consider calling only archivedDataWithRootObject on a background queue, and then communicating the results back to the main queue where you’ll do the save on your UI context.
If it is still the save that takes too long, you can use the approach for background asynchronous saving recommended by Apple. You need two contexts. One context – let’s call it background – is of private queue concurrency type. It is also configured with persistent store coordinator. Another context – let’s call it UI – is of main queue concurrency type. It is configured with background context as a parent.
When working with your user interface, you’re using the UI context. So all managed objects are inserted, modified, and deleted in this context. Then when you need to save you do:
NSError *error;
BOOL saved = [UIContext save:&error];
if (!saved) {
NSLog(#“Error saving UI context: %#“, error);
} else {
NSManagedObjectContext *parent = UIContext.parentContext;
[parent performBlock:^{
NSError *parentError;
BOOL parentSaved = [parent save:&parentError];
if (!parentSaved) {
NSLog(#“Error saving parent: %#“, parentError);
}
}];
}
The save of the UI context is very fast because it doesn’t write data to disk. It just pushes changes to its parent. And because parent is of private queue concurrency type and you do the save inside performBlock’s block, that save happens in background without blocking the main thread.
Now about different managed objects in different contexts from your example. As you discovered, you can’t set an object from one context to a property of an object in another context. You need to choose a context where you need to do the change. Then transfer NSManagedObjectID of one of the objects to the target context. Then create a managed object from ID using one of the context’s methods. And finally set this object to a property of another one.
Essentially you are on the right track, but missing a couple of key elements;
Firstly you will need to transfer test from your main context to the secondary - this is done in the following way;
//this is the object saved in your main managedObjectContext;
NSManagedObjectID *currentTest = test.objectID;
creating the secondary context for adding your related objects can be performed on a background thread. You can use and NSBlockOperation to do the secondary save and create the context at the same time.
here is a simple example using the standard person / address example wired to an IBAction
- (IBAction)button1Click:(id)sender {
NSError *saveError = nil;
// create instance of person to save in our primary context
Person *newParson = [[Person alloc]initIntoManagedObjectContext:self.mainContext];
newParson.name = #"Joe";
[self.mainContext save:&saveError];
//get the objectID of the Person saved in the main context
__block NSManagedObjectID *currentPersonid = newParson.objectID;
//we'll use an NSBlockOperation for the background processing and save
NSBlockOperation *addRelationships = [NSBlockOperation blockOperationWithBlock:^{
// create a second context
NSManagedObjectContext *secondContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[secondContext setPersistentStoreCoordinator:coordinator];
NSError *blockSaveError = nil;
/// find the person record in the second context
Person *differentContextPerson = (Person*)[secondContext objectWithID:currentPersonid];
Address *homeAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
homeAddress.address = #"2500 1st ave";
homeAddress.city = #"New York";
homeAddress.state = #"NY";
homeAddress.zipcode = #"12345";
Address *workAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
workAddress.address = #"100 home Ave";
workAddress.city = #"Newark";
homeAddress.state = #"NJ";
homeAddress.zipcode = #"45612";
[differentContextPerson addAddressObject:homeAddress];
[differentContextPerson addAddressObject:workAddress];
[secondContext save:&blockSaveError];
}];
[addRelationships start];
}
in the above initIntoManagedObjectContext is a simple helper method in the NSManagedObject subclass as follows;
- (id)initIntoManagedObjectContext:(NSManagedObjectContext *)context {
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:context];
self = [super initWithEntity:entity insertIntoManagedObjectContext:context];
return self;
}
An important note from Apple docs regarding NSBlockOperation:
You must create the managed context on the thread on which it will be used. If you use NSOperation, note that its init method is invoked on the same thread as the caller. You must not, therefore, create a managed object context for the queue in the queue’s init method, otherwise it is associated with the caller’s thread. Instead, you should create the context in main (for a serial queue) or start (for a concurrent queue).
I'm stuck on this problem with CoreData and Parent-Child MOCs: when adding objects to child MOC, saving them and saving the parent MOC all the objects gets their attributes reset to defaultValue.
I pasted here the logs from the two MOCs, specifically are the "stringAttribute" and "date" attributes that in these log are reset.
I searched for this problem everywhere but i didn't find anything, I also looked at lots of implementation of Parent-Child MOCs but I can't figure out what I'm doing wrong.
Thanks in advance!
Here's the code snippets:
I add some NSManagedObject to the main context and then save with saveContext: method
// Another singleton method
- (void)anotherMethod
{
[...]
[self.managedObjectContext insertObject:managedObject];
NSError *error;
save = [self saveContext:&error];
[...]
}
// Database manager singleton method
- (BOOL)saveContext:(DKError *__autoreleasing *)error
{
__block BOOL save = NO;
__block NSError *internalError;
[self.managedObjectContext performBlockAndWait:^{
internalError = nil;
[self.managedObjectContext log]; // See log 1.1 below
save = [self.managedObjectContext save:&internalError];
if (!save) {
NSLog(#"Error saving data in main context");
} else {
[self.managedObjectContext.parentContext performBlock:^{
internalError = nil;
save = NO;
[self.managedObjectContext.parentContext log]; // See log 1.2 below
save = [self.managedObjectContext.parentContext save:&internalError];
if (!save) {
NSLog(#"Error saving data to disk!");
}
}];
}
}];
*error = [DKError errorWithNSError:internalError]; // Custom error class
return save;
}
Parent - Child contexts code
- (NSManagedObjectContext *)writerObjectContext
{
if (_writerObjectContext != nil)
return _writerObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_writerObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_writerObjectContext setPersistentStoreCoordinator:coordinator];
}
return _writerObjectContext;
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:[self writerObjectContext]];
return _managedObjectContext;
}
Log 1.1
Inserted objects:
{(
<Entity: 0x9595120> (entity: Entity; id: 0x9582d40 <x-coredata:///Entity/t24D0F98B-CB94-41D3-BEDD-79913454A9152> ; data: {
[...]
dateAttribute = "2013-07-12 10:36:31 +0000";
stringAttribute = ricercaEntity;
[...]
})
)}
Log 1.2
Inserted objects:
{(
<Entity: 0xb53ce80> (entity: Entity; id: 0x9582d40 <x-coredata:///Entity/t24D0F98B-CB94-41D3-BEDD-79913454A9152> ; data: {
[...]
dateAttribute = "2013-01-05 11:00:00 +0000";
stringAttribute = nil;
[...]
})
)}
UPDATE
I've should have mention that the managedObject added to the context is initialized with context nil. Then before calling saveContext: I check for existence of object.managedObjectContext and if it's nil then I'll set that as [self managedObjectContext], the one created with the method above. So either if the managedObject is created with nil context, or created with:
+ (id)newObjectForInsertion
{
return [[self alloc] initWithEntity:[self entityDescription] insertIntoManagedObjectContext:[DKDatabaseManager defaultManager].managedObjectContext];
}
the associated managedObjectContext is on the same queue (NSMainQueueConcurrencyType).
Otherwise if the managedObject is create with +newObjectForInsertion all of the saveContext: concurrency-chain return YES and all the changes are passed to parent context.
I don't know if it's a bug or the way CoreData should work.
Same problem on Apple Developer Forums:
https://devforums.apple.com/thread/174677?tstart=90
You should init context with concurrencyType:
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
Also, set merge policy
[context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
NSMergeByPropertyObjectTrumpMergePolicy
This policy merges conflicts between the persistent store’s version of
the object and the current in-memory version, giving priority to
in-memory changes. The merge occurs by individual property. For
properties that have been changed in both the external source and in
memory, the in-memory changes trump the external ones.
Btw, I found similar question: strange-behavior-when-using-child-parent-nsmanagedobjectcontext look at the accepted answer which uses notifications to merge.