View Source Resourceful.Type.Ecto.Query (Resourceful v0.1.1)
This module is something of hack designed to deal with the fact that Ecto does not make it simple to:
- Dynamically assigned aliases to joins. When specifying
:as
it must be a compile-time atom. A recent reference to this issue can be found here. This was learned the hard way. This is especially frustrating because the aside from assignment, the aliases can be referenced dynamically throughout the rest of the query DSL with a^
. - Preload joins if a join is already present and handle join introspection in general.
The easiest way to solve this was to manipulate the struct because there's nothing magical about aliases. They're just a named reference to a positional binding. Preloads operate similarly. A tree of tuples and keyword lists defines which associations should be preloaded.
Join introspection is also important so fields can be added to a query in an ad-hoc manner without having to care whether the query contains joins or not.
a-note-about-intended-use-cases
A Note About Intended Use Cases
It's important to remember that unless you go out of your way to mess with data structures in fields (I say this because this module is going out of its way to mess with data structures) relationships that support inclusion must follow a one-to-one path from start to finish.
See Resourceful.Type.Relationship
for a broader explanation on this design
decision.
is-this-a-good-idea
Is this a good idea?
It's certainly possible the underlying Ecto.Query
structs change. This sort
of manipulation is effectively using a private API. Hopefully getting proper
support for this behavior is doable.
Additionally, it's possible to handle some (and maybe all) of this using macros. However, that ultimately seemed every more convoluted and confusing. At least this is mostly simple recursion.
What it does do is solve the problem it needed to solve.
Link to this section Summary
Functions
Including a field does two things
Joining a field joins all associations connected to all relationships in the
field's graph. For example, if viewing a song and the field
album.artist.name
is joined, two relationships will be joined: the song's
album
, and the album's artist
.
Like join_field/2
except it takes a type and a field name rather than the
graphed field directly.
Link to this section Functions
@spec include_field( any(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, String.t() ) :: %Ecto.Query{ aliases: term(), assocs: term(), combinations: term(), distinct: term(), from: term(), group_bys: term(), havings: term(), joins: term(), limit: term(), lock: term(), offset: term(), order_bys: term(), prefix: term(), preloads: term(), select: term(), sources: term(), updates: term(), wheres: term(), windows: term(), with_ctes: term() }
Including a field does two things:
- It automatically joins any necessary relationships (see
join_field/3
). - It preloads said relationships.
This allows a single query to bring back all of the data and also allows filtering and sorting to work on related attributes, not just those of the root type.
For reference, an Ecto.Query
stores preload instructions meant to happen
with joins in the :assocs
key in the form of a tree of keyword lists and
tuples.
A single join looks like this: [<assoc_name>: {<position>, []}]
The tuple
contains the position of the binging for the related join and a list of any
child joins which would be in the same form as the parent.
In the song/album/artist example this could look like the following:
[album: {1, [artist: {2, []}]}]
.
@spec join_field(any(), %Resourceful.Type.GraphedField{ field: term(), map_to: term(), name: term(), parent: term(), query_alias: term() }) :: %Ecto.Query{ aliases: term(), assocs: term(), combinations: term(), distinct: term(), from: term(), group_bys: term(), havings: term(), joins: term(), limit: term(), lock: term(), offset: term(), order_bys: term(), prefix: term(), preloads: term(), select: term(), sources: term(), updates: term(), wheres: term(), windows: term(), with_ctes: term() }
Joining a field joins all associations connected to all relationships in the
field's graph. For example, if viewing a song and the field
album.artist.name
is joined, two relationships will be joined: the song's
album
, and the album's artist
.
The join will be given an alias of the full relationship name. So, the album
will be aliases as album
and the artist will be aliased as album.artist
.
If those joins have already been made, the queryable will remain unchanged.
Fields resolving to attributes are ignored, but if they are children of a relationship, the relationship will be included.
@spec join_field( any(), %Resourceful.Type{ cache: term(), fields: term(), id: term(), max_depth: term(), max_filters: term(), max_sorters: term(), meta: term(), name: term(), registry: term() }, String.t() ) :: %Ecto.Query{ aliases: term(), assocs: term(), combinations: term(), distinct: term(), from: term(), group_bys: term(), havings: term(), joins: term(), limit: term(), lock: term(), offset: term(), order_bys: term(), prefix: term(), preloads: term(), select: term(), sources: term(), updates: term(), wheres: term(), windows: term(), with_ctes: term() }
Like join_field/2
except it takes a type and a field name rather than the
graphed field directly.