Same NSPredicate fetch different NSManagedObject in different context after merging - ios

I'm working on a multi-thread app. I need to create a NSManagedObjectContext run on background thread (named it privateContext), do some updating, inserting and deleting things on same NSPersistentStoreCoordinator of mainContext. After privateContext is saved, I merge changes of privateContext with mainContext.
If I create a NSManagedObject on mainContext, it is temporary without any save operation and objectID.URIRepresentation is like x-coredata:///ManagedObject/XXX-XXX-XXX-XXX-XXX.
Then I go to background thread and fetch with same NSPredicate on privateContext. The NSManagedObject I get is now x-coredata://XXX-XXX-XXX-XXX-XXX/ManagedObject/p1, which means it's not temporary.
After I do some updating on this object, save privateContext and merge with mainContext. I fetch with same NSPredicate on mainContext, I get a temporary object finally and all changes are gone.
And if I turn the app off and re-open it, all changes are back, and I'll get a persistent object...
EDIT
My question is how to fetch the right object (not temporary one) on mainContext after merging?
EDIT (codes about privateContext and mainContext)
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[privateContext setPersistentStoreCoordinator:[mainContext persistentStoreCoordinator]];
[privateContext setUndoManager:nil];
// Inserting, updating and deleting on privateContext
if ([privateContext hasChanges]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(privateContextDidSaveNotification:)
name:NSManagedObjectContextDidSaveNotification
object:privateContext];
NSError *saveError;
if (![privateContext save:&saveError]) {
// Error logs
}
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSManagedObjectContextDidSaveNotification
object:privateContext];
}
Here's privateContextDidSaveNotification: selector
- (void)privateContextDidSaveNotification:(NSNotification *)notification
{
[mainContext performBlock:^{
[mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}

Related

CoreData child contexts, NSFetchedResultsController and main thread

Following this excellent post by Olivier Drobnik, I've implemented the three-layer CoreData stack proposed by CoreData guru Marcus S. Zarra:
The only difference from this diagram and my code is that I only use one Temporary Background MOC, in order to avoid duplicates when inserting objects in several temp MOCs. Here's my context initialisation code:
#pragma mark - NSManagedObjectContexts
+ (NSManagedObjectContext *)privateManagedObjectContext
{
if (!_privateManagedObjectContext) {
// Setup MOC attached to PSC
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
// Add notification to perform save when the child is updated
_privateContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _privateManagedObjectContext) {
[_privateManagedObjectContext performBlock:^{
NSLog(#"AMBCoreData -> saving privateMOC");
NSError *error;
if (![_privateManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving _privateMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _privateManagedObjectContext;
}
+ (NSManagedObjectContext *)mainUIManagedObjectContext
{
if (!_mainUIManagedObjectContext) {
// Setup MOC attached to parent privateMOC in main queue
_mainUIManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainUIManagedObjectContext setParentContext:[self privateManagedObjectContext]];
// Add notification to perform save when the child is updated
_mainUIContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _mainUIManagedObjectContext) {
NSLog(#"AMBCoreData -> saving mainUIMOC");
[_mainUIManagedObjectContext performBlock:^{
NSError *error;
if (![_mainUIManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving mainUIMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _mainUIManagedObjectContext;
}
+ (NSManagedObjectContext *)importManagedObjectContext
{
if (!_importManagedObjectContext) {
// Setup MOC attached to parent mainUIMOC in private queue
_importManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_importManagedObjectContext setParentContext:[self mainUIManagedObjectContext]];
}
return _importManagedObjectContext;
}
This code is pretty straightforward. I'm replicating the above diagram using only the mainUIManagedObjectContext in the NSMainQueueConcurrencyType. Every time the child context, importManagedObjectContext gets saved, a notification is fired and all the parent contexts performs a save in it's current thread.
I've implemented a test view controller with a UITableView and a NSFetchedResultsController attached. This is the code in the viewDidLoad of my test view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Task"];
[request setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"insertDate" ascending:NO]]];
self.fetchRequest = request;
NSFetchedResultsController *frc =
[[NSFetchedResultsController alloc]
initWithFetchRequest:self.fetchRequest
managedObjectContext:[AMBCoreData mainUIManagedObjectContext]
sectionNameKeyPath:nil
cacheName:nil];
frc.delegate = self;
[self setFetchedResultsController:frc];
[self.fetchedResultsController performFetch:nil];
}
Here I attach the mainUIManagedObjectContext to the NSFetchedResultsController. Later, in my viewDidAppear, I run a loop to insert a few Task entities:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[AMBCoreData importManagedObjectContext] performBlock:^{
for (int i = 0; i < 5000; i++) {
Task *task = [NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:[AMBCoreData importManagedObjectContext]];
task.title = [NSString stringWithFormat:#"Task %d", i];
task.insertDate = [NSDate new];
[[AMBCoreData importManagedObjectContext] save:nil];
}];
}
The thing is, I'm inserting 5000 objects and the UI is freezing when the data is populated into the table view. Florian Kugler ran a test with this architecture inserting 15.000 objects and with instruments he got this main thread usage (blue is for main thread, gray for any other threads):
But here's my main thread CPU usage with 5000 objects, profiled using an iPhone 5:
As you can see, my main thread usage is far greater than Florian's and also my UI freezes for a few seconds. My question is, am I doing something wrong? Is this the expected behaviour when using this three-layer MOC architecture with a NSFetchedResultsController and a UITableView? I know that inserting 5000 objects is not the usual behaviour of most apps, so when I've tried with 50 or 100 objects the freeze was inexistent or unnoticeable, but the main thread usage was high (although I admit that in this case it can be due another reasons like waking up the app).
Yes, it is expected, because Main MOC is involved in the saves of its children. It is convenient and kind of okay when children of the UI context don’t do big saves, but often becomes a performance problem if those saves are bigger. You can’t be sure that the UI thread does only minimum job when using this pattern.
For the big saves I would recommend creating a context that is configured directly with the persistent store coordinator. After big save happens, you just refetch and optionally refresh data in the UI context. For more details see my answer here.

CoreData Threading. iOS 6 vs 7

I'm having an issue related to my core data implementation and threading. It works fine on iOS 7 but I can't get the main context to refresh properly on iOS 6.
I have two NSPersistentStoreCoordinators pointing to the same database file. I then have a main context and a background context. All new child contexts are children of the background context.
When I update items on child contexts in background threads on 6 they never merge to the main context until I restart the app and the main context fetches them from the store. On ios7 this all works fine however. How can I ensure the main context is refreshing the merge properly without faulting and reloading the db?
I know iOS 7 turned on asynchrounous sql access by default, but I've done that on 6 with:
NSSQLitePragmasOption : #{#"journal_mode" : #"WAL"}
Here is how my context's are set up:
self.mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[self.mainManagedObjectContext setPersistentStoreCoordinator:self.mainPersistentStoreCoordinator];
[self.mainManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[self.mainManagedObjectContext setUndoManager:nil];
self.backgroundParentContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[self.backgroundParentContext setPersistentStoreCoordinator:self.backgroundPersistentStoreCoordinator];
[self.backgroundParentContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[self.backgroundParentContext setUndoManager:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(backgroundContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.backgroundParentContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mainContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.mainManagedObjectContext];
Here is how I create a child context:
- (NSManagedObjectContext *)newChildManagedObjectContext
{
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[childContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[childContext setParentContext:self.backgroundParentContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidSave:)
name:NSManagedObjectContextDidSaveNotification object:childContext];
[childContext setUndoManager:nil];
return childContext;
}
Here are the notification methods:
- (void)mainContextDidSave:(NSNotification *)notification
{
__block NSNotification *strongNotification = notification;
[self.backgroundParentContext performBlockAndWait:^{
[self.backgroundParentContext mergeChangesFromContextDidSaveNotification:strongNotification];
}];
}
- (void)backgroundContextDidSave:(NSNotification *)notification
{
[self.mainManagedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:NO];
}
- (void)managedObjectContextDidSave:(NSNotification *)notification
{
[self.backgroundParentContext performBlockAndWait:^{
NSError *error = nil;
if (![self.backgroundParentContext save:&error]) {
DNSLog(#"error saving context: %#", error);
}
}];
}
UPDATE:
Well it appears this is just never going to work on 6. According the docs, they have changed quite a bit around merging contexts for 7. So I'm going to have to take a different approach for 6 I guess...
I haven't been using two NSPersistentStoreCoordinators on iOS 6, but merging changes between contexts from different store coordinators was mentioned explicitly during WWDC, so I think it's changed in iOS 7. See the following sessions:
207 - What’s New in Core Data (starting from slide number 145).
211 - Core Data Performance (starting from slide number 91),

Core Data in background: Using separate context and notifications, but not updating DB with new values

Overview: I have a refresh process that runs in the background (just using performInBackground is all) and part of that has db updates so I have a separate MOC for the background thread. I then use the didSave notification to merge in the changes, but I'm not see those updates in the DB/my UI. I've logged the object itself before and after the save and I can see the attribute is changed, but in the method called by notification, I log the objects in the context received and it doesnt have updated value. I know some other things are probably ugly, but just trying to figure out this core data piece. I previously had just one MOC and things worked (non core data should be fine), but I've rearchitected things to be in background now and wanted to use guidance of a separate MOC.
Create context, setup notification, set attribute and save. Post save, the value is 0
// this creates context with same PSC as main MOC
NSManagedObjectContext *context = [[MyAppDelegate application] temporaryContext];
[[NSNotificationCenter defaultCenter] addObserver:(MyAppDelegate *)[[UIApplication sharedApplication] delegate]
selector:#selector(managedObjectContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:context];
NSLog(#"value is %d", [[myObject email] boolValue]); //value is 1
[myObject setEmail:[NSNumber numberWithBool:![[myObject email] boolValue]]];
NSError *error;
if (![context save:&error]) {
NSLog(#"Error in saving BriefCase object - error:%#" ,error);
}
NSLog(#"value is %d", [[myObject email] boolValue]); //value is 0 now
Here, I check the MOC I was sent and i see all values are 1. So when merge happens, no updates made. Where is the disconnect if I saw the object had a 0 after saving?
// Called when a ManagedObjectContext performs save operation
- (void)managedObjectContextDidSave:(NSNotification *)notification {
NSManagedObjectContext *receivedMOC = [notification object];
NSArray *items = [MyObjectClass getAllMyObjectsInManagedObjectContext:receivedMOC];
for (int i=0; i < [items count]; i++) {
NSLog(#"value is %d", [[[items objectAtIndex:i] email] boolValue]);
}
add'l context creation code, may be the issue
- (NSManagedObjectContext *)temporaryContext {
NSManagedObjectContext *newContext = [[[NSManagedObjectContext alloc] init] autorelease];
NSPersistentStoreCoordinator *psc = [self.managedObjectContext persistentStoreCoordinator];
[newContext setUndoManager:nil];
[newContext setPersistentStoreCoordinator:psc];
return newContext;
The temporaryContext creates a new MOC on each call, so is seems that you actually work with 2 different background MOCs. myModel is created on a MOC that is different from
NSManagedObjectContext *context = [[MyAppDelegate application] temporaryContext];
and therefore saving the latter has no effect on the changes in myModel.
You have to modify your code such that only one temporary context is created and passed around.
I don't see you calling
[mainMOC mergeChangesFromContextDidSaveNotification:saveNotification];
when handling the notification. This might explain what is happening. This should be called on the main thread.

Core Data background thread not updating record

I am having an issue with Core Data in a background GCD thread... I want to update a record, but after fetching it and setting the values it doesn't seem to actually save the updated record.
isUpdate is a BOOL I have setup that tells me whether I am running a first time parse/save or whether it's a record I need to update. In my case, when I update a record it doesn't actually seem to update in my store.
I'm using MagicalRecord helpers. Here's my code:
// Create background context
NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] init];
[backgroundContext setPersistentStoreCoordinator:[NSPersistentStoreCoordinator defaultStoreCoordinator]];
// Save the background context and handle the save notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
// Parsing data...
//..
Record *record;
if (!isUpdate) {
record = [NSEntityDescription insertNewObjectForEntityForName:#"Record" inManagedObjectContext:backgroundContext];
} else {
NSPredicate *recordPredicate = [NSPredicate predicateWithFormat:#"SELF.tag == %#", [[node attributeForName:#"tag"] stringValue]];
record = [Record findFirstWithPredicate:recordPredicate];
}
[record setTitle:[[recordNode attributeForName:#"title"] stringValue]];
// Parsing other data...
//..
NSError *error = nil;
// save the context
[backgroundContext save:&error];
if (error) {
NSLog(#"An error occurred: %#", error);
}
And here's the notification:
- (void)backgroundContextDidSave:(NSNotification *)notification {
// Make sure we're on the main thread when updating the main context
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(backgroundContextDidSave:)
withObject:notification
waitUntilDone:NO];
return;
}
// merge in the changes to the main context on the main thread
[[NSManagedObjectContext defaultContext] mergeChangesFromContextDidSaveNotification:notification];
}
Your code sounds quite strange to me.
Why do you register NSManagedObjectContextDidSaveNotification notification in the background thread? Maybe I'm wrong but you need to register that notification in a different point in your app.
If you want to make it works you could register that notification in the main thread. For example you could do it in the AppDelegate.
For example in didFinishLaunchingWithOptions: method you cand do
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
Then, always within the AppDelegate, you can merge the changes with the method you wrote:
- (void)backgroundContextDidSave:(NSNotification *)notification {
// Make sure we're on the main thread when updating the main context
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(backgroundContextDidSave:)
withObject:notification
waitUntilDone:YES];
return;
}
// merge in the changes to the main context on the main thread
[[NSManagedObjectContext defaultContext] mergeChangesFromContextDidSaveNotification:notification];
}
The code performs these steps:
First checks if you are running in the main thread or not.
Since the notification you register could originate from a thread
different from the main one, you need to perform the selector on the
main thread.
Finally performs the merge with the notification that contains the
changes you made in background.
Once done, you can see that the main context is updated with the changes made in the other one.
Edit
Maybe you can try also to change the waitUntilDone to YES.
Hope it helps.
You are mixing two contexts. This code is probably bad:
record = [Record findFirstWithPredicate:recordPredicate];
I assume that this finds record in different context instead of Your backgroundContext. You should change it to something like this:
record = [Record findFirstWithPredicate:recordPredicate inManagedObjectContext:backgroundContext];

ASINetworkQueue inside NSOperation error saving Core Data

I want to use the ASINetworkQueue inside an NSOperation. This works great and makes no problem. What fails is saving Core Data. I set up a new NSManagedObjectContext for this Operation like it is told in the docs.
I think that the problem is that I save the data when the ASINetworkQueue finishes and delegate selector is called. Because the delegates are called on the mainThread, the save message fails.
Can this be the problem and does anybody has a solution?
You are using the PerformSelectorOnMainThread method right (to merge the changes from the new instantiated ManagedObjectContext)?
I do something like this in my Operations (ctx is my instantiated MOC):
First register for notifications:
// Register context with the notification center
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:ctx];
Then when you need to save the context:
if ([ctx hasChanges]) {
error = nil;
// Save the context.
if (![ctx save:&error])
{
// Do something with the error
}
// Clear out the scratchpad
[ctx reset];
}
And then the method that does the merging with the main MOC:
- (void)mergeChanges:(NSNotification *)notification
{
id appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *mainContext = [appDelegate managedObjectContext];
// Merge changes into the main context on the main thread
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:NO];
// NSLog(#"Merged Changes");
}
Hope this helps

Resources