-
-
Notifications
You must be signed in to change notification settings - Fork 755
How to best create this complicated query? #887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Hello @bellebethcooper,
Allow me a preliminary comment about this paragraph. Fetching values with a single request that packs a lot of associations has a few advantages, but it is not required for Combine support. Yes, packed requests are often more efficient. They can also streamline code (look nicer - honestly, not always). But Combine publishers accept as many fetches as you want: // OK! Works as well with ValueObservation publisher
let cancellable = dbQueue
.readPublisher { db in
let value1 = try fetchValue1(db)
let value2 = try fetchValue2(db)
return (value1, value2)
}
.sink { (value1, value2) in
...
} What is most often wrong, because it disregards database consistency, is merging several publishers together. The sample code below can't guarantee that values 1 and 2 are fetched from the same database state. Concurrent writes could sneak in between the independent publishers, and ruin some important invariant. Such case may be harmless. But it may also introduce bugs. // DUBIOUS, TO SAY THE LEAST
let cancellable = Publishers
.Zip(
dbQueue.readPublisher(fetchValue1),
dbQueue.readPublisher(fetchValue2))
.sink { (value1, value2) in
...
} From this point of view, "packed requests" foster data consistency: you can't split them, so you can't zip their components, or combineLatest, or merge, etc. You can safely perform several independent requests, from a consistent database state, as long as values are fetched inside a single database access block, between End of the preliminary comment, which sheds some light on this sentence of the Good Practices for Designing Record Types, which may have inspired your own introduction:
I'll look at your precise setup in a further comment :-) |
All right, sounds good :-) First of all, thanks for providing the definition of your records: it made it very easy to setup a support playground!
Your request runs the following SQL queries. As seen with you in #888, the first request deals with everything but SELECT goalPeriod.*, MAX(goalPeriod.created), goal.*
FROM goalPeriod
JOIN goal ON (goal.id = goalPeriod.goalID)
AND (goal.rule IN (?, ...))
GROUP BY goalPeriod.goalId;
SELECT goalData.*, goal.id AS grdb_id
FROM goalData
JOIN goal ON (goal.id = goalData.goalID)
AND (goal.rule IN (?, ...))
AND (goal.id IN (?, ...))
WHERE goalData.date >= ? The first request indeed fetches "a subset of goals based on their (Yes, complex queries should often be tested, regardless of your SQL and GRDB skills). The second request fetches the filtered Kudos, the goal is achieved :-) Maybe you feel like your request is not in the correct order: you want to fetch goals with associated records, and you end up fetching goal periods with associated records. Maybe you had a difficulty with the You can still rewrite the request so that it looks like it fetches goals with associated records. But we need a way to reference let goalPeriods = TableAlias()
let request = Goal
// filter by goals matching rules
.filter(/* rules */)
// Also get the latest GoalPeriod
.including(required: Goal.goalPeriods.aliased(goalPeriods)
.annotated(with: max(Column("created"))))
.group(goalPeriods[Column("goalId")])
// Also get all of the GoalDatas for the current week
.including(all: Goal.goalDatas.filter(/* date */)) Your call.
Good. As stated in the sample code, this technique relies on a special processing of The subject of selecting ONE record in a // FUTURE
Goal
.filter(/* rules */)
.including(required: Goal.goalPeriods.order(Column("date")).last)
.including(all: Goal.goalDatas.filter(/* date */))
This will be the topic of another answer :-) |
I notice that let request = Goal
// filter by goals matching rules
.filter(/* rules */)
// Also get the latest GoalPeriod
.including(required: Goal.goalPeriods.annotated(with: max(Column("created"))))
.group(Column("id"))
// Also get all of the GoalDatas for the current week
.including(all: Goal.goalDatas.filter(/* date */)) |
Hi @groue, thanks so much for your quick and detailed response! This is really helpful. Great point about being able to observe multiple requests. I hadn't thought of doing it that way at all, but it's really helpful to know that's an option.
I'm on-board with this! What's your go-to approach to testing part of a query like this one? Your final suggestion seems to work fine, thank you! I think part of my problem was that I mistakenly thought I had to use Thanks again for your thoughts on this, I really appreciate it! |
Sure. I usually distinguish:
GRDB tries really hard to provide apis that behave the same for both More or less, that's how I do it those days! |
Great, that helps a lot. Thanks very much! |
I wanted to cheer you up for writing this sentence. To-many associations are usually used with You "unlock" new features when you make the link between joining Swift methods, and their SQL mapping: // SELECT foo.* FROM foo JOIN bar ON ...
Foo.joining(required: Foo.bar)
// SELECT foo.* FROM foo LEFT JOIN bar ON ...
Foo.joining(optional: Foo.bar)
// SELECT foo.*, bar.* FROM foo JOIN bar ON ...
Foo.including(required: Foo.bar)
// SELECT foo.*, bar.* FROM foo LEFT JOIN bar ON ...
Foo.including(optional: Foo.bar) For example, it's clear that you can load all authors with their books with struct AuthorInfo {
var author: Author
var books: [Book]
}
// Two SQL requests
let request = Author
.including(all: Author.books)
.asRequest(of: AuthorInfo.self) But you may also load all (author, book) pairs, with struct Authorship {
var author: Author
var book: Book
}
// SELECT author.*, book.*
// FROM author
// JOIN book ON book.authorID = author.id
let request = Author
.including(required: Author.books)
.asRequest(of: Authorship.self)
And from the SQL query above, you can derive more SQL queries, based on the same struct AuthorInfo {
var author: Author
var latestBook: Book
}
// SELECT author.*, book.*, MAX(book.date)
// FROM author
// JOIN book ON book.authorID = author.id
// GROUP BY author.id
let request = Author
.including(required: Author.books
.annotated(with: max(Column("date")))
.forKey("latestBook"))
.groupByPrimaryKey()
.asRequest(of: AuthorInfo.self) And voilà, we have the authors with their latest book again (it's even simpler this way than in the sample code in #856... a never-ending quest...) SQL is rich 😅 There are multiple ways to write requests that fetch the desired data. The more one gets fluent with SQL, the more one knows what the database can do, the more one's will becomes sharp. In order to help apply SQL skills, GRDB joining methods map directly onto SQL joins, as seen in the beginning of this comment. The |
Thanks @groue, this is really helpful. Figuring out how to build up requests in GRDB to get the data I want is one of my favourite things to do at the moment! But I'm also working on improving my SQL knowledge and I can see how this will make these problems easier for me to figure out. Thanks again for your pointers! |
What did you do?
Hi Gwendal,
Thanks for all your hard work with GRDB 5! I love having all the GRDBCombine stuff built-in now :)
I've spent a long time working on a complicated query recently and I got it working but wanted to check if you have any advice about a better way to handle this situation. I hope that's okay! Feel free to close this issue if not, as it's not an actual bug report or anything.
The basic situation is this: I have three models, and I want to create a query using associations that will fill a local model (i.e. not in the database). I want to keep this in one query so I can observe it using Combine publishers to keep my SwiftUI views updated.
Here are basic versions of the models I'm working with:
And here's the intermediary model I want to fill from my request:
So a
Goal
has manyGoalPeriod
s and manyGoalData
s. I want to request for a subset of goals based on theirRule
type, and then for each of those goals I want to fill aGoalPeriodInfo
model with that goal, only the most recent of its relatedGoalPeriod
s, and a subset of itsGoalData
models based on a filter.Here's the request I came up with that seems to do what I want:
I got the idea to limit my
GoalPeriod
request to the most recent one usingmax
from this issue.Here are a couple of other ways I tried to structure this request that didn't work:
I'm using
ValueObservation
to observe the request pretty much exactly how you've done it in the demo app.So my question is just, is there anything I'm doing obviously wrong here? And is there a different way you'd recommend handling this complicated request?
Thanks!
Environment
GRDB flavor(s): GRDB
GRDB version: 5.2.0
Installation method: SPM
Xcode version: 12.2
Swift version: 5
Platform(s) running GRDB: iOS
macOS version running Xcode: 11.0.1
The text was updated successfully, but these errors were encountered: