Help with multithreaded Core Data app design - objective-c

Above is a simplification of what my model looks like. My app has a NSWindowController object controlling two NSViewController objects for the user and account entities. When a user logs in to the app, they can modify user or account information by bringing up the relevant view controller. In the background I have the application periodically populating the user's logs in the application delegate on a separate thread.
I am using a separate NSManagedObjectContext for the background thread and the application delegate's NSManagedObjectContext for data entry in the view controllers. I would like to know a few things:
1) is this good practice? Should I create a NSManagedObjectContext for each view controller and then merge the contexts whenever the user is done making changes?
2) Because the log entity is created in the background thread, it has it's own NSManagedObjectContext. However, each log includes information from the user and account entities, which are created in the application delegate's NSManagedObjectContext. This is how I am fetching a user:
- (NSManagedObjectID*) fetchUser:(NSString*) userID {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"user":inManagedObjectContext:self.managedObjectContext];
/** snip **/
}
This method is called by the background thread as follows:
NSManagedObjectID* userObjectID = [self fetchUser:userID];
NSManagedObject* userObject = [self.logsManagedObjectContext objectWithID:userObjectID];
Is what I'm doing in fetchUser thread-safe? Do I need to lock the main managed object context while fetching a user in case one of the views is modifying the same user? From this article I understand (perhaps incorrectly) that I may have to do so. So far I haven't run into any problems but I don't want to leave a potential edge case.
3) When one of the view controllers makes changes to the application delegate's NSManagedObjectContext it posts a notification that is handled as follows:
- (void)contextDidSave:(NSNotification *)notification {
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[self.logManagedObectContext performSelector:selector onThread:backgroundThread withObject:notification waitUntilDone:NO];
}
Is this how I should handle the merge or should I be merging the application delegate's NSManagedObjectContext instead? I found that doing that (on the main thread) locked up the UI.
Any help will be appreciated.

NSManagedObjectContext objects are not thread-safe. This means that if you wish to access Core Data from multiple threads, you will need one for each thread (and created on the thread too). Each of these can use the same NSPersistentStoreCoordinator, which will serialise access to the persistent store.
This occurs because each NSManagedObjectContext knows how to properly lock the NSPersistentStoreCoordinator when it is in use, avoiding collisions. By following these rules, you should remain thread-safe.
As you're already doing, NSManagedObjectID objects should be used to pass Core Data objects from one MOC to another (and by extension from one thread to another). However you are calling fetchUser: which uses the MOC from your main thread, on a background one. This isn't correct. That fetchUser: method call must be called from the main thread. Of course, there's nothing to stop you from retrieving the user in the background thread using the background MOC.
In summary, always make calls to an NSManagedObjectContext from the thread it was created in.
The trick here is to make sure that both MOCs know about the other's saves, so you must register to receive the notifications from each context. You should then be performing the mergeChangesFromContextDidSaveNotification: from the appropriate thread for the MOC. At the moment, your background context is being notified about changes from the main thread's context, but not vice versa.
Oh, and there's no need to have a separate context for each NSViewController. As UI elements, their interactions with the context will occur on the same (main) thread, so sharing is fine.

Related

Core Data separate managed object context sharing same persistent store does not see changes

I am really baffled today by what I discovered.
I thought as long as all context are using the same Core Data persistent store, any changes in one context should appear in the other context after saving the context.
E.g. In view controller A I have 1 context (context A), in view controller B, I have another context (context B). Now both context A and context B point to the same persistent store.
In context A, I fetched a managed object from the persistent store, updated a property of the managed object, then I save the changes back to the persistent store with managedObjectContext save operation.
Now I open my second view controller and perform a fetch request from the same persistent store but my second view controller does not see the updated property change, until I restart the simulator.
The really strange thing is, if it's the first time I insert a new managed object into the persistent store, controller B will see the changes but subsequent changes are not shown.
I have already fixed this problem after a long battle, I just want to know why having two separate context (both on main thread of course) sharing the same persistent store does not see the changes until simulator restart.
For those who want to know how I fixed it, in my base view controller which controller A and B both inherit from, instead of alloc-initing a new context (a hence why controller A and B have two separate contexts), I told the base view controller to reference the context in the app delegate (as a result, controller A and B now point to the same context).
Here's a diagram explaining what I am seeing:
The resulting value fetched in second view controller is the old value.
If I commit some data to the persistent store, it should become permanent and accessible ANYWHERE as long as I am fetching from that same persistent store, unless I am misunderstanding that managedObjectContext save: doesn't actually make the changes to persistent store immediately until the app results.
Zhang,
without details it's difficult to figure out the problem.
A simple suggestion is to verify if you merge changes between the two different context within your app. In other words, you need to verify the main context merges changes that come from the other one. This could simple achieved like the following.
Register for this notification where, for example, in your AppDelegate or you are creating the Core Data stack.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil];
Implement the contextChanged: method to merge changes.
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == [self managedObjectContext])
return;
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(contextChanged:) withObject:notification waitUntilDone:YES];
return;
}
[[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}
For further info see Marcus Zarra tutorial.
In addition, if you use tables in combination with NSFetchedResultsController remember to implement delegate's methods. For info see NSFetchedResultsControllerDelegate class.
Hope that helps.

Best practice for a long-running foreground operation that uses Core Data?

I have an app that imports a potentially large amount of data from the web after the user explicitly presses a Sync button, and stores that data using Core Data. Since I want to show feedback and I don't want the user interacting with the rest of the app while this happens, pressing the Sync button brings up a Modal dialog. Since I want the operation to happen immediately, the operation executes in the viewDidAppear method. I'm sure this is frowned upon.
There are a bunch of problems with the approach right now:
Everything happens in the main thread. The user kind of gets feedback because there is an activity indicator that continues to animate, but there's no way to indicate progress or show intermediate messages. This is not the right way to do things.
But, I am told that when using Core Data, everything has to use the main thread, so breaking off the work into another thread does not seem like it will be straightforward.
If the app enters the background state (user hits Home button or iPad falls sleep), it's game over - the operation dies. It's clear to me from the documentation why this is the case.
I know there are "I'm about to enter the background" events that you can handle, but it's not as though I can move execution of code from one place to another in the middle of a file download. Whatever solution I use has to be a continuous action that executes in the same way both before and after the transitions to/from the background.
I want the operation to execute in the foreground as far as the user is concerned. It does not make sense for them to interact with other parts of the app while this operation is taking place.
I am reading the Apple documentation on this, but I'm asking this in hopes of finding more concise guidance on this particular combination of needs. Thanks.
You really should not freeze the main thread. You can still "prohibit" certain UI actions.
Create a separate context, as a child, and do all your work in there. When done (or at certain intervals), save the context to the main context, and notify the main thread to do some UI update interaction... maybe a progress bar or something...
NSManagedContext *backgroundContext = [NSManagedContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
backgroudContext.parentContext = [self mainManagedObjectContext];
[backgroundContext performBlock:^{
// This block is running in a background thread.
// Go get your data from the web
// Call this to push data to the main MOC (either at end, or at intervals)
[backgroundContext save:&error];
// When you want to do something on the main thread...
dispatch_async(dispatch_get_main_queue(), ^{
// This block is running on the main queue... I can do anything with the UI...
}];
}];
Couple of things to note... your mainMOC needs to be private or main queue concurrency type. If you are using the Core Data template, where it is in the app delegate, just change the alloc/init to initWithConcurrencyType:NSMainQueueConcurrencyType.
I would, however, suggest using the canonical main/parent relationship. Create a private MOC, assign it to the persistent store, then create a main MOC, set its parent to be that private MOC. Now you are ready to handle any I/O with background operations, without blocking your UI.
Still, when loading from the web, use the pattern above: create a child MOC, then load objects into the main MOC.
Note, that the data is not saved to disk until the "root" MOC calls save.

Execute NSFetchRequest on application startup

In another question ( Accessing an NSApplications delegate in another class? ) I asked about calling the Application's delegate because I needed it's managedObjectContext for a fetch request. However, when I try to let all values be displayed in an NSTableView on application startup, I'm running into problems. DataController, my NSTableViewDataSource, calls it's init-method before my application delegate calls it's applicationWillFinishStartup or any other method to initialize the managedObjectContext. What am I doing wrong? How else can I fill an NSTableView with already existing objects?
You should access managedObjectContext only via its getter, even from DataController, as in [appDelegate managedObjectContext] or appDelegate.managedObjectContext.
Your managedObjectContext method should automatically set up the managed object context; you shouldn't write an explicit moc setup routines in your applicationDidFinishLaunching, etc. And the standard core-data template is written that way.
Now, for this to work, the app delegate needs to be properly set up from the point of view of DataController. However, init is called before all the IBOutlet is set up, so that's the wrong place to perform setup operations of objects inside the nib. Instead, use awakeFromNib to do these things. awakeFromNib is sent to every object after the IBOutlet etc. are all set up.
That said, writing your own DataController is a total waste of time. Just instantiate the standard NSArrayController in the nib file, and use it in the Core Data mode via binding. There's absolutely no need for you to write the fetch request by yourself. Study Apple's own CoreData sample codes and then google "Binding CoreData Tutorial" for many tutorials available on-line.

NSManagedObject: create on separate thread

I understand that Core Data is not thread safe and that NSManagedObjectContext and NSManagedObjects associated with a context cannot be passed from thread to thread.
Question:
However, if I have a NSManagedObjectContext on my main thread, can I create a NSManagedObject object on a background thread (WITHOUT attaching it to any context -- that is, simply call alloc/init on NSManagedObject), then pass that NSManagedObject back to the main thread and add it to the context from there? I've reviewed the docs on Core Data concurrency, but can't find anything that says this usage pattern is okay.
Context:
I have a background thread that performs a complex task and then publishes a result. The result is an NSManagedObject subclass that contains a few attributes: time, a file path, and a success or error message (as a string). I want to create the result object on the background thread, then toss it back to the main thread and add it to the Core Data context where it will be displayed in a tableView.
If I can't create the managedObject on the background thread, then I'll need to create a dictionary, pass the dictionary to the main thread, read the keys, create the managedObject from those values, etc. Just seems cleaner to create the managedObject on the background thread if possible.
The better approach is to have a context per thread. This way each thread will have its own scratch pad to play around with. Then when you background thread finishes, tell your main thread to update its view or ui table view or how every you are presenting your data.
You'll need to notify your main thread when changes occur. The big problem is, the contexts between different threads and main thread are not aware of each other. There is a way in core data to keep the contexts in sync. When you want to save, the context on the background thread should broadcast an NSManagedObjectContextDidSaveNotification notification.
So for example, in your main method in your NSOperation, you could do this:
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:context];
the mergeChanges would be a private method in your NSOperation instance.
example of merge changes
- (void)mergeChanges:(NSNotification *)notification
{
ApplicationController *appController = [[NSApplication sharedApplication] delegate];
NSManagedObjectContext *mainContext = [appController managedObjectContext];
// Merge changes into the main context on the main thread
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}

How to stop Core Data application from loading managedObjectContext automatically?

I am building a core data application that checks for a saved user login upon launch and sets up the model, store, coordinator and context afterwards. The only problem I have is that as soon as the user clicks on any view in the interface, the application tries to get the managedObjectContext causing an exception seeing as I haven't created the stores yet.
Is there any way to stop it from doing this?
Cheers.
If you're using the Coredata boiler plate stuff provided by Apple, you will notice the managedObjectContext object is loaded lazily when its property is accessed.
Simply tell your view controller to access the context via its property (i.e. self.managedObjectContext) instead of accessing the variable directly, and the context, object model and persistent store coordinator will be created appropriately.
PS: This is just a guess as you didn't post any of your related code here.
Why are you showing views that depend on the managed object context without either creating it already or arranging for it to be created on access?
The usual pattern is to have your managed object context getter look something like this:
- (NSManagedObjectContext *)managedObjectContext {
if (!_managedObjectContext) {
// create context, and store it in _managedObjectContext
}
return _managedObjectContext;
}
(in this code, _managedObjectContext is an ivar in the class to hold the context). That way the context gets created automagically when needed. Apple's standard sample code does just this for you.