Multi-context kernel data: merging into an unsaved context

I am trying to implement this main data stack:

PSC <--+-- MainMOC | +-- BackgroundPrivateMOC 

There are some things that I really don’t understand. Perhaps we have an object in our Persisten Store, and we get it from the main MOC to make some changes (the user changes it manually). At the same time, my BG MOC makes some changes with the same object and saves the changes to PS. After saving, we must combine BG MOC with MAIN MOC (this is common practice). What I expect after the merge is that the MAIN MOC contains changes from the BG MOC (because the changes were made a bit later than MAIN). But this actually did not happen. All that I had after the merge was completed is dirty refreshedObjects = 1 in my MAIN MOC, and if I retrieve this object again through the MAIN MOC, I don’t see any changes made using BG MOC.

  • How to propagate BG changes to the MAIN MOC correctly until the MAIN MOC was saved before the BG changes were made?
  • How to handle the situation when my MAIN MOC has nonzero refreshedObjects after the merge is completed, and how to promote these objects in the MAIN MOC to make them available for fetching and with?

I believe that my sample code will help you better understand my problem. You can simply download the project ( https://www.dropbox.com/s/1qr50zto5j4hj40/ThreadedCoreData.zip?dl=0 ) and run the XCTest that I prepared.

The following is the error code:

 @implementation ThrdCoreData_Tests - (void)setUp { [super setUp]; /** OUR SIMPLE STACK: PSC <--+-- MainMOC | +-- BackgroundPrivateMOC */ NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; // main context (Main queue) _mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [_mainMOC setPersistentStoreCoordinator:coordinator]; [_mainMOC setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy]; // background context (Private Queue) _bgMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; _bgMOC.persistentStoreCoordinator = self.persistentStoreCoordinator; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeBGChangesToMain:) name:NSManagedObjectContextDidSaveNotification object:_bgMOC]; u_int32_t value = arc4random_uniform(3000000000); // simply generate new random values for the test _mainMOCVlaue = [NSString stringWithFormat:@"%u" , value]; _expectedBGValue = [NSString stringWithFormat:@"%u" , value/2]; Earthquake * mainEq = [Earthquake MR_findFirstInContext:self.mainMOC]; if (!mainEq){ // At the very first time the test is running, create one single test oject. Earthquake * mainEq = [Earthquake MR_createEntityInContext:self.mainMOC]; mainEq.location = nil; // initial value will be nil [self.mainMOC MR_saveOnlySelfAndWait]; } } - (void)testThatBGMOCSuccessfullyMergesWithMain { _expectation = [self expectationWithDescription:@"test finished"]; // lets change our single object in main MOC. I expect that the value will be later overwritten by `_expectedBGValue` Earthquake * mainEq = [Earthquake MR_findFirstInContext:self.mainMOC]; NSLog(@"\nCurrently stored value:\n%@\nNew main value:\n%@", mainEq.location, _mainMOCVlaue); mainEq.location = _mainMOCVlaue; // the test will succeed if this line commented // now change that object in BG MOC by setting `_expectedBGValue` [_bgMOC performBlockAndWait:^{ Earthquake * bgEq = [Earthquake MR_findFirstInContext:_bgMOC]; bgEq.location = _expectedBGValue; NSLog(@"\nNew expected value set:\n%@", _expectedBGValue); [_bgMOC MR_saveToPersistentStoreAndWait]; // this will trigger the `mergeBGChangesToMain` method }]; [self waitForExpectationsWithTimeout:3 handler:nil]; } - (void)mergeBGChangesToMain:(NSNotification *)notification { dispatch_async(dispatch_get_main_queue(), ^{ [self.mainMOC mergeChangesFromContextDidSaveNotification:notification]; // now after merge done, lets find our object with expected value `_expectedBGValue`: Earthquake * expectedEQ = [Earthquake MR_findFirstByAttribute:@"location" withValue:_expectedBGValue inContext:self.mainMOC]; if (!expectedEQ){ Earthquake * eqFirst = [Earthquake MR_findFirstInContext:self.mainMOC]; NSLog(@"\nCurrent main MOC value is:\n%@\nexptected:\n%@", eqFirst.location, _expectedBGValue); } XCTAssert(expectedEQ != nil, @"Expected value not found"); [_expectation fulfill]; }); } 
0
source share
1 answer

First, when sending a master data code, I suggest that you do not send a zip code depending on a third-party library unless this third-party library is directly related to your problem. I assume that MR is a magical record, but I do not use it, and it seems to be only muddy water of the message, because who knows what it (or not) does under the covers.

In other words, try trimming the examples until the code is needed ... and no more ... and if necessary, include third-party libraries.

Secondly, when writing unit tests to use basic data, I suggest using a stack in memory. You always start empty, and it can be initialized, but you want to. Much easier to use for testing.

However, your problem is not understanding what mergeChangesFromContextDidSaveNotification does (and does not).

Basically, you have an object in a persistent repository of master data. You have two different MOCs attached to the repository through the same PSC.

Then your test loads the object in the main MOC and changes the value without saving in PSC. Then the second MOC loads the same object and changes its value to something else (i.e. Storage, and both MOCs have a different value for a specific attribute of the same object).

Now, when we save the MOC, if there are conflicts, conflicts will be processed as directed by mergePolicy . However, the merge policy does not apply to mergeChangesFromContextDidSaveNotification .

You can think of mergeChangesFromContextDidSaveNotification as inserting any new objects, deleting any deleted objects, and “updating” any updated objects while saving any local changes.

In your test, if you add another attribute (for example, “name”) and change the “name” and “location” in the BG MOC, but change only the “location” in the main MOC, you will see that the “name” is combined with the BG MOC in the main MOC, as expected.

However, as you point out in your question, the "location" does not seem to come together. In fact, it merges, but any local changes override what is in the store ... and this is exactly what you want, because the user most likely made this change and does not want it to be changed behind them.

Basically, any pending local changes will override the changes from the MOC to be merged.

If you want something else, you must implement this behavior when you perform a merge, for example ...

 - (void)mergeBGChangesToMain:(NSNotification*)note { NSMutableSet *updatedObjectIDs = [NSMutableSet set]; for (NSManagedObject *obj in [note.userInfo objectForKey:NSUpdatedObjectsKey]) { [updatedObjectIDs addObject:[obj objectID]]; } [_mainMOC performBlock:^{ for (NSManagedObject *obj in [_mainMOC updatedObjects]) { if ([updatedObjectIDs containsObject:obj.objectID]) { [_mainMOC refreshObject:obj mergeChanges:NO]; } } [_mainMOC mergeChangesFromContextDidSaveNotification:note]; }]; } 

This code first collects the ObjectID each object that has been updated in the unified-of-MOC.

Before performing the merge, we will then review each of the updated objects in the merge-MOC. If we merge an object into our MOC, and our merge-MOC also changes this object, then we want to allow values ​​in unified-from-MOC to override values ​​in unified-MOC. Thus, we update the local object from the repository, basically discarding any local changes (there are side effects, for example, causing the object to become an error, releasing links to any relations and releasing any properties of the transient process - see the documentation for refreshObject: mergeChanges :. )

Consider the following category, which takes into account your situation, and the general problem when using observers such as NSFetchedResultsController .

 @interface NSManagedObjectContext (WJHMerging) - (void)mergeChangesIntoContext:(NSManagedObjectContext*)moc withDidSaveNotification:(NSNotification*)notification faultUpdatedObjects:(BOOL)faultUpdatedObjects overrideLocalChanges:(BOOL)overrideLocalChanges completion:(void(^)())completionBlock; @end @implementation NSManagedObjectContext (WJHMerging) - (void)mergeChangesIntoContext:(NSManagedObjectContext *)moc withDidSaveNotification:(NSNotification *)notification faultUpdatedObjects:(BOOL)faultUpdatedObjects overrideLocalChanges:(BOOL)overrideLocalChanges completion:(void (^)())completionBlock { NSAssert(self == notification.object, @"Not called with"); NSSet *updatedObjects = notification.userInfo[NSUpdatedObjectsKey]; NSMutableSet *updatedObjectIDs = nil; if (overrideLocalChanges || faultUpdatedObjects) { updatedObjectIDs = [NSMutableSet setWithCapacity:updatedObjects.count]; for (NSManagedObject *obj in updatedObjects) { [updatedObjectIDs addObject:[obj objectID]]; } } [moc performBlock:^{ if (overrideLocalChanges) { for (NSManagedObject *obj in [moc updatedObjects]) { if ([updatedObjectIDs containsObject:obj.objectID]) { [moc refreshObject:obj mergeChanges:NO]; } } } if (faultUpdatedObjects) { for (NSManagedObjectID *objectID in updatedObjectIDs) { [[moc objectWithID:objectID] willAccessValueForKey:nil]; } } [moc mergeChangesFromContextDidSaveNotification:notification]; if (completionBlock) { completionBlock(); } }]; } @end 
+3
source

Source: https://habr.com/ru/post/956255/


All Articles