Multithreaded Core Data - NSManagedObject invalidated - ios

As the title suggests im working with a Core Data Application which gets filled with objects in different background threads (XML Parsing)
In my background thread I'm doing this
managedContext = [[NSManagedObjectContext alloc] init];
[managedContext setUndoManager:nil];
[managedContext setPersistentStoreCoordinator: [[DataManager sharedManager] persistentStoreCoordinator]];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:managedContext];
NSMutableArray *returnSource = [[self parseDocument:doc] retain];
[managedContext save:&error];
if (error) {
NSLog(#"saving error in datafeed");
}
[managedContext reset];
[self performSelectorOnMainThread:#selector(parseCompleteWithSource:) withObject:returnSource waitUntilDone:YES];
The Merge method looks like this:
NSManagedObjectContext *mainContext = [[DataManager sharedManager] managedObjectContext];
// Merge changes into the main context on the main thread
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
[[NSNotificationCenter defaultCenter] removeObserver:self];
I think the merge is successful but as i want to display it in an UITableView it always tells me that my objects are invalidated which is to be expected because of
[managedContext reset];
What i want to do is show the Items which are currently in the database, in the background parse the xml and if thats finished i want to update the UITableView with the new / updated objects. How would I do that, can i "update" the objects to the other Context somehow or is the merge not working correctly?
Do I need to define something specific in the Main ObjectContext?
I've tried different mergepolicies without any luck.
Hope you can help me, thanks!

I believe your problem is the contents of the returnSource array. If that is a bunch of NSManagedObject instances then they will have been created on the background thread by the background thread context.
You call to -[NSManagedObjectContext reset] will invalidate them, since that is what you explicitly tell the context to do. But that is not the big problem.
You then go on to send the array to a the main thread, passing NSManagedObjectinstances over thread borders, and between contexts is a big no-no.
What you need to do is:
Create an array with the NSManagedObjectIDs of the NSManagedObject.
Send the object ID array over thread boundry.
Recreate an array with NSManagedObjects from the managed object IDs on the new thread with it's context.
I have made some Core Data helpers, following the rule of three (the third time you write something, make it general).
Most importantly I have hidden the complexity of managing different managed object contexts for each thread, handling notifications, and all that junk. Instead I have introduced the concept of thread local contexts. Basically lazily created NSManagedObjectContext instances that automatically registers for updates and cleanup when the current thread exits.
A normal use-case:
NSManagedObjectCotext* context = [NSManagedObjectCotext threadLocalContext];
// Do your stuff!!
NSError* error = nil;
if (![context saveWithFailureOption:NSManagedObjectContextCWSaveFailureOptionThreadDefault
error:&error])
{
// Handle error.
}
The full source code, including a sample app for parsing the news RSS from apple.com and store it in Core Data, is available here: https://github.com/jayway/CWCoreData.

There is no reason in this case to call reset on the background context because it will disappear anyway with the background thread and you never use it again in any case. You usually use reset with undo management when you want the context to forget previous steps.
Your major problem here is that your background thread is configured to receive notifications from the managed object context it creates. That is rather pointless.
Instead, you need to register the tableview controller (on the front thread) to receive the NSManagedObjectContextDidSaveNotification from the context on background thread. That way, when the background context saves, the front/main context will update itself with the new data which will trigger an update of the tableview.

Related

best way to concurrently fetch data from a server and modify core data graph as the data arrives? [duplicate]

Question: How do I get my child context to see changes persisted on the parent context so that they trigger my NSFetchedResultsController to update the UI?
Here's the setup:
You've got an app that downloads and adds lots of XML data (about 2 million records, each roughly the size of a normal paragraph of text) The .sqlite file becomes about 500 MB in size. Adding this content into Core Data takes time, but you want the user to be able to use the app while the data loads into the data store incrementally. It's got to be invisible and imperceptible to the user that large amounts of data are being moved around, so no hangs, no jitters: scrolls like butter. Still, the app is more useful, the more data is added to it, so we can't wait forever for the data to be added to the Core Data store. In code this means I'd really like to avoid code like this in the import code:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
The app is iOS 5 only so the slowest device it needs to support is an iPhone 3GS.
Here are the resources I've used so far to develop my current solution:
Apple's Core Data Programming Guide: Efficiently Importing Data
Use Autorelease Pools to keep the memory down
Relationships Cost. Import flat, then patch up relationships at the end
Don't query if you can help it, it slows things down in an O(n^2) manner
Import in Batches: save, reset, drain and repeat
Turn off the Undo Manager on import
iDeveloper TV - Core Data Performance
Use 3 Contexts: Master, Main and Confinement context types
iDeveloper TV - Core Data for Mac, iPhone & iPad Update
Running saves on other queues with performBlock makes things fast.
Encryption slows things down, turn it off if you can.
Importing and Displaying Large Data Sets in Core Data by Marcus Zarra
You can slow down the import by giving time to the current run loop,
so things feel smooth to the user.
Sample Code proves that it is possible to do large imports and keep the UI responsive, but not as fast as with 3 contexts and async saving to disk.
My Current Solution
I've got 3 instances of NSManagedObjectContext:
masterManagedObjectContext - This is the context that has the NSPersistentStoreCoordinator and is responsible for saving to disk. I do this so my saves can be asynchronous and therefore very fast. I create it on launch like this:
masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
mainManagedObjectContext - This is the context the UI uses everywhere. It is a child of the masterManagedObjectContext. I create it like this:
mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];
backgroundContext - This context is created in my NSOperation subclass that is responsible for importing the XML data into Core Data. I create it in the operation's main method and link it to the master context there.
backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];
This actually works very, VERY fast. Just by doing this 3 context setup I was able to improve my import speed by over 10x! Honestly, this is hard to believe. (This basic design should be part of the standard Core Data template...)
During the import process I save 2 different ways. Every 1000 items I save on the background context:
BOOL saveSuccess = [backgroundContext save:&error];
Then at the end of the import process, I save on the master/parent context which, ostensibly, pushes modifications out to the other child contexts including the main context:
[masterManagedObjectContext performBlock:^{
NSError *parentContextError = nil;
BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];
Problem: The problem is that my UI will not update until I reload the view.
I have a simple UIViewController with a UITableView that is being fed data using a NSFetchedResultsController. When the Import process completes, the NSFetchedResultsController see's no changes from the parent/master context and so the UI doesn't automatically update like I'm used to seeing. If I pop the UIViewController off the stack and load it again all the data is there.
Question: How do I get my child context to see changes persisted on the parent context so that they trigger my NSFetchedResultsController to update the UI?
I have tried the following which just hangs the app:
- (void)saveMasterContext {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
NSError *error = nil;
BOOL saveSuccess = [masterManagedObjectContext save:&error];
[notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == mainManagedObjectContext) return;
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(contextChanged:) withObject:notification waitUntilDone:YES];
return;
}
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
You should probably save the master MOC in strides as well. No sense having that MOC wait until the end to save. It has its own thread, and it will help keep memory down as well.
You wrote:
Then at the end of the import process, I save on the master/parent
context which, ostensibly, pushes modifications out to the other child
contexts including the main context:
In your configuration, you have two children (the main MOC and the background MOC), both parented to the "master."
When you save on a child, it pushes the changes up into the parent. Other children of that MOC will see the data the next time they perform a fetch... they are not explicitly notified.
So, when BG saves, its data is pushed to MASTER. Note, however, that none of this data is on disk until MASTER saves. Furthermore, any new items will not get permanent IDs until the MASTER saves to disk.
In your scenario, you are pulling the data into the MAIN MOC by merging from the MASTER save during the DidSave notification.
That should work, so I'm curious as to where it is "hung." I will note, that you are not running on the main MOC thread in the canonical way (at least not for iOS 5).
Also, you probably only are interested in merging changes from the master MOC (though your registration looks like it is only for that anyway). If I were to use the update-on-did-save-notification, I'd do this...
- (void)contextChanged:(NSNotification*)notification {
// Only interested in merging from master into main.
if ([notification object] != masterManagedObjectContext) return;
[mainManagedObjectContext performBlock:^{
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
// NOTE: our MOC should not be updated, but we need to reload the data as well
}];
}
Now, for what may be your real issue regarding the hang... you show two different calls to save on the master. the first is well protected in its own performBlock, but the second is not (though you may be calling saveMasterContext in a performBlock...
However, I'd also change this code...
- (void)saveMasterContext {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
// Make sure the master runs in it's own thread...
[masterManagedObjectContext performBlock:^{
NSError *error = nil;
BOOL saveSuccess = [masterManagedObjectContext save:&error];
// Handle error...
[notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}];
}
However, note that the MAIN is a child of MASTER. So, it should not have to merge the changes. Instead, just watch for the DidSave on the master, and just refetch! The data is sitting in your parent already, just waiting for you to ask for it. That's one of the benefits of having the data in the parent in the first place.
Another alternative to consider (and I'd be interested to hear about your results -- that's a lot of data)...
Instead of making the background MOC a child of the MASTER, make it a child of the MAIN.
Get this. Every time the BG saves, it automatically gets pushed into the MAIN. Now, the MAIN has to call save, and then the master has to call save, but all those are doing is moving pointers... until the master saves to disk.
The beauty of that method is that the data goes from the background MOC straight into your applications MOC (then passes through to get saved).
There is some penalty for the pass-through, but all the heavy lifting gets done in the MASTER when it hits the disk. And if you kick those saves on the master with performBlock, then main thread just sends off the request, and returns immediately.
Please let me know how it goes!

Core Data: Parent context and change propagation

I have the following Core Data setup in my app:
Persistent Store Coordinator
^ Background MOC (NSPrivateQueueConcurrencyType)
^ Main Queue MOC (NSMainQueueConcurrencyType)
Here is the initialization code:
_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundContext setPersistentStoreCoordinator:self.coordinator];
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainContext setParentContext:_backgroundContext];
I use the background MOC for importing large amounts of data. I also use it to perform complex fetch requests in the background and then pass the object IDs to the main queue to fetch the objects using these IDs.
This works quite well. However, I am not sure how to let the main queue MOC know about the changes made in the background MOC. I know that if I perform a fetch request on the main queue MOC, it will get the changes, but that's not what I want.
Is it OK to use the NSManagedObjectContextObjectsDidChangeNotification notification posted by the background MOC and call mergeChangesFromContextDidSaveNotification: on the main queue MOC? This should then cause the NSManagedObjectContextObjectsDidChangeNotification notification of the main queue MOC to fire. I am listening for this notification in my view controllers and examine the userInfo for changes and redisplay data accordingly.
I think you usually do it this way if you have one persistent store coordinator with two attached MOCs. But I am not sure if it is the right way to do, when you have child/parent contexts.
Having the main MOC use a private parent MOC for asynchronous I/O is fine. However, you should not use that parent MOC for anything but performing background work on behalf of the main MOC. There are many reasons for this (among them performance and nasty issues related to transient object IDs).
If you want to do background updating of the store, here is what I suggest.
PSC <--+-- PrivateMOC <---- MainMOC
|
+-- BackgroundPrivateMOC
This will allow background operation that causes the least interruption to the main MOC, while allowing the PSC caches to be shared.
Now, for sharing data...
The MainMOC should listen for and merge DidSave notifications from the BackgroundPrivateMO.
The BackgroundMOC can listen for and merge DidSave notifications from the PrivateMOC.
This allows merging to use only permanent object IDs and optimizes performance.
I'd say that listening to NSManagedObjectContextObjectsDidChangeNotification notification is not probably the best solution.
The way I do it and it works is following.
Here is main context creation:
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
_mainContext.persistentStoreCoordinator = _persistentStoreCoordinator;
Here is background context creation:
_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
_backgroundContext.parentContext = self.mainContext;
Now, background context is only for writing (or reading) objects (may be in background thread). Main context is only for reading from the main queue.
Save on background context should look like:
__block BOOL saved = [_backgroundContext save:error];
if (saved && _backgroundContext.parentContext) {
[_backgroundContext.parentContext performBlockAndWait:^{
saved = [self.parentContext save:error];
}];
}
This save method guarantees that all changes will be propagated to main context. If you do a lot of work in many background threads get more familiar with performBlockAndWait: method, which provides mutual exclusion on context.
If you want to be notified about objects' changes, you don't have to listen for notification, you can simply setup NSFetchedResultsController and register as its delegate.

Executing Core Data saving on a background thread?

I've got a button which marks a selected entry in a Core Data SQLite as a "Favourite", meaning I'm just flipping a BOOL for that index from off to on.
Currently, when I do this, I call save on the managedObjectContext, which takes maybe 500ms, perhaps a little more, according to Instruments.
I have some code that executes at the same time which triggers a nifty little particle explosion ("Hooray, a favourite!"), but I'm running into an issue where that explosion is delayed until after the save has completed.
I'm not sure why, since the code to trigger the explosion is before the save call. I'm a relatively new programmer so perhaps I'm missing something, but doesn't code execute line by line in a case like this, in that the explosion would trigger, then the save would occur while it's going? The delegate call here may be taking some time too, but the same question applies, why would it matter if it's after those lines of code?
EDIT: Am I right in saying that the main thread is being blocked before the particles appear by the next lines of code, meaning the UI can't update itself?
Here's my code:
// Particle animation
LikeExplosion *likeExplosionView = [[LikeExplosion alloc] initWithFrame: CGRectMake(0, 0, 320, 400)];
[likeExplosionView setUserInteractionEnabled: NO];
[self.view addSubview: likeExplosionView];
[self.view bringSubviewToFront: likeExplosionView];
[likeExplosionView decayOverTime: 1.1];
// Delegate call to reload tableview elsewhere
[self.delegate detailViewControllerDidLikeLine];
// Update current object
[_selectedLine setIsLiked: [NSNumber numberWithBool: YES]];
[_selectedLine setIsDisliked: [NSNumber numberWithBool: NO]];
// Update context
NSError *error;
if (![[[CDManager sharedManager] managedObjectContext] save:&error]) NSLog(#"Saving changes failed: %#, %#", error, [error userInfo]);
First question: why is there a delay, when I'm calling the animation code first in the method?
Second question: would putting the save call on a background thread solve the problem, and is it safe / a good idea to do so?
Animations, and generally anything have to do with the UI is executed on the main thread. If you don't want the persistence to disk (the saving process) to hold up your UI (main thread) you need to put the context on it own private queue via NSManagedObjectContext's initWithConcurrencyType: method. The private queue will handle any background threads having to do with the context for you. The three types are:
NSConfinementConcurrencyType
NSPrivateQueueConcurrencyType
NSMainQueueConcurrencyType
You would want NSPrivateQueueConcurrencyType.
You could take a more complex architecture route by using child/nested managed object contexts with different concurrency types, but if you are new to Core Data, stick with a single context until you get a firm grasp of contexts and queues.
1) your animation will not start running until the main runloop complete its cycle. this cycle will not complete as save: is a blocking method.
2) Moving your save to a background thread will solve the issue, but you must access the main managedObjectContext in the main thread, so, you either have to use a background context:
NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:context];
[context performBlock:^{
//make changes
NSError* error = nil;
[context save:&error];
//remember to remove observer after the save (in mergeChanges: and dealloc)
}];
You might be able to start the animation without moving to background by scheduling the save on the main thread in the next runloop using: [self performSelectorOnMainThread:#selector(saveMain) withObject:nil waitUntilDone:NO];
Check out this great article on CIMGF.com! ;)
After reading the tutorial you should know how to approach this problem.

Implementing Fast and Efficient Core Data Import on iOS 5

Question: How do I get my child context to see changes persisted on the parent context so that they trigger my NSFetchedResultsController to update the UI?
Here's the setup:
You've got an app that downloads and adds lots of XML data (about 2 million records, each roughly the size of a normal paragraph of text) The .sqlite file becomes about 500 MB in size. Adding this content into Core Data takes time, but you want the user to be able to use the app while the data loads into the data store incrementally. It's got to be invisible and imperceptible to the user that large amounts of data are being moved around, so no hangs, no jitters: scrolls like butter. Still, the app is more useful, the more data is added to it, so we can't wait forever for the data to be added to the Core Data store. In code this means I'd really like to avoid code like this in the import code:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
The app is iOS 5 only so the slowest device it needs to support is an iPhone 3GS.
Here are the resources I've used so far to develop my current solution:
Apple's Core Data Programming Guide: Efficiently Importing Data
Use Autorelease Pools to keep the memory down
Relationships Cost. Import flat, then patch up relationships at the end
Don't query if you can help it, it slows things down in an O(n^2) manner
Import in Batches: save, reset, drain and repeat
Turn off the Undo Manager on import
iDeveloper TV - Core Data Performance
Use 3 Contexts: Master, Main and Confinement context types
iDeveloper TV - Core Data for Mac, iPhone & iPad Update
Running saves on other queues with performBlock makes things fast.
Encryption slows things down, turn it off if you can.
Importing and Displaying Large Data Sets in Core Data by Marcus Zarra
You can slow down the import by giving time to the current run loop,
so things feel smooth to the user.
Sample Code proves that it is possible to do large imports and keep the UI responsive, but not as fast as with 3 contexts and async saving to disk.
My Current Solution
I've got 3 instances of NSManagedObjectContext:
masterManagedObjectContext - This is the context that has the NSPersistentStoreCoordinator and is responsible for saving to disk. I do this so my saves can be asynchronous and therefore very fast. I create it on launch like this:
masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
mainManagedObjectContext - This is the context the UI uses everywhere. It is a child of the masterManagedObjectContext. I create it like this:
mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];
backgroundContext - This context is created in my NSOperation subclass that is responsible for importing the XML data into Core Data. I create it in the operation's main method and link it to the master context there.
backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];
This actually works very, VERY fast. Just by doing this 3 context setup I was able to improve my import speed by over 10x! Honestly, this is hard to believe. (This basic design should be part of the standard Core Data template...)
During the import process I save 2 different ways. Every 1000 items I save on the background context:
BOOL saveSuccess = [backgroundContext save:&error];
Then at the end of the import process, I save on the master/parent context which, ostensibly, pushes modifications out to the other child contexts including the main context:
[masterManagedObjectContext performBlock:^{
NSError *parentContextError = nil;
BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];
Problem: The problem is that my UI will not update until I reload the view.
I have a simple UIViewController with a UITableView that is being fed data using a NSFetchedResultsController. When the Import process completes, the NSFetchedResultsController see's no changes from the parent/master context and so the UI doesn't automatically update like I'm used to seeing. If I pop the UIViewController off the stack and load it again all the data is there.
Question: How do I get my child context to see changes persisted on the parent context so that they trigger my NSFetchedResultsController to update the UI?
I have tried the following which just hangs the app:
- (void)saveMasterContext {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
NSError *error = nil;
BOOL saveSuccess = [masterManagedObjectContext save:&error];
[notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == mainManagedObjectContext) return;
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(contextChanged:) withObject:notification waitUntilDone:YES];
return;
}
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
You should probably save the master MOC in strides as well. No sense having that MOC wait until the end to save. It has its own thread, and it will help keep memory down as well.
You wrote:
Then at the end of the import process, I save on the master/parent
context which, ostensibly, pushes modifications out to the other child
contexts including the main context:
In your configuration, you have two children (the main MOC and the background MOC), both parented to the "master."
When you save on a child, it pushes the changes up into the parent. Other children of that MOC will see the data the next time they perform a fetch... they are not explicitly notified.
So, when BG saves, its data is pushed to MASTER. Note, however, that none of this data is on disk until MASTER saves. Furthermore, any new items will not get permanent IDs until the MASTER saves to disk.
In your scenario, you are pulling the data into the MAIN MOC by merging from the MASTER save during the DidSave notification.
That should work, so I'm curious as to where it is "hung." I will note, that you are not running on the main MOC thread in the canonical way (at least not for iOS 5).
Also, you probably only are interested in merging changes from the master MOC (though your registration looks like it is only for that anyway). If I were to use the update-on-did-save-notification, I'd do this...
- (void)contextChanged:(NSNotification*)notification {
// Only interested in merging from master into main.
if ([notification object] != masterManagedObjectContext) return;
[mainManagedObjectContext performBlock:^{
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
// NOTE: our MOC should not be updated, but we need to reload the data as well
}];
}
Now, for what may be your real issue regarding the hang... you show two different calls to save on the master. the first is well protected in its own performBlock, but the second is not (though you may be calling saveMasterContext in a performBlock...
However, I'd also change this code...
- (void)saveMasterContext {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
// Make sure the master runs in it's own thread...
[masterManagedObjectContext performBlock:^{
NSError *error = nil;
BOOL saveSuccess = [masterManagedObjectContext save:&error];
// Handle error...
[notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}];
}
However, note that the MAIN is a child of MASTER. So, it should not have to merge the changes. Instead, just watch for the DidSave on the master, and just refetch! The data is sitting in your parent already, just waiting for you to ask for it. That's one of the benefits of having the data in the parent in the first place.
Another alternative to consider (and I'd be interested to hear about your results -- that's a lot of data)...
Instead of making the background MOC a child of the MASTER, make it a child of the MAIN.
Get this. Every time the BG saves, it automatically gets pushed into the MAIN. Now, the MAIN has to call save, and then the master has to call save, but all those are doing is moving pointers... until the master saves to disk.
The beauty of that method is that the data goes from the background MOC straight into your applications MOC (then passes through to get saved).
There is some penalty for the pass-through, but all the heavy lifting gets done in the MASTER when it hits the disk. And if you kick those saves on the master with performBlock, then main thread just sends off the request, and returns immediately.
Please let me know how it goes!

NSFetchRequest not seeing changes to relationships in NSManagedObjectContext

I am using a UIManagedDocument for core data. I have Calendar objects in my managedObjectContext that have a calendarSelected attribute. The Calendar entity has a to-many relationship to CalendarEntry.
When I change their calendarSelected attribute and then perform a NSFetchRequest to get CalendarEntry objects with the following predicate:
[NSPredicate predicateWithFormat:#"calendar.calendarSelected = YES"]
the calendar.calendarSelected does not seem to be seeing the change I made without me calling
[myManagedDocument saveToURL:myManagedDocument.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {}];
first. I read somewhere that fetching things from the same context should honor changes made to in that context even if the changes had not been written to the persistent store. What am I missing?
Update:
It appears to be happening when the calendarEvents relationships is a fault: calendarEvents = "<relationship fault: 0x91aec90 'calendarEvents'>"; but works when the relationship is not a fault.
If the issue occurs only on newly-inserted CalendarEntry objects pointed to by the to-many relationship, then it's possible that they don't yet have permanent object ids. This is easy to debug via just dumping the object id and checking to see if it's temporary or permanent.
I've seen this happen when the containing object in the to-many relationship is retained; it seems that so long as it is retained, the to-many contained objects in the relationship never get to a point wherein they obtain permanent ids. Somewhat easy to debug by putting the application in the background and restarting it; the backgrounding will typically force the UIManagedDocument to save, and things will start working as you expect thereafter, as the CalendarEntry entities will have been assigned permanent ids and thus will become fetchable.
So far as saving the UIManagedDocument, you don't have control over when that'll happen when using a UIManagedDocument. The best thing to do is schedule a save to occur in the future, via updateChangeCount:UIDocumentChangeDone, but again, it'll happen 'soon', but not deterministically, i.e., it's not possible to know when it'll happen.
To resolve the temporary vs. permanent object id issue, if that's what you're seeing, try the following category on NSManagedObjectContext; call it after you've completed inserting new CalendarEntry objects, in order to force the issue.
// Obtain permanent ids for any objects that have been inserted into
// this context.
//
// As of this writing (iOS 5.1), we need to manually obtain permanent
// ids for any inserted objects prior to a save being performed, or any
// objects that are currently retained, that have relationships to the
// inserted objects, won't be updated with permanent ids for the related
// objects when a save is performed unless the retained object is refetched.
// For many objects, that will never occur, so we need to force the issue.
- (BOOL)obtainPermanentIDsForInsertedObjects:(NSError **)error
{
NSSet * inserts = [self insertedObjects];
if ([inserts count] == 0) return YES;
return [self obtainPermanentIDsForObjects:[inserts allObjects]
error:error];
}
Basically, after making any changes, you must either use an undo manager, or call [document updateChangeCount:UIDocumentChangeDone]; and THAT'S ALL! Any other "save" calls will just break something else down the line somewhere.
The canonical method of updating should be...
You know you are on main thread
NSManagedObjectContext *ctx = document.managedObjectContext;
// Do stuff with it
[document updateChangeCount:UIDocumentChangeDone];
You are on a different thread
[document.managedObjectContext performBlock:^{
// Do stuff with it on the main thread
[document updateChangeCount:UIDocumentChangeDone];
}];
OR
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = document.managedObjectContext;
[moc performBlock:^{
// Do stuff with it on a background thread
// Push changes up to parent (which is the document's main MOC)
// The document will know it's dirty and save changes
[moc save:&error];
}];
OR if you want to make changes in a background thread, without messing with the main document MOC, do them on the parent... the main MOC will not see them until the next fetch.
[document.parentContext.managedObjectContext performBlock:^{
// Do stuff with it on a background thread, but the main MOC does not
// see the changes until it does a fetch.
}];

Resources