How to ignore changes in mergeChangesFromContextDidSaveNotification in NSManagedObjectContextWillSaveNotification
Asked Answered
V

2

9

I am using a Private Managed Object Context to create some new objects into the persistent store, then after saving the private MOC, merging them into the main MOC using mergeChangesFromContextDidSaveNotification. This works fine, and updates the UI as required, and the NSManagedObjectContextWillSaveNotification is NOT invoked here for the mainMOC.

Then I make some changes to the mainMOC using the UI, and listen to NSManagedObjectContextWillSaveNotification. The notification is posted, but it contains not only the edits I made, but also the objects that were merged in from the PrivateMOC using mergeChangesFromContextDidSaveNotification.

Is there a way to ignore the changes that were merged in from another context into the mainContext, on subsequent contextDidChange notifications?

Here is the setup:

- (void) loadData {
   privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSPrivateQueueConcurrencyType];

   privateContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;

   [[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(contextWillSave:)
                                             name:NSManagedObjectContextWillSaveNotification
                                           object: self.mainContext];

   NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:record.recordType inManagedObjectContext: self.privateContext];

   // fill in object

   if ([self.privateContext hasChanges]) {
       [self savePrivateContextAndMergeWithMainContext: self.privateContext];
   }
}

- (void) savePrivateContextAndMergeWithMainContext: (NSManagedObjectContext *) privateContext {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(privateContextDidChange:) name:NSManagedObjectContextDidSaveNotification object:privateContext];
    __block NSError *error = nil;
    [privateContext performBlockAndWait:^{
        NSLog(@"PrivateContext saved");
        [privateContext save:&error];
    }];


    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:privateContext];

    if (error) {
        NSLog(@"error = %@", error);
    }
}

- (void) privateContextDidChange: (NSNotification *) notification{

    [self.mainContext performBlockAndWait:^{
        NSLog(@"merged into mainContext");
        [self.mainContext mergeChangesFromContextDidSaveNotification:notification];
    }];
}

This works fine and saving the private context and merging into the mainContext doesn't trigger a contextWillSave notification. But on editing the data from the UI (on the main MOC) triggers the notification and includes the data that was previously saved using the private MOC.

Hope that's clear. Let me know if I should include anything else.

-- UPDATE --

Seems like the problem is with specifically deleting objects from the private context. After deleting from the private context, and calling mergeChangesFromContextDidSaveNotification on the main MOC, the mainMoc's deletedObjects set still shows the object that was deleted. This doesn't happen with inserts or updates in the private context. Is this documented anywhere? What could be the workaround?

Vasquez answered 1/3, 2016 at 21:2 Comment(3)
The main context observer is to listen for changes made by the UI, so they can be communicated to the server. That's why I don't want the changes made in the private context to show up in the main context listener. They don't initially when changes are merged into the main context, but it looks like the MOC still marks them as 'changed,' and the next time you save the main context, they show up in the notification object.Vasquez
Actually, the problem seems to be more with objects deleted in the background context, not inserted. The deletes are what are causing me this problem, since they seem to hang around after mergeChangesFromContextDidSaveNotification: is calledVasquez
To get you right: When you insert objects in the private (sub)context and save this to the main (super)context, this objects are not marked as inserted objects of the main (super)context?Repressive
T
3

Modifying privateContextDidChange like this ...

- (void) privateContextDidChange: (NSNotification *) notification{
    if (notification.object == PrivateMOC) {
        [self.mainContext performBlockAndWait:^{
            NSLog(@"merged into mainContext");
            [self.mainContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

... where PrivateMOC is the reference to the Private Managed Object Context?

Tagliatelle answered 8/3, 2016 at 13:46 Comment(2)
This doesn't help with the problem at hand.Vasquez
Might not help with this particular case; but gives answer to one (part) of the questions: "Is there a way to ignore the changes that were merged in from another context [...]" :)Tagliatelle
C
1

Core Data has been around for a number of years now, and over that time the concurrency model has evolved.

The "thread confinement" and "locking" concurrency models used notifications to communicate changes between contexts. When a context was saved a notification would be broadcast with information about the changes. That information could be used to merge the changes from one context into others. This never scaled particularly well and was often a major pain point for applications. The "locking" and "thread confinement" models have been obsolete for a number of years now.

When "queue confinement" was introduced the concept of "nested contexts" was introduced along with it. A context could have a parent context, and when the child context is saved those changes are automatically communicated to the parent (and no further). This is the recommended way to communicate changes between contexts when using queue confinement, which you are.

It is not recommended that you use mergeChangesFromContextDidSaveNotification: with a NSPrivateQueueConcurrencyType context.

Core Data performs a lot of internal bookkeeping and change tracking on your behalf. Normally during a save operation that information is aggregated and coalesced - this is part of the processPendingChanges operation. When the save operation is performed inside a performBlockAndWait: this may not happen, or may not be complete - which results in the changes (and any change notification) being incomplete or not happening at all. performBlockAndWait: is reentrant, which is not compatible with the processPendingChanges process. Using performBlock: instead will result in more consistent behavior.

If you use nested contexts to communicate changes rather than notifications your issue as you describe in your question will be solved.

Cobaltous answered 6/3, 2016 at 23:24 Comment(1)
"It is not recommended that you use mergeChangesFromContextDidSaveNotification: with a NSPrivateQueueConcurrencyType context." -- do you have any reference to this? I've seen many articles in which the opposite is stated; that while parent-child contexts are useful in some cases, we should still use mergeChangesFromContextDidSaveNotification: from private contexts when appropriate. I've been reading Objc.io's Core Data book and they use mergeChangesFromContextDidSaveNotification in many placesVasquez

© 2022 - 2024 — McMap. All rights reserved.