We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Build an Ecto select a field at a time
almirsarajcic
An Ecto query has one select, which can make optional query helpers awkward. select_merge/3 lets each helper add fields to an existing map select.
defmodule MyApp.Blog do
import Ecto.Query
alias MyApp.Blog.Post
def posts_query do
from p in Post,
as: :post,
where: p.published,
order_by: [desc: p.inserted_at],
select: %{post: p}
end
def with_comment_count(query) do
from [post: p] in query,
left_join: c in assoc(p, :comments),
group_by: p.id,
select_merge: %{comment_count: count(c.id)}
end
def with_author_name(query) do
from [post: p] in query,
join: a in assoc(p, :author),
select_merge: %{author_name: a.name}
end
end
Each helper adds the field it owns. They can then be combined at the call site:
posts_query()
|> with_comment_count()
|> Repo.all()
posts_query()
|> with_comment_count()
|> with_author_name()
|> Repo.all()
The values passed to select_merge must be maps. Starting with %{post: p} above leaves room for computed fields while keeping the complete %Post{} under the :post key.
If a query has no explicit select, Ecto selects the source struct. A merge into that struct may only set fields declared on the schema, including virtual fields. Use a map when you need arbitrary keys:
# Raises if Post does not declare :comment_count.
from p in Post, select: p, select_merge: %{comment_count: 7}
# A map select accepts the additional key.
from p in Post, select: %{id: p.id, title: p.title}, select_merge: %{comment_count: 7}
Do not use select_merge to set association fields; preloading the association will overwrite them. Ecto also handles a struct from an outer join specially: when the joined row is missing, the selected value remains nil rather than becoming a struct full of nil fields.
copied to clipboard