Build an Ecto select a field at a time

almirsarajcic

almirsarajcic

1 hour ago

0 comments

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.

Ecto.Query.select_merge/3 docs

Comments (0)

Sign in with GitHub to join the discussion