I wrote a small utility which is based on this approach and helps me to organize my complex queries. You may like it as well
I sometimes wonder why we even bother “hiding” the query behind keyword lists. Why don’t we just pass Ecto.Query
from the, live view in this case, interface to the business logic? This doesn’t govern how the query is going to be executed but at the same time allows for efficient preloading. The latter is usually not thought of at all in these keyword-based interfaces.
It’s because you cannot control the composition easily if you pass parts of the query.
Let’s say we have 2 tables:
book ↔ author
And you want to have the following filters:
- book name;
- author name;
By passing this as keyword option (for example {:filter, :book_name, name}
) you can actually do the required joins if they are necessary:
def filter(query, {:filter, :author_name, name}) do
query
# This join is conditional and can be absent from the default query
|> join(:inner, [book], author in assoc(book, :author), as: :author)
|> where([author: author], author.name == ^name
end
And this is just one of the simplest examples, in complex queries you can have multiple joins mixed with preloads, you really don’t want to have to deal with this in your business logic side of code.
Add another layer of association nesting and it becomes unwieldy. In order to preload more than one layer, you need to gather all the joins, somehow, and construct a tree. Or… make the user specify how the preloads should look. It’s not much different than a manual Query struct construction. Maybe you can provide composable functions but at the end, if you want nested preloads, you need to manually specify them or resort to trickery.
Maybe You can use dataloader as it allows batching the load of nested associations
I actually experimented with exposing a query
function which accepts a GraphQL query as an alternative to a keyword list facade. Didn’t give it enough time and usage but it looked promising. Of course, it came with all the problems GQL introduces but it’s convenient.
Sadly I cannot share more complex examples from work, but I don’t see how the solution of passing partial queries can work without you either implementing a lot of joins yourself or repeating code, preloads aside (I’ve seen them being used in queries as well as separately and both variants work well, it highly depends on use-case).
It would be interesting to see an actual example where this is applied successfully in data filtering and sorting, where multiple tables are involved.
Queries cannot be inspected (all the fields are private), so you cannot control the input at all and you‘d allow essentially arbitrary data loading at that point. That‘s imo a way to open interface to expose.
I like your approach, thanks for sharing!
The problem I have with dataloader is that it lacks a lot of documentation on best practices and how to achieve loading deeply nested items, and conditionally loading based on other record relationships. For 2 years I’ve bumbled and stumbled through this library and still am very confused on how it should be used outside of Absinthe.
Yes it is not so easy… but I could get some insight from this non-free course