Ecto hierarchical queries beyond preload
The usual way to get a tree of nested structs from an Ecto query is with preload
. The preload
option lets you load associations for an Ecto schema, either by issuing a separate query:
Repo.all from p in Post, preload: [:comments]
or by restructuring the result of an explicitly written join tree:
Repo.all from p in Post,
join: c in assoc(p, :comments),
where: c.published_at > p.updated_at,
preload: [comments: c]
This works when the nested structs are already associations in the schema, but what if we want to load ad-hoc fields, or if we don't want to use a schema at all?
Manually rearranging the result in Elixir
For example, suppose users can react to comments, so each Comment
has a number of Reaction
children. How would we return a list of all posts, as well as the most recent comment for each post, as well as all the reactions for each comment? I.e. we want to somehow return:
[
%{
post: %Post{},
last_comment: %Comment{
reactions: [
%Reaction{},
...
]
}
},
...
]
The SQL is straightforward:
select ...
from posts p
left lateral join (
select ...
from comments c
where c.post_id = p.id
order by c.inserted_at desc
limit 1
) lc on true
left join reactions r on r.comment_id = lc.id
One option is to translate the SQL directly into Ecto query syntax:
Repo.all from p in Posts,
as: :post,
left_lateral_join: lc in subquery(
from c in Comment,
where: c.post_id == parent_as(:post).id,
order_by: [:desc, c.inserted_at],
limit: 1),
on: true,
left_join: reaction in assoc(lc, :reactions),
select: %{post: p, last_comment: lc, reaction: r}
)
This returns a flat list of rows, so we need to manually restructure the result into a list of nested structs:
# Input:
# [
# %{post: %Post{}, last_comment: %Comment{}, reaction: %Reaction{}},
# ...
# ]
#
# Output:
# [
# %{post: %Post{}, last_comment: %Comment{reactions: [%Reaction{}, ...]}}
# ]
def restructure(rows) do
# Elixir code goes here
end
This is a fair bit of work, but workable and very flexible.
Using JSON
We can save some work by returning a tree-like object from the database with json_agg
:
Add association and preload
Cannot be done dynamically. Why?