Динамические запросы
Исходный кодEcto изначально проектировался с выразительным API запросов, использующим синтаксис Elixir для написания запросов, предварительно компилируемых ради производительности и безопасности. При построении запросов можно использовать синтаксис с ключевыми словами
import Ecto.Query from p in Post, where: p.author == "José" and p.category == "Elixir", where: p.published_at > ^minimum_date, order_by: [desc: p.published_at]
или синтаксис на основе конвейера
import Ecto.Query Post |> where([p], p.author == "José" and p.category == "Elixir") |> where([p], p.published_at > ^minimum_date) |> order_by([p], desc: p.published_at)
Оба API также можно комбинировать. Например, представьте, что вы хотите вынести фильтрацию published_at и сортировку в функцию. При синтаксисе с ключевыми словами можно написать так:
def most_recent_from(query, minimum_date) do
from p in query,
where: p.published_at > ^minimum_date,
order_by: [desc: p.published_at]
end
а при синтаксисе с конвейером:
def most_recent_from(query, minimum_date) do query |> where([p], p.published_at > ^minimum_date) |> order_by([p], desc: p.published_at) end
Примеры выше показывают, что запросы можно создавать и комбинировать на высоком уровне: объединяя каждый вызов where, order_by и так далее. Однако иногда требуется динамически определять содержимое самих where или order_by. Например, веб-приложение может предоставлять поиск по существующим публикациям. Пользователь должен иметь возможность задать несколько критериев, таких как имя автора, категория публикации, интервал публикации и т. д.
Кроме того, хотя многие разработчики предпочитают синтаксис на основе конвейера, необходимость повторять привязку p делает его довольно многословным по сравнению с синтаксисом с ключевыми словами.
Для решения этих задач Ecto предоставляет API, ориентированный на структуры данных, для построения запросов, а также мощный механизм динамических запросов. Давайте рассмотрим его.
Ориентация на структуры данных
Ecto предлагает более простой API для запросов как с ключевыми словами, так и на основе конвейера, используя структуры данных как полноценные сущности. Рассмотрим пример:
from p in Post, where: [author: "José", category: "Elixir"], where: p.published_at > ^minimum_date, order_by: [desc: :published_at]
и
Post |> where(author: "José", category: "Elixir") |> where([p], p.published_at > ^minimum_date) |> order_by(desc: :published_at)
Обратите внимание: в большинстве выражений нам удалось отказаться от селектора p. Все конструкции Ecto принимают структуры данных в качестве входных значений. Такие структуры данных также можно задавать динамически, как показано ниже:
where = [author: "José", category: "Elixir"] order_by = [desc: :published_at] Post |> where(^where) |> where([p], p.published_at > ^minimum_date) |> order_by(^order_by)
Использование структур данных уже обеспечивает запросам Ecto значительную гибкость, однако не все выражения можно преобразовать в структуры данных. Например, where преобразует пару «ключ-значение» в сравнение key == value, поэтому сравнения на основе порядка, такие как p.published_at > ^minimum_date, по-прежнему нужно записывать как раньше.
Динамические фрагменты
Для случаев, когда нельзя полагаться на структуры данных, но при этом нужно динамически строить запросы, в Ecto предусмотрен макрос Ecto.Query.dynamic/2.
Макрос dynamic позволяет условно создавать фрагменты запросов и интерполировать их в основной запрос. Например, представьте, что в примере выше вам может понадобиться фильтровать публикации по дате публикации. Конечно, можно написать так:
query =
Post
|> where(^where)
|> order_by(^order_by)
query =
if published_at = params["published_at"] do
where(query, [p], p.published_at < ^published_at)
else
query
end
Но с помощью динамических фрагментов это можно записать и так:
where = [author: "José", category: "Elixir"]
order_by = [desc: :published_at]
filter_published_at =
if published_at = params["published_at"] do
dynamic([p], p.published_at < ^published_at)
else
true
end
Post
|> where(^where)
|> where(^filter_published_at)
|> order_by(^order_by)
Макрос dynamic позволяет создавать динамические выражения, которые затем интерполируются в запрос. Выражения dynamic также можно интерполировать в динамические выражения, что позволяет разработчикам без труда создавать сложные выражения динамически.
Используя динамические фрагменты, можно отделить обработку параметров от построения запроса. Рассмотрим более сложный пример.
Построение динамических запросов
Вернёмся к исходной задаче. Мы хотим реализовать поиск, позволяющий пользователю задавать множество способов обхода всех публикаций. Например, пользователь может выбрать способ сортировки данных, фильтрацию по автору и категории, а также выбрать публикации, созданные после определённой даты.
Чтобы решить эту задачу в Ecto, можно разбить её на ряд небольших функций, создающих структуры данных или динамические фрагменты, а затем интерполировать их в запрос:
def filter(params) do
Post
|> order_by(^filter_order_by(params["order_by"]))
|> where(^filter_where(params))
end
def filter_order_by("published_at_desc"),
do: [desc: dynamic([p], p.published_at)]
def filter_order_by("published_at"),
do: [asc: dynamic([p], p.published_at)]
def filter_order_by(_),
do: []
def filter_where(params) do
Enum.reduce(params, dynamic(true), fn
{"author", value}, dynamic ->
dynamic([p], ^dynamic and p.author == ^value)
{"category", value}, dynamic ->
dynamic([p], ^dynamic and p.category == ^value)
{"published_at", value}, dynamic ->
dynamic([p], ^dynamic and p.published_at > ^value)
{_, _}, dynamic ->
# Not a where parameter
dynamic
end)
end
Поскольку нам удалось разбить задачу на небольшие функции, принимающие обычные структуры данных, мы можем использовать все доступные в Elixir инструменты для работы с данными. Для обработки параметра order_by может быть удобнее всего использовать сопоставление с образцом для параметра order_by. Для построения условия where можно использовать reduce, начав с пустого динамического выражения (которое всегда возвращает true) и уточняя его новыми условиями по мере обработки параметров.
Тестирование тоже становится проще: каждую функцию можно проверять отдельно, даже при использовании динамических запросов:
test "filter published at based on the given date" do
assert dynamic_match?(
filter_where(%{}),
"true"
)
assert dynamic_match?(
filter_where(%{"published_at" => "2010-04-17"}),
"true and q.published_at > ^\"2010-04-17\""
)
end
defp dynamic_match?(dynamic, string) do
inspect(dynamic) == "dynamic([q], #{string})"
end
В примере выше мы создали небольшой вспомогательный метод, позволяющий проверять содержимое динамического выражения путём сопоставления с результатами inspect(dynamic).
Динамические запросы и объединения
Объединения таблиц в запросах тоже можно задавать динамически. Например, внесём два изменения в пример выше. Предположим, что теперь можно также сортировать по имени автора ("author_name" и "author_name_desc"), а авторы хранятся в отдельной таблице, поэтому фильтру по авторам в filter_where теперь нужно использовать таблицу объединения.
Итоговое решение будет выглядеть так:
def filter(params) do
Post
# 1. Add named join binding
|> join(:inner, [p], assoc(p, :authors), as: :authors)
|> order_by(^filter_order_by(params["order_by"]))
|> where(^filter_where(params))
end
# 2. Returned dynamic with join binding
def filter_order_by("published_at_desc"),
do: [desc: dynamic([p], p.published_at)]
def filter_order_by("published_at"),
do: dynamic([p], p.published_at)
def filter_order_by("author_name_desc"),
do: [desc: dynamic([authors: a], a.name)]
def filter_order_by("author_name"),
do: dynamic([authors: a], a.name)
def filter_order_by(_),
do: []
# 3. Change the authors clause inside reduce
def filter_where(params) do
Enum.reduce(params, dynamic(true), fn
{"author", value}, dynamic ->
dynamic([authors: a], ^dynamic and a.name == ^value)
{"category", value}, dynamic ->
dynamic([p], ^dynamic and p.category == ^value)
{"published_at", value}, dynamic ->
dynamic([p], ^dynamic and p.published_at > ^value)
{_, _}, dynamic ->
# Not a where parameter
dynamic
end)
end
Чтобы в будущем добавить новые фильтры, достаточно добавить новые условия в вызов Enum.reduce/3 в filter_where.
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/dynamic-queries.html