Агрегаты и подзапросы
Исходный кодТеперь пора обсудить агрегаты и подзапросы. Как мы увидим, одно напрямую основано на другом.
Агрегаты
Ecto предоставляет в репозиториях удобную функцию для вычисления агрегатов.
Например, предположим, что у каждого поста есть целочисленный столбец с именем visits. Тогда можно найти среднее количество посещений всех постов:
MyApp.Repo.aggregate(MyApp.Post, :avg, :visits) #=> Decimal.new(1743)
Внутренне приведённый выше запрос преобразуется в:
MyApp.Repo.one(from p in MyApp.Post, select: avg(p.visits))
Функция Ecto.Repo.aggregate/4 поддерживает любые агрегатные операции, перечисленные в модуле Ecto.Query.API.
На первый взгляд реализация aggregate/4 довольно проста. Можно даже задаться вопросом, зачем вообще эта функция появилась в Ecto. Однако при использовании в запросах условий limit, offset или distinct возникают сложности.
Представим, что вместо вычисления среднего значения для всех постов вы хотите узнать среднее значение только для 10 лучших. Первая попытка может выглядеть так:
MyApp.Repo.one(
from p in MyApp.Post,
order_by: [desc: :visits],
limit: 10,
select: avg(p.visits)
)
#=> Decimal.new(1743)
Упс. Приведённый выше запрос вернул то же значение, что и предыдущие запросы. Параметр limit: 10 здесь ни на что не влияет, поскольку ограничивает результат агрегирования, а запросы с агрегатами в любом случае возвращают только одну строку. Чтобы получить правильный результат, сначала нужно найти 10 лучших постов и лишь затем выполнить агрегирование. Именно это и делает aggregate/4:
query =
from MyApp.Post,
order_by: [desc: :visits],
limit: 10
MyApp.Repo.aggregate(query, :avg, :visits)
#=> Decimal.new(4682)
Если в запросе указаны limit, offset или distinct, aggregate/4 автоматически оборачивает указанный запрос в подзапрос. Поэтому запрос, выполняемый приведённым выше aggregate/4, фактически эквивалентен следующему:
inner_query =
from MyApp.Post,
order_by: [desc: :visits],
limit: 10
query =
from q in subquery(inner_query),
select: avg(q.visits)
MyApp.Repo.one(query)
Рассмотрим подзапросы подробнее.
Подзапросы
В предыдущем разделе мы уже рассмотрели запросы, которые было бы трудно выразить без поддержки подзапросов. Это один из многих примеров, которые привели к добавлению подзапросов в Ecto.
Подзапросы в Ecto создаются вызовом Ecto.Query.subquery/1. Эта функция принимает любую структуру данных, которую можно преобразовать в запрос с помощью протокола Ecto.Queryable, и возвращает конструкцию подзапроса (которая также поддерживает запросы).
В Ecto подзапрос может выбирать целую таблицу (p) или поле (p.field). Все поля, выбранные в подзапросе, доступны из родительского запроса. Вернёмся к запросу с агрегатом из предыдущего раздела:
inner_query =
from MyApp.Post,
order_by: [desc: :visits],
limit: 10
query =
from q in subquery(inner_query),
select: avg(q.visits)
MyApp.Repo.one(query)
Поскольку в запросе не указано условие :select, он вернёт select: p, где p определяется схемой MyApp.Post. Так как запрос вернёт все поля из MyApp.Post, после преобразования в подзапрос все поля из MyApp.Post будут доступны в родительском запросе, например q.visits. Более того, Ecto сохраняет свойства схемы между запросами. Например, если написать q.field_that_does_not_exist, запрос Ecto не скомпилируется.
Если в подзапросе нужно сослаться на родительский запрос, это можно сделать с помощью именованных привязок в родительском запросе и Ecto.Query.API.parent_as/1 в подзапросе. Вот пример:
inner_query =
from c in Comment,
where: parent_as(:posts).id == c.post_id
query =
from p in Post,
as: :posts,
inner_lateral_join: c in subquery(inner_query)
MyApp.Repo.one(query)
Ecto также позволяет возвращать из подзапроса карту Elixir, ключи которой становятся напрямую доступны родительскому запросу.
Рассмотрим последний пример. Представим, что вы управляете библиотекой (обычной библиотекой в реальном мире) и у вас есть таблица, в которой регистрируется каждый случай выдачи книги. В таблице "lendings" используется автоматически увеличиваемый первичный ключ, и ей может соответствовать следующая схема:
defmodule Library.Lending do
use Ecto.Schema
schema "lendings" do
belongs_to :book, MyApp.Book # defines book_id
belongs_to :visitor, MyApp.Visitor # defines visitor_id
end
end
Теперь предположим, что мы хотим получить название каждой книги и имя последнего человека, которому библиотека её выдала. Для этого нужно найти последний идентификатор выдачи каждой книги, а затем выполнить объединение с таблицами книг и посетителей. С подзапросами это довольно просто:
last_lendings =
from l in MyApp.Lending,
group_by: l.book_id,
select: %{
book_id: l.book_id,
last_lending_id: max(l.id)
}
from l in Lending,
join: last in subquery(last_lendings),
on: last.last_lending_id == l.id,
join: b in assoc(l, :book),
join: v in assoc(l, :visitor),
select: {b.name, v.name}
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/aggregates-and-subqueries.html