0
votes

I have an application that uses CoreData and has several background contexts (NSManagedObjectContext).

While writing some tests I've observed a weird behavior that seems to be controversial with official documentation: Changes from one context are automatically propagated to another one, while .automaticallyMergesChangesFromParent is set to false in both contexts.

Both contexts are received from NSPersistentContainer - one is from .viewContext, other - with .newBackgroundContext() function.

as save says, on context saving changes are committed to context's parent store, which is NSPersistentContainer.
But in fact, changes also appear in another context, despite the fact automaticallyMergesChangesFromParent == false (it's a default value).

let persistentContainer = NSPersistentContainer(name: "TESTING")
let mainContext = persistentContainer.viewContext
let otherContext = persistentContainer.newBackgroundContext()

//test entity is created on anotherContext
let entity: TestEntity = NSEntityDescription.insertNewObject
(forEntityName: String(describing: TestEntity.self),
 into: anotherContext) as! TestEntity
entity.statusCode = "Testing"

//prepare fetch request for test entity
let fetchReq: NSFetchRequest<TestEntity> = TestEntity.fetchRequest()
fetchReq.predicate = NSPredicate(format: "statusCode = %@",
 argumentArray: ["Testing"])

//ensure that entity is not present in mainContext
let entityFromMain = try! mainContext.fetch(fetchReq)
XCTAssertEqual(entityFromMain.count, 0)

//save context that has entity
try! otherContext.save()

//ensure that changes from parent store aren't merged automatically
XCTAssertFalse(mainContext.automaticallyMergesChangesFromParent)

//get inserted entity from mainContext
let entityOnMainAfterSaving = try! mainContext.fetch(fetchReq)

//entity is present in mainContext
XCTAssertTrue(entityOnMainAfterSaving.count > 0)

Expected output - entityOnMainAfterSaving should not contain a newly created entity, but it is already there, although mainContext wasn't refreshed.

UPDATE: I'm asking this because in my app there is a situation, where:
1. entity's property is changed in otherContext
2. otherContext is saved
3. entity is received through viewContext
4. property's value is not updated to its state as in p.1 (!)

At the same time, property's value will get updated if viewContext.refreshAllObjects() is called right before fetching in p.3

2

2 Answers

0
votes

This is the expected output.

You are saving an entity on a background context. The background context is a context from a persistent container. The method save tells us the following:

Attempts to commit unsaved changes to registered objects to the context’s parent store.

  • What is the parent store of the backgroundContext? persistentContainer.
  • What is the parent store of the viewContext? persistentContainer.

When you query on the viewContext, you will receive all values that are in the database. When you call save on the backgroundContext, it will tell the context's parent store (persistentContainer) that it wants to save. After the save, the values are committed in the 'real database', and not only on the private background context.

You can say that the viewContext is special in a way it queries the most up to date committed data. If you would have 2 private background contexts and set automaticallyMergesChangesFromParent to false, they don't merge back eachother commit actions, thus queries objects which are saved in private background context A will not be visible in private background context B.

If automaticallyMergesChangesFromParent is set to true, background contexts will be updated from commits from other contexts (if they derive from the same persistent container)

It would be absolutely nuts to not merge commits from other contexts in the viewContext, I think you can figure out why, it would not be maintainable.

0
votes

You seem to be confusing automaticallyMergesChangesFromParent or merging contexts in general with fetching.

A fetch will always reach out to the persistent store irrespective of whether the context has been merged or not with another. That's how fetches work in Core Data. There's a section on "Avoiding Fetch Requests" on this in this book (https://www.objc.io/books/core-data/) explaining the same thing. I paraphrase:

“The biggest performance offenders are fetch requests. Fetch requests have to traverse the entire Core Data stack. By API contract, a fetch request — even though it originates at the managed object context — will consult the SQLite store in the file system.

Because of this, fetch requests are inherently expensive.”

Excerpt From: Florian Kugler. “Core Data.”

And another from page 25, Fetch Requests:

“One important thing we want to point out now is this: every time you execute a fetch request, Core Data goes through the complete Core Data stack, all the way to the file system. By contract, a fetch request is a round trip: from the context, through the persistent store coordinator and the persistent store, down to SQLite, and then all the way back.”

Excerpt From: Florian Kugler. “Core Data.”

This is why even though you've turned automaticallyMergesChangesFromParent off, a fetch will still read the most recent value out of the database.

The 2015 and 2016 WWDC sessions on Core Data are good (well, they all are) and I'd recommend you go through them. There are hardly half a dozen sessions in the last 5 years or so. I've learnt a lot from watching these sessions as they go over best practices as well as the incremental changes added to Core Data over the years.

If you wish to continue seeing an older snapshot of your data for whatever reason (perhaps your background context is continually adding / removing entries and you're not ready to refresh your view context just yet), then I would suggest you look into Query Generation. It should give you what you're trying to achieve.