This year Apple released some new features in Core Data. One I am going to take a look at is Query Generations.
A Core Data Query Generation is a stable view of your data, or in other words, a snapshot in time. Also defined as a read transaction. All reads in a context will see the same view of data until you choose to advance it.
In this example I’ll show how a context can be “pinned” to a specific query generation.
Given a context we can pin it to the current query generation like so:
1 |
contextA.setQueryGenerationFrom(NSQueryGenerationToken.current) |
Once a context is pinned to a specific query generation it will be given a sable view of its data regardless of the SQLite store activity.
The initial state of the SQLite database can be seen in the image below:
At a high level the steps I follow in the code below can be outlined like so:
1. have two contexts ready to be used, contextA and contextB
2. contextA -> set query generation to current
3. contextA -> we will fetch a specific person (dave0)
3. contextB -> update the name property of all persons to “DaveUpdate” in the SQLite database
4. contextA -> fetch dave23 and make sure the result is not nil, if not nil then test is good and we know name dave23 was not changed to “DaveUpdate” with the NSBatchUpdateRequest
5. contextA -> save and check queryGenerationToken, it must be nil right after save. This demonstrates how a pinned context will move to the store’s version at save time, at that point it can be re-pinned if needed
Here’s the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
contextA?.performAndWait { // default behavior is unpinned, so we test for nil. XCTAssertNil(self.contextA?.queryGenerationToken) // the context was unpinned, go ahead and pin it to current try? self.contextA?.setQueryGenerationFrom(NSQueryGenerationToken.current) XCTAssertNotNil(self.contextA?.queryGenerationToken) do { let fr: NSFetchRequest<Person> = Person.fetchRequest() fr.predicate = NSPredicate(format: "name == %@", "dave0") let result = try fr.execute() let dave0 = result.first let cars = dave0?.cars if let cars = dave0?.cars, let carsArray:[Car] = Array(cars) as? [Car] { for car in carsArray { print("car.name = \(car.name)") } } print("dave0 intially has cars = \(cars)") XCTAssertNotNil(cars) XCTAssertNotNil(dave0, "dave0 not found in store!") XCTAssert(dave0?.age == self.dave0InitialAge, "dave0 (\(dave0?.age)) age does not match dave0InitialAge (\(self.dave0InitialAge))") } catch { XCTAssert(false, "do catch error \(error)") } ////////////////////////// EDIT on B during A execution ///////////////////////////// self.contextB?.performAndWait { // delete any car for Person.name == dave0 do { let fr: NSFetchRequest<Person> = Person.fetchRequest() fr.predicate = NSPredicate(format: "name == %@", "dave0") let result = try fr.execute() let dave0 = result.first XCTAssert(dave0?.cars?.count == 1, "should have one car!") if let cars = dave0?.cars, let carsArray:[Car] = Array(cars) as? [Car] { for car in carsArray { self.contextB!.delete(car) } } try self.contextB!.save() } catch { XCTAssert(false, "do catch error \(error)") } // Store UPDATE on Persons let update = NSBatchUpdateRequest(entityName:"Person") update.resultType = .updatedObjectsCountResultType update.propertiesToUpdate = ["name" : NSExpression(forConstantValue:"DaveUpdate")] do { try self.contextB!.execute(update) } catch { XCTAssert(false, "do catch error \(error)") } } ////////////////////////////////////////////////////////////////////////////////////// do { // test to see that in contextA we still have a person.name == dave23 let fr23: NSFetchRequest<Person> = Person.fetchRequest() fr23.predicate = NSPredicate(format: "name == %@", "dave23") let result = try fr23.execute() let dave23 = result.first print("dave23 = \(dave23?.name)") XCTAssertNotNil(dave23, "could not find entity dave23!") XCTAssert(dave23?.name == "dave23", "dave23 entity does not have name dave23! actual name is \(dave23?.name)") XCTAssertNotNil(self.contextA?.queryGenerationToken, "queryGenerationToken should not be nil here!") // let's see what we get for cars relationship. We deleted all cars in contextB // so without query generations we would get nothing. let fr: NSFetchRequest<Person> = Person.fetchRequest() fr.predicate = NSPredicate(format: "name == %@", "dave0") let result0 = try fr.execute() let dave0 = result0.first if let cars = dave0?.cars, let carsArray:[Car] = Array(cars) as? [Car] { XCTAssert(carsArray.count > 0, "cars for dave0 are gone!") for car in carsArray { print("dave0.cars = \(car)") } } else { XCTAssert(false, "dave0's cars got stolen!") } // now lets change the name dave23?.name = "NAME_23" if self.contextA?.hasChanges == true { try self.contextA?.save() // query generation will be set to nil at save() // saving a context puts its query generation to state nil, so we test it to make sure that is the case. XCTAssertNil(self.contextA?.queryGenerationToken, "queryGenerationToken should be nil here!") } let dave23AfterSave = result.first XCTAssert(dave23AfterSave?.name == "NAME_23", "name should be NAME_23") } catch { XCTAssert(false, "do catch error \(error)") } } |
So, how does the code above prove that query generation is indeed giving us a snapshot in time of the data?
1 |
let result = try fr23.execute() |
The result is not nil! It contains a Person entity even if contextB did change person.name for all Person entities in store. Here’s a screenshot of the state of the database right after the line (which will cause each person to have name == “DaveUpdate”):
1 |
try self.contextB!.execute(update) |
Without pinning the context “let result = try fr23.execute()” would return no result because it would have seen persons with name == “DaveUpdate”.
What about a relationship?
Our model defines the following relationship: Person <->> Car. Every time we run this test the store is populated with Person entities, each Person entity is assigned one Car.
In contextB we update all person’s name values to “DaveUpdate”. In contextB we also delete the Car assigned to dave0.
Because contextA was pinned before contextB changed the content of the store, we’re then able to still fetch dave23, dave0 and its car even though the actual data in the SQLite store does not contain any person with name “dave23” and “dave0” and its cars relationship.
Conclusion:
The new Query Generation feature allows us to have the option of having a more transactional read approach within a certain context. It can be useful when the state needs to support a view maybe that simply needs to represent the state of the data at certain given point in time regardless of what happens to the actual data in the SQLite store concurrently. It’s also going to prevent the very annoying error we often get when dealing with faults that can’t be loaded from store anymore because of the stable state we have in a pinned context.
It is important to note that context registered objects are not automatically refreshed on generation update. If you need to do so you can call contextA.fetch() or contextA.refreshAllObjects().
In order to use query generations in Core Data you must use an SQLite store in WAL mode.
Great post.
I really like reading a post that will make people think. Also, thank you for permitting me to comment!|
Hey very interesting blog!|
Great info. Lucky me I came across your site by chance (stumbleupon). I’ve bookmarked it for later!|
Saved as a favorite, I really like your site!|
Usually I don’t learn article on blogs, however I wish to say that this write-up very compelled me to check out and do it! Your writing taste has been surprised me. Thank you, very nice article.|
Hello there, I found your site by the use of Google even as searching for a similar topic, your site got here up, it seems great. I have bookmarked it in my google bookmarks.
It’s a pity you don’t have a donate button! I’d most certainly donate to this superb blog! I suppose for now i’ll settle for bookmarking and adding your RSS feed to my Google account. I look forward to fresh updates and will talk about this blog with my Facebook group. Chat soon!|