Spec-Zone.ru › Phoenix

Динамические запросы

Исходный код

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.

← Предыдущая страница Типы длительности с Postgrex
Следующая страница → Встроенные схемы

Пакет Hex Предпросмотр на Hex (текущий файл) Просмотреть llms.txt Скачать версию ePub

Создано с помощью ExDoc (v0.40.2) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/dynamic-queries.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API