Spec-Zone.ru › Phoenix

Ecto.Query

Исходный код

Предоставляет DSL для запросов.

Запросы используются для получения и изменения данных из репозитория (см. Ecto.Repo). Запросы Ecto бывают двух видов: основанные на ключевых словах и основанные на макросах. В большинстве примеров будет использоваться синтаксис на основе ключевых слов; синтаксис на основе макросов будет рассмотрен в последующих разделах.

Рассмотрим пример запроса:

# Imports only from/2 of Ecto.Query
import Ecto.Query, only: [from: 2]

# Create a query
query = from u in "users",
          where: u.age > 18,
          select: u.name

# Send the query to the repository
Repo.all(query)

В приведённом выше примере мы напрямую обращаемся к таблице "users" в базе данных. Запросы не обращаются к хранилищу данных, пока не будут переданы в качестве аргументов функции из Ecto.Repo.

Выражения запросов

Ecto поддерживает в запросах ограниченный набор выражений. Например, в приведённом ниже запросе мы используем u.age для доступа к полю, оператор сравнения > и литерал 0:

query = from u in "users", where: u.age > 0, select: u.name

Полный список операций можно найти в Ecto.Query.API. Помимо перечисленных там операций, в запросах поддерживаются следующие литералы:

  • Целые числа: 1, 2, 3
  • Числа с плавающей запятой: 1.0, 2.0, 3.0
  • Логические значения: true, false
  • Бинарные данные: <<1, 2, 3>>
  • Строки: "foo bar", ~s(this is a string)
  • Атомы (кроме логических значений и nil): :foo, :bar
  • Массивы: [1, 2, 3], ~w(interpolate words)

Все остальные типы и динамические значения необходимо передавать в качестве параметра с помощью интерполяции, как описано ниже.

Интерполяция и приведение типов

В выражение запроса можно подставить внешние значения и выражения Elixir с помощью ^:

def with_minimum(age, height_ft) do
  from u in "users",
    where: u.age > ^age and u.height > ^(height_ft * 3.28),
    select: u.name
end

with_minimum(18, 5.0)

При интерполяции значений может потребоваться явно указать Ecto ожидаемый тип интерполируемого значения:

age = "18"
Repo.all(from u in "users",
          where: u.age > type(^age, :integer),
          select: u.name)

В приведённом выше примере Ecto приведёт возраст к типу integer. Если значение невозможно привести к нужному типу, будет вызвано исключение Ecto.Query.CastError.

Чтобы не указывать типы каждый раз, можно определить Ecto.Schema. В таком случае Ecto проанализирует запросы и автоматически приведёт интерполируемое значение "age" при сравнении с полем u.age, если поле age определено в схеме с типом :integer:

age = "18"
Repo.all(from u in User, where: u.age > ^age, select: u.name)

Ещё одно преимущество использования схем состоит в том, что больше не нужно указывать параметр select в запросах: по умолчанию Ecto получает все поля, заданные в схеме:

age = "18"
Repo.all(from u in User, where: u.age > ^age)

Поэтому в дальнейших примерах мы будем использовать схемы, но помните: Ecto не требует их для создания запросов.

Сравнение с nil

Сравнение с nil в фильтрах, например where и having, запрещено и приводит к ошибке:

# Raises if age is nil
from u in User, where: u.age == ^age

Это сделано в целях безопасности, чтобы предотвратить атаки, пытающиеся перебирать записи с колонками, содержащими nil. Чтобы проверить, что значение равно nil, используйте вместо этого is_nil/1:

from u in User, where: is_nil(u.age)

Композиция

Запросы Ecto можно компоновать. Например, приведённый выше запрос можно определить в две части:

# Create a query
query = from u in User, where: u.age > 18

# Extend the query
query = from u in query, select: u.name

Для композиции запросов используется тот же синтаксис, что и для создания запроса. Отличие в том, что вместо схемы, например User, справа от in мы передали сам запрос.

Справа от in можно использовать любое значение, если оно реализует протокол Ecto.Queryable. Пока нам известно, что этот протокол реализован и для атомов (например, User), и для строк (например, "users").

В любом случае, независимо от того, была ли указана схема, запросы Ecto всегда можно компоновать благодаря системе привязок.

Позиционные привязки

Слева от in мы указываем привязки запроса. Это делается внутри конструкций from и join. В запросе ниже u — это привязка, а u.age — обращение к полю через эту привязку.

query = from u in User, where: u.age > 18

Привязки не предоставляются из запроса. При композиции запросов необходимо повторно указывать привязки для каждого уточняющего запроса. Например, чтобы дополнительно сузить приведённый выше запрос, нужно снова сообщить Ecto, какие привязки следует ожидать:

query = from u in query, select: u.city

В Ecto привязки являются позиционными, и их имена не обязаны совпадать в исходном и уточняющем запросах. Например, приведённый выше запрос можно также записать так:

query = from q in query, select: q.city

Для Ecto это ничего не изменит. Это важно, поскольку позволяет разработчикам компоновать запросы, не заботясь о привязках, использованных в исходном запросе.

При использовании соединений привязки должны соответствовать порядку, в котором они указаны:

# Create a query
query = from p in Post,
          join: c in Comment, on: c.post_id == p.id

# Extend the query
query = from [p, c] in query,
          select: {p.title, c.body}

При композиции не обязательно указывать все привязки. Например, чтобы упорядочить приведённые выше результаты по дате добавления публикации, можно дополнить запрос следующим образом:

query = from q in query, order_by: q.inserted_at

Приведённый выше пример будет работать, если исходный запрос содержит 1 или 10 привязок. Если число привязок меньше числа from и join, Ecto сопоставит только указанные вами привязки. Первая привязка всегда соответствует источнику, заданному в from.

Аналогично, если вас интересует только последняя привязка (или последние привязки) в запросе, можно использовать ..., чтобы обозначить «все предыдущие привязки» и сопоставить последнюю.

Предположим, например, что вы написали:

posts_with_comments =
  from p in query, join: c in Comment, on: c.post_id == p.id

Теперь мы хотим убедиться, что возвращаются и заголовок публикации, и текст комментария. Хотя мы можем не знать, сколько привязок есть в запросе, мы уверены, что posts — первая привязка, а comments — последняя, поэтому можем написать:

from [p, ..., c] in posts_with_comments, select: {p.title, c.body}

Иными словами, ... включает все привязки между первой и последней — их может быть одна, несколько или ни одной.

Именованные привязки

Ещё один способ гибко создавать запросы с соединениями — использовать именованные привязки. Вернёмся к предыдущему примеру: с помощью параметра as: :comment можно присвоить соединению comments конкретное имя:

posts_with_comments =
  from p in Post,
    join: c in Comment, as: :comment, on: c.post_id == p.id

Теперь мы можем обращаться к нему с помощью следующей формы списка привязок:

from [p, comment: c] in posts_with_comments, select: {p.title, c.body}

Такой подход избавляет от необходимости отслеживать позиции привязок при композиции запроса. Параметр :as можно задать как для соединений, так и для from:

from p in Post, as: :post

В качестве имён привязок допускаются только атомы. Ссылки на именованные привязки всегда должны располагаться в конце списка привязок:

[positional_binding_1, positional_binding_2, named_1: binding, named_2: binding]

Именованные привязки также можно использовать для отложенной привязки с конструкцией as/1, позволяющей ссылаться на привязку, которая ещё не определена:

from c in Comment, where: as(:posts).id == c.post_id

Это особенно полезно при работе с подзапросами, когда может понадобиться ссылка на родительскую привязку через parent_as, которая неизвестна при написании подзапроса:

child_query = from c in Comment, where: parent_as(:posts).id == c.post_id
from p in Post, as: :posts, inner_lateral_join: c in subquery(child_query)

При построении запросов можно также сопоставить конкретную привязку. Например, предположим, что вы хотите создать универсальную функцию сортировки, которая упорядочивает результаты по заданному field с заданным as в query:

# Knowing the name of the binding
def sort(query, as, field) do
  from [{^as, x}] in query, order_by: field(x, ^field)
end

Операции без привязок

Хотя привязки очень полезны при работе с соединениями, они не нужны, если запрос содержит только предложение from. Для таких случаев Ecto позволяет создавать запросы без указания привязки:

from Post,
  where: [category: "fresh and new"],
  order_by: [desc: :published_at],
  select: [:id, :title, :body]

Приведённый выше запрос выберет все публикации с категорией "fresh and new", упорядочит их по дате последней публикации и вернёт структуры Post, в которых заданы только поля id, title и body. Это эквивалентно следующему:

from p in Post,
  where: p.category == "fresh and new",
  order_by: [desc: p.published_at],
  select: struct(p, [:id, :title, :body])

Одно из преимуществ запросов без привязок — их управление данными, поэтому они удобны для динамического построения запросов. Например, приведённый выше запрос можно также записать так:

where = [category: "fresh and new"]
order_by = [desc: :published_at]
select = [:id, :title, :body]
from Post, where: ^where, order_by: ^order_by, select: ^select

Эта возможность очень полезна, когда запросы нужно строить на основе пользовательского ввода, например для форм веб-поиска, интерфейсов командной строки и т. д.

Фрагменты

Если нужен обходной путь, Ecto предоставляет фрагменты (см. Ecto.Query.API.fragment/1) для вставки фрагментов SQL (и не только SQL) в запросы.

Например, чтобы получить все публикации и при этом выполнить в базе данных функцию "lower(?)", подставив p.title вместо ?, можно написать:

from p in Post,
  where: is_nil(p.published_at) and
         fragment("lower(?)", p.title) == ^title

Кроме того, большинство адаптеров предоставляют прямые API для запросов, например Ecto.Adapters.SQL.query/4, позволяя разработчикам полностью обходить запросы Ecto.

API макросов

Во всех предыдущих примерах для создания запроса использовался синтаксис запросов на основе ключевых слов:

import Ecto.Query
from u in "users", where: u.age > 18, select: u.name

Поскольку в Elixir широко используется оператор конвейера, Ecto также поддерживает синтаксис на его основе:

"users"
|> where([u], u.age > 18)
|> select([u], u.name)

Примеры на основе ключевых слов и конвейера эквивалентны. Недостаток использования макросов состоит в том, что привязку необходимо указывать для каждой операции. Однако, поскольку примеры на основе ключевых слов и конвейера эквивалентны, синтаксис без привязок также работает с макросами. Обратите внимание: следующий пример не полностью эквивалентен предыдущему, поскольку возвращает не имя, а структуру User:

"users"
|> where([u], u.age > 18)
|> select([:name])

Такой синтаксис позволяет разработчикам использовать привязки только в более сложных выражениях запросов.

В этом модуле описан каждый из этих макросов и приведены примеры как в формате запросов на основе ключевых слов, так и в формате выражений с конвейером.

Префикс запроса

Для запросов можно задать префикс. Для пользователей Postgres он указывает схему, в которой находится таблица, а для пользователей MySQL — базу данных, в которой находится таблица. Если префикс не задан, предполагается, что запросы Postgres относятся к схеме public, а запросы MySQL — к базе данных, указанной в конфигурации репозитория.

Префикс запроса можно задать как для всего запроса, так и для каждого отдельного выражения from и join. Если для from или join не задан prefix, используется префикс схемы, заданной для from или join. Префикс запроса используется, только если не задан ни один из перечисленных выше префиксов.

Рассмотрим несколько примеров. Проще всего задать префикс глобально, передав параметр операции репозитория:

results = Repo.all(query, prefix: "accounts")

Можно также задать префикс для всего запроса, установив поле prefix:

results =
  query # May be User or an Ecto.Query itself
  |> Ecto.Query.put_query_prefix("accounts")
  |> Repo.all()

Установка префикса в запросе изменяет префикс по умолчанию для всех выражений from и join. Переопределить префикс запроса можно, задав @schema_prefix в определениях схемы или передав параметр prefix:

from u in User,
  prefix: "accounts",
  join: p in assoc(u, :posts),
  prefix: "public"

Итак, при выборе префикса используется следующий порядок приоритетов:

  1. Наивысший приоритет имеет параметр :prefix, переданный в from/join
  2. Затем используется атрибут @schema_prefix, объявленный в схеме, переданной в from/join
  3. Затем используется префикс запроса. Его можно задать в запросе с помощью put_query_prefix/2 или передать параметр :prefix при вызове модуля Repo (если используются оба способа, приоритет имеет первый)

Префиксы, заданные в запросе, сохраняются при загрузке данных.

Сводка

Типы

dynamic_expr()
t()

Функции

%Ecto.Query{}

Структура Ecto.Query.

distinct(query, binding \\ [], expr)

Выражение distinct в запросе.

dynamic(binding \\ [], expr)

Создаёт динамическое выражение запроса.

except(query, other_query)

Выражение except (разность множеств) в запросе.

except_all(query, other_query)

Выражение except (разность множеств) в запросе.

exclude(query, field)

Сбрасывает ранее заданное поле или поля запроса.

first(queryable, order_by \\ nil)

Ограничивает запрос, возвращая первый результат в порядке первичного ключа.

from(expr, kw \\ [])

Создаёт запрос.

group_by(query, binding \\ [], expr)

Выражение group by в запросе.

has_named_binding?(queryable, key)

Возвращает true, если запрос содержит привязку с заданным именем, и false в противном случае.

having(query, binding \\ [], expr)

Выражение having с оператором AND в запросе.

intersect(query, other_query)

Выражение intersect в запросе.

intersect_all(query, other_query)

Выражение intersect в запросе.

is_named_binding(query, name)

То же, что has_named_binding?/2, но допускается в условиях guard.

join(query, qual, binding \\ [], expr, opts \\ [])

Выражение join в запросе.

last(queryable, order_by \\ nil)

Ограничивает запрос, возвращая последний результат в порядке первичного ключа.

limit(query, binding \\ [], expr)

Выражение limit в запросе.

lock(query, binding \\ [], expr)

Выражение блокировки в запросе.

offset(query, binding \\ [], expr)

Выражение offset в запросе.

or_having(query, binding \\ [], expr)

Выражение having с оператором OR в запросе.

or_where(query, binding \\ [], expr)

Выражение where с оператором OR в запросе.

order_by(query, binding \\ [], expr)

Выражение order by в запросе.

preload(query, bindings \\ [], expr)

Предварительно загружает связи в набор результатов.

prepend_order_by(query, binding \\ [], expr)

Выражение order by, добавляемое перед существующими выражениями сортировки.

put_query_prefix(query, prefix)

Устанавливает в запросе заданный префикс.

recursive_ctes(query, value)

Включает или отключает рекурсивный режим для CTE.

reverse_order(query)

Обращает порядок сортировки запроса.

select(query, binding \\ [], expr)

Выражение select в запросе.

select_merge(query, binding \\ [], expr)

Объединяемое выражение select в запросе.

subquery(query, opts \\ [])

Преобразует запрос в подзапрос.

union(query, other_query)

Выражение union в запросе.

union_all(query, other_query)

Выражение union all в запросе.

update(query, binding \\ [], expr)

Выражение update в запросе.

where(query, binding \\ [], expr)

Выражение where с оператором AND в запросе.

windows(query, binding \\ [], expr)

Определяет окна, которые можно использовать с Ecto.Query.WindowAPI.

with_cte(query, name, opts)

Общее табличное выражение (CTE), также известное как выражение WITH.

with_named_binding(query, key, fun)

Применяет функцию обратного вызова к запросу, если в нём нет заданной именованной привязки. В противном случае возвращает исходный запрос.

with_ties(query, binding \\ [], expr)

Включает или отключает сохранение связанных результатов для выражений limit.

Типы

dynamic_expr()Исходный код

@type dynamic_expr() :: %Ecto.Query.DynamicExpr{
  binding: term(),
  file: term(),
  fun: term(),
  line: term()
}

t()Исходный код

@type t() :: %Ecto.Query{
  aliases: term(),
  assocs: term(),
  combinations: term(),
  distinct: term(),
  from: term(),
  group_bys: term(),
  havings: term(),
  joins: term(),
  limit: term(),
  lock: term(),
  offset: term(),
  order_bys: term(),
  prefix: term(),
  preloads: term(),
  select: term(),
  sources: term(),
  updates: term(),
  wheres: term(),
  windows: term(),
  with_ctes: term()
}

Функции

%Ecto.Query{}Источник

Структура Ecto.Query.

Пользователям Ecto следует считать эту структуру непрозрачной и не обращаться напрямую к её полям. Авторы адаптеров могут читать её содержимое, но ни в коем случае не изменять его.

distinct(query, binding \\ [], expr)Источник

Выражение запроса distinct.

Если значение равно true, в результате остаются только уникальные значения из выражения select.

Если ваша база данных поддерживает эту возможность, можно также передать выражения запроса в distinct, и будет создан запрос с DISTINCT ON. В таких случаях distinct принимает точно такие же выражения, как order_by, а любое выражение distinct будет автоматически добавлено перед выражениями order_by, если присутствует выражение order_by.

Примеры с ключевыми словами

# Returns the list of different categories in the Post schema
from(p in Post, distinct: true, select: p.category)

# If your database supports DISTINCT ON(),
# you can pass expressions to distinct too
from(p in Post,
   distinct: p.category,
   order_by: [p.date])

# The DISTINCT ON() also supports ordering similar to ORDER BY.
from(p in Post,
   distinct: [desc: p.category],
   order_by: [p.date])

# Using atoms
from(p in Post, distinct: :category, order_by: :date)

Пример с выражениями

Post
|> distinct(true)
|> order_by([p], [p.category, p.author])

dynamic(binding \\ [], expr)Источник

Создаёт динамическое выражение запроса.

Динамические выражения запроса позволяют разработчикам составлять выражения запроса по частям, чтобы позднее их можно было интерполировать в части запроса или другого динамического выражения.

Примеры

Предположим, у вас есть набор условий, на основе которых нужно построить запрос:

conditions = false

conditions =
  if params["is_public"] do
    dynamic([p], p.is_public or ^conditions)
  else
    conditions
  end

conditions =
  if params["allow_reviewers"] do
    dynamic([p, a], a.reviewer == true or ^conditions)
  else
    conditions
  end

from query, where: ^conditions

В приведённом выше примере мы смогли составить выражения запроса по частям, используя разные привязки, а затем интерполировать их все сразу в сам запрос.

Динамическое выражение всегда можно интерполировать внутри другого динамического выражения, а также в конструкции, описанные ниже.

where, having и on у join

Макрос dynamic можно интерполировать в корне where, having или on у join.

Например, предположим, что переменная conditions определена в предыдущем разделе. Следующий вариант недопустим, поскольку выражение находится не в корне where:

from q in query, where: q.some_condition and ^conditions

К счастью, это легко исправить, просто переписав выражение следующим образом:

conditions = dynamic([q], q.some_condition and ^conditions)
from query, where: ^conditions

Границы динамических выражений

Приведение типов не распространяется через границы динамических выражений. При записи динамического выражения, например dynamic([p], p.visits > ^param), Ecto автоматически приведёт ^param к типу p.visits.

Однако, если p.visits само является динамическим, как в примере ниже, Ecto не сможет передать его тип выражению ^param:

field = dynamic([p], p.visits)
dynamic(^field > ^param)

order_by

Динамические выражения можно интерполировать внутри списков ключевых слов в корне order_by. Например, можно написать:

order_by = [
  asc: :some_field,
  desc: dynamic([p], fragment("?->>?", p.another_field, "json_key"))
]

from query, order_by: ^order_by

Динамические выражения также поддерживаются в предложениях order_by/2 внутри windows/2.

Как и в случае с where и подобными конструкциями, передавать динамические выражения за пределы корневого элемента нельзя. Например, следующий вариант работать не будет:

from query, order_by: [asc: ^dynamic(...)]

А этот будет:

from query, order_by: ^[asc: dynamic(...)]

group_by

Динамические выражения можно интерполировать внутри списков ключевых слов в корне group_by. Например, можно написать:

group_by = [
  :some_field,
  dynamic([p], fragment("?->>?", p.another_field, "json_key"))
]

from query, group_by: ^group_by

Динамические выражения также поддерживаются в предложениях partition_by/2 внутри windows/2.

Как и в случае с where и подобными конструкциями, передавать динамические выражения за пределы корневого элемента нельзя. Например, следующий вариант работать не будет:

from query, group_by: [:some_field, ^dynamic(...)]

А этот будет:

from query, group_by: ^[:some_field, dynamic(...)]

select и select_merge

Динамические выражения можно помещать в карты, интерполируемые в корне select или select_merge. Например, можно написать:

fields = %{
  period: dynamic([p], p.month),
  metric: dynamic([p], p.distance)
}

from query, select: ^fields

Как и в случае с where и подобными конструкциями, передавать динамические выражения за пределы корневого элемента нельзя. Например, следующий вариант работать не будет:

from query, select: %{field: ^dynamic(...)}

А этот будет:

from query, select: ^%{field: dynamic(...)}

Карты с динамическими выражениями также можно объединять с существующими структурами select, что позволяет создавать разнообразные частично динамические выборки:

metric = dynamic([p], p.distance)

from query, select: [:period, :metric], select_merge: ^%{metric: metric}

Также допускается задавать псевдонимы полям с помощью selected_as/2 и обращаться к ним с помощью selected_as/1:

fields = %{
  period: dynamic([p], selected_as(p.month, :month)),
  metric: dynamic([p], p.distance)
}

order = dynamic(selected_as(:month))

from query, select: ^fields, order_by: ^order

update

Внутри обновлений также поддерживается dynamic, например:

updates = [
  set: [average: dynamic([p], p.sum / p.count)]
]

from query, update: ^updates

preload

Динамические выражения можно использовать с preload, чтобы динамически задавать привязку для присоединённой ассоциации. Например, можно написать:

preloads = [
  :non_joined_assoc,
  joined_assoc: dynamic([joined: j], j)
]

from x in query,
  join: assoc(x, :joined_assoc),
  as: :joined,
  preload: ^preloads

В приведённом выше примере используется именованная привязка (:joined), но также можно использовать позиционные привязки:

preloads = [
  :non_joined_assoc,
  joined_assoc: dynamic([_, j], j)
]

from x in query,
  join: assoc(x, :joined_assoc)
  preload: ^preloads

Как и в случае с where и подобными конструкциями, передавать динамические выражения за пределы интерполируемого корневого элемента нельзя. Например, следующий вариант работать не будет:

from query, preload: [comments: ^dynamic(...)]

А этот будет:

from query, preload: ^[comments: dynamic(...)]

Динамические выражения, используемые в preload, должны вычисляться в одну привязку. Например, следующий вариант работать не будет:

preloads = dynamic([comments: c, likes: l], [comments: {c, likes: l}])

А этот будет:

dynamic_comments = dynamic([comments: c], c)
dynamic_likes = dynamic([likes: l], l)

preloads = [
  comments: {dynamic_comments, likes: dynamic_likes}
]

except(query, other_query)Источник

Выражение запроса except (разность множеств).

Вычисляет разность наборов результатов нескольких запросов. select каждого запроса должны быть абсолютно одинаковыми и иметь одинаковые типы в одинаковом порядке.

Выражение except возвращает только уникальные строки, как если бы каждый запрос возвращал уникальные результаты. Это может привести к снижению производительности. Если требуется вычислить разность нескольких наборов результатов без удаления дублирующихся строк, используйте except_all/2.

Поведение объединённых запросов

При работе с объединёнными запросами необходимо учитывать несколько особенностей поведения, иначе результат запроса может оказаться неожиданно неверным.

Сортировка, лимит и смещение

Выражения order_by, limit и offset родительского запроса применяются к результату всего объединения. order_by необходимо указывать одним из следующих способов, поскольку объединение двух или более запросов автоматически не получает псевдоним:

  • Используйте Ecto.Query.API.fragment/1, чтобы передать оператор order_by, напрямую обращающийся к полям объединения.
  • Оберните объединение в подзапрос и обратитесь к привязке подзапроса.

Порядок выбора столбцов

Столбцы каждого запроса в объединении должны указываться в совершенно одинаковом порядке. В противном случае значения одного столбца могут оказаться в другом. Это относится ко всем типам выражений select, включая карты.

Например, следующий запрос поменяет местами значения имени поставщика и города, поскольку именно в таком порядке поля указаны в запросе к клиентам.

supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
customer_query = from c in Customer, select: %{name: c.name, city: c.city}
except(supplier_query, ^customer_query)

Выбор литеральных атомов

При выборе литерального атома его значение должно быть одинаковым во всех запросах. Иначе значение из родительского запроса будет применено ко всем остальным запросам. То же относится к выбору карт с атомами в качестве ключей.

Примеры с ключевыми словами

# Unordered result
supplier_query = from s in Supplier, select: s.city
from c in Customer, select: c.city, except: ^supplier_query

# Ordered result
supplier_query = from s in Supplier, select: s.city
except_query = from c in Customer, select: c.city, except: ^supplier_query
from s in subquery(except_query), order_by: s.city

Примеры с выражениями

# Unordered result
supplier_query = Supplier |> select([s], s.city)
Customer |> select([c], c.city) |> except(^supplier_query)

# Ordered result
customer_query = Customer |> select([c], c.city) |> order_by(fragment("city"))
supplier_query = Supplier |> select([s], s.city)
except(customer_query, ^supplier_query)

except_all(query, other_query)Источник

Выражение запроса except (разность множеств).

Вычисляет разность наборов результатов нескольких запросов. select каждого запроса должны быть абсолютно одинаковыми и иметь одинаковые типы в одинаковом порядке.

Поведение объединённых запросов

При работе с объединёнными запросами необходимо учитывать несколько особенностей поведения, иначе результат запроса может оказаться неожиданно неверным.

Сортировка, лимит и смещение

Выражения order_by, limit и offset родительского запроса применяются к результату всего объединения. order_by необходимо указывать одним из следующих способов, поскольку объединение двух или более запросов автоматически не получает псевдоним:

  • Используйте Ecto.Query.API.fragment/1, чтобы передать оператор order_by, напрямую обращающийся к полям объединения.
  • Оберните объединение в подзапрос и обратитесь к привязке подзапроса.

Порядок выбора столбцов

Столбцы каждого запроса в объединении должны указываться в совершенно одинаковом порядке. В противном случае значения одного столбца могут оказаться в другом. Это относится ко всем типам выражений select, включая карты.

Например, следующий запрос поменяет местами значения имени поставщика и города, поскольку именно в таком порядке поля указаны в запросе к клиентам.

supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
customer_query = from c in Customer, select: %{name: c.name, city: c.city}
except_all(supplier_query, ^customer_query)

Выбор литеральных атомов

При выборе литерального атома его значение должно быть одинаковым во всех запросах. Иначе значение из родительского запроса будет применено ко всем остальным запросам. То же относится к выбору карт с атомами в качестве ключей.

Примеры с ключевыми словами

# Unordered result
supplier_query = from s in Supplier, select: s.city
from c in Customer, select: c.city, except_all: ^supplier_query

# Ordered result
supplier_query = from s in Supplier, select: s.city
except_all_query = from c in Customer, select: c.city, except_all: ^supplier_query
from s in subquery(except_all_query), order_by: s.city

Примеры с выражениями

# Unordered result
supplier_query = Supplier |> select([s], s.city)
Customer |> select([c], c.city) |> except_all(^supplier_query)

# Ordered result
customer_query = Customer |> select([c], c.city) |> order_by(fragment("city"))
supplier_query = Supplier |> select([s], s.city)
except_all(customer_query, ^supplier_query)

exclude(query, field)Источник

Сбрасывает ранее заданное поле или поля запроса.

Можно сбросить многие поля, кроме источника запроса (from). При исключении :join будут удалены все типы соединений. Если требуется удалить соединение только определённого типа, см. следующий абзац.

Примеры

Ecto.Query.exclude(query, :join)
Ecto.Query.exclude(query, :where)
Ecto.Query.exclude(query, :order_by)
Ecto.Query.exclude(query, :group_by)
Ecto.Query.exclude(query, :having)
Ecto.Query.exclude(query, :distinct)
Ecto.Query.exclude(query, :select)
Ecto.Query.exclude(query, :combinations)
Ecto.Query.exclude(query, :with_ctes)
Ecto.Query.exclude(query, :limit)
Ecto.Query.exclude(query, :offset)
Ecto.Query.exclude(query, :lock)
Ecto.Query.exclude(query, :preload)
Ecto.Query.exclude(query, :update)
Ecto.Query.exclude(query, :windows)

Чтобы удалить несколько элементов одновременно, передайте список

Ecto.Query.exclude(query, [:join, :where])
Ecto.Query.exclude(query, [:limit, :offset])

Можно удалять соединения определённых типов, например left_join и inner_join:

Ecto.Query.exclude(query, :inner_join)
Ecto.Query.exclude(query, :cross_join)
Ecto.Query.exclude(query, :cross_lateral_join)
Ecto.Query.exclude(query, :left_join)
Ecto.Query.exclude(query, :right_join)
Ecto.Query.exclude(query, :full_join)
Ecto.Query.exclude(query, :inner_lateral_join)
Ecto.Query.exclude(query, :left_lateral_join)

Однако имейте в виду: если соединение удалено, а его привязки использовались в других местах, привязки не будут удалены, и запрос не скомпилируется.

Можно удалять определённые окна по имени:

Ecto.Query.exclude(query, {:windows, [name1, name2]})

Если окно используется в другом месте, например в select или order_by, оно не будет удалено. Выражения необходимо воссоздать вручную.

first(queryable, order_by \\ nil)Источник

Ограничивает запрос, возвращая первый результат, отсортированный по первичному ключу.

Запрос будет автоматически отсортирован по первичному ключу, если не задан order_by и в запросе не установлен order_by. Лимит всегда устанавливается равным 1.

Примеры

Post |> first |> Repo.one
query |> first(:inserted_at) |> Repo.one

from(expr, kw \\ [])Источник

Создаёт запрос.

Запрос может быть задан как запросом с ключевыми словами, так и выражением запроса.

Если это запрос с ключевыми словами, первый аргумент должен быть выражением in, значением, реализующим протокол Ecto.Queryable, или Ecto.Query.API.fragment/1. Если в другой части выражения запросу нужна ссылка на источник данных, для создания ссылочной переменной необходимо использовать in. Второй аргумент должен быть запросом с ключевыми словами, в котором ключи обозначают типы выражений, а значения являются выражениями.

Если это выражение запроса, первый аргумент должен быть значением, реализующим протокол Ecto.Queryable, а второй аргумент — выражением.

Подсказки

Ключевое слово hints можно использовать для указания подсказок запроса:

from p in Post,
  hints: ["USE INDEX FOO"],
  where: p.title == "title"

Его также можно использовать как универсальный механизм добавления операторов после предложения from. Например, с его помощью можно включить выборку таблицы:

from p in Post,
  hints: "TABLESAMPLE SYSTEM(1)"

Подсказки from должны быть строками (или списком строк), известными во время компиляции, либо небезопасными фрагментами. Небезопасный фрагмент можно использовать для задания динамических подсказок:

sample = "SYSTEM_ROWS(1)"

from p in Post,
  hints: ["TABLESAMPLE", unsafe_fragment(^sample)]

Небезопасные фрагменты

Результат unsafe_fragment/1 будет напрямую вставлен в итоговый оператор SQL без экранирования. Поэтому никогда не следует использовать данные из ненадёжных источников, например пользовательский ввод. Иначе это может привести к опасным атакам с помощью SQL-инъекций.

Примеры с ключевыми словами

# `in` expression
from(c in City, select: c)

# Ecto.Queryable
from(City, limit: 1)

# Fragment with user-defined function and predefined columns
from(f in fragment("my_table_valued_function(arg)"), select: f.x)

# Fragment with built-in function and undefined columns
from(f in fragment("select generate_series(?::integer, ?::integer) as x", ^0, ^10), select: f.x)

# Fragment with schema
from(f in {fragment("my_table_valued_function(arg)"), Schema})

Примеры с выражениями

# Schema
City |> select([c], c)

# Source
"cities" |> select([c], c)

# Source with schema
{"cities", Source} |> select([c], c)

# Ecto.Query
from(c in City) |> select([c], c)

Примеры

def paginate(query, page, size) do
  from query,
    limit: ^size,
    offset: ^((page-1) * size)
end

В примере выше не используется in, поскольку limit и offset не требуют ссылки на источник данных. Однако для расширения запроса выражением where потребуется использовать in:

def published(query) do
  from p in query, where: not(is_nil(p.published_at))
end

Обратите внимание, что мы создали переменную p для ссылки на исходный источник данных запроса. Предполагается, что исходный запрос имел только один источник. Если в заданном запросе больше одного источника, для доступа к дополнительным источникам можно использовать позиционные или именованные привязки.

def published_multi(query) do
  from [p,o] in query,
  where: not(is_nil(p.published_at)) and not(is_nil(o.published_at))
end

Обратите внимание, что переменные p и o можно называть как угодно, поскольку их имена не имеют значения для запроса, отправляемого в базу данных.

group_by(query, binding \\ [], expr)Источник

Выражение запроса group by.

Объединяет строки схемы с одинаковыми значениями в указанных полях. Использование group_by «группирует» запрос, придавая выражению select другую семантику. Если запрос сгруппирован, в select можно использовать только поля, на которые есть ссылки в group_by, либо поля, переданные в качестве аргументов агрегатной функции.

group_by также принимает список атомов, каждый из которых обозначает поле источника. В более сложных запросах можно обращаться к полям напрямую, а не через атомы.

Примеры с ключевыми словами

# Returns the number of posts in each category
from(p in Post,
  group_by: p.category,
  select: {p.category, count(p.id)})

# Using atoms
from(p in Post, group_by: :category, select: {p.category, count(p.id)})

# Using direct fields access
from(p in Post,
  join: c in assoc(p, :category),
  group_by: [p.id, c.name])

Пример с выражениями

Post |> group_by([p], p.category) |> select([p], count(p.id))

has_named_binding?(queryable, key)Источник

Возвращает true, если запрос содержит привязку с указанным именем, иначе — false.

Дополнительные сведения об именованных привязках см. в разделе «Именованные привязки» документации этого модуля.

having(query, binding \\ [], expr)Источник

Выражение запроса AND having.

Как и where, having фильтрует строки схемы, но выполняется после группировки, поэтому для сгруппированного запроса имеет ту же семантику, что и select (см. group_by/3). having группирует запрос, даже если в нём нет выражения group_by.

Пример с ключевыми словами

# Returns the number of posts in each category where the
# average number of comments is above ten
from(p in Post,
  group_by: p.category,
  having: avg(p.num_comments) > 10,
  select: {p.category, count(p.id)})

Пример с выражениями

Post
|> group_by([p], p.category)
|> having([p], avg(p.num_comments) > 10)
|> select([p], count(p.id))

intersect(query, other_query)Источник

Выражение запроса intersect.

Вычисляет пересечение наборов результатов нескольких запросов. select каждого запроса должны быть абсолютно одинаковыми и иметь одинаковые типы в одинаковом порядке.

Выражение intersect возвращает только уникальные строки, как если бы каждый запрос возвращал уникальные результаты. Это может привести к снижению производительности. Если требуется вычислить пересечение нескольких наборов результатов без удаления дублирующихся строк, используйте intersect_all/2.

Поведение объединённых запросов

При работе с объединёнными запросами необходимо учитывать несколько особенностей поведения, иначе результат запроса может оказаться неожиданно неверным.

Сортировка, лимит и смещение

Выражения order_by, limit и offset родительского запроса применяются к результату всего объединения. order_by необходимо указывать одним из следующих способов, поскольку объединение двух или более запросов автоматически не получает псевдоним:

  • Используйте Ecto.Query.API.fragment/1, чтобы передать оператор order_by, напрямую обращающийся к полям объединения.
  • Оберните объединение в подзапрос и обратитесь к привязке подзапроса.

Порядок выбора столбцов

Столбцы каждого запроса в объединении должны указываться в совершенно одинаковом порядке. В противном случае значения одного столбца могут оказаться в другом. Это относится ко всем типам выражений select, включая карты.

Например, следующий запрос поменяет местами значения имени поставщика и города, поскольку именно в таком порядке поля указаны в запросе к клиентам.

supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
customer_query = from c in Customer, select: %{name: c.name, city: c.city}
intersect(supplier_query, ^customer_query)

Выбор литеральных атомов

При выборе литерального атома его значение должно быть одинаковым во всех запросах. Иначе значение из родительского запроса будет применено ко всем остальным запросам. То же относится к выбору карт с атомами в качестве ключей.

Примеры с ключевыми словами

# Unordered result
supplier_query = from s in Supplier, select: s.city
from c in Customer, select: c.city, intersect: ^supplier_query

# Ordered result
supplier_query = from s in Supplier, select: s.city
intersect_query = from c in Customer, select: c.city, intersect: ^supplier_query
from s in subquery(intersect_query), order_by: s.city

Примеры с выражениями

# Unordered result
supplier_query = Supplier |> select([s], s.city)
Customer |> select([c], c.city) |> intersect(^supplier_query)

# Ordered result
customer_query = Customer |> select([c], c.city) |> order_by(fragment("city"))
supplier_query = Supplier |> select([s], s.city)
intersect(customer_query, ^supplier_query)

intersect_all(query, other_query)Источник

Выражение запроса intersect.

Вычисляет пересечение наборов результатов нескольких запросов. select каждого запроса должны быть абсолютно одинаковыми и иметь одинаковые типы в одинаковом порядке.

Поведение объединённых запросов

При работе с объединёнными запросами необходимо учитывать несколько особенностей поведения, иначе результат запроса может оказаться неожиданно неверным.

Сортировка, лимит и смещение

Выражения order_by, limit и offset родительского запроса применяются к результату всего объединения. order_by необходимо указывать одним из следующих способов, поскольку объединение двух или более запросов автоматически не получает псевдоним:

  • Используйте Ecto.Query.API.fragment/1, чтобы передать оператор order_by, напрямую обращающийся к полям объединения.
  • Оберните объединение в подзапрос и обратитесь к привязке подзапроса.

Порядок выбора столбцов

Столбцы каждого запроса в объединении должны указываться в совершенно одинаковом порядке. В противном случае значения одного столбца могут оказаться в другом. Это относится ко всем типам выражений select, включая карты.

Например, следующий запрос поменяет местами значения имени поставщика и города, поскольку именно в таком порядке поля указаны в запросе к клиентам.

supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
customer_query = from c in Customer, select: %{name: c.name, city: c.city}
intersect_all(supplier_query, ^customer_query)

Выбор литеральных атомов

При выборе литерального атома его значение должно быть одинаковым во всех запросах. Иначе значение из родительского запроса будет применено ко всем остальным запросам. То же относится к выбору карт с атомами в качестве ключей.

Примеры с ключевыми словами

# Unordered result
supplier_query = from s in Supplier, select: s.city
from c in Customer, select: c.city, intersect_all: ^supplier_query

# Ordered result
supplier_query = from s in Supplier, select: s.city
intersect_all_query = from c in Customer, select: c.city, intersect_all: ^supplier_query
from s in subquery(intersect_all_query), order_by: s.city

Примеры с выражениями

# Unordered result
supplier_query = Supplier |> select([s], s.city)
Customer |> select([c], c.city) |> intersect_all(^supplier_query)

# Ordered result
customer_query = Customer |> select([c], c.city) |> order_by(fragment("city"))
supplier_query = Supplier |> select([s], s.city)
intersect_all(customer_query, ^supplier_query)

is_named_binding(query, name)Источник

То же, что и has_named_binding?/2, но эту функцию можно использовать в защитных выражениях.

join(query, qual, binding \\ [], expr, opts \\ [])Исходный код

Выражение запроса join.

Получает источник, который нужно присоединить к запросу, и условие соединения. Условием соединения может быть любое выражение, вычисляемое в логическое значение. Квалификатор должен быть одним из :inner, :left, :right, :cross, :cross_lateral, :full, :inner_lateral или :left_lateral.

В запросе на основе ключевых слов ключевое слово :join можно заменить на :inner_join, :left_join, :right_join, :cross_join, :cross_lateral_join, :full_join, :inner_lateral_join или :left_lateral_join. :join эквивалентно :inner_join.

В настоящее время можно выполнять соединение с:

  • Ecto.Schema, например p in Post
  • интерполированным запросом Ecto с нулём или более предложениями where, например c in ^(from "posts", where: [public: true])
  • ассоциацией, например c in assoc(post, :comments)
  • подзапросом, например c in subquery(another_query)
  • фрагментом запроса, например c in fragment("SOME COMPLEX QUERY"); см. раздел «Соединение с фрагментами» ниже.

Параметры

Для каждого соединения можно задать следующие параметры:

  • :on — выражение запроса или список ключевых слов для фильтрации соединения; по умолчанию — true
  • :as — именованная привязка для соединения
  • :prefix — префикс, используемый для соединения при выполнении запроса к базе данных
  • :hints — строка или список строк, используемых в качестве подсказок для базы данных

В синтаксисе запросов на основе ключевых слов эти параметры нужно указывать сразу после соединения. В синтаксисе выражений параметры передаются пятым аргументом.

Неуказанное условие соединения

Если не указать параметр :on при выполнении соединения, не являющегося перекрёстным, будет выдано предупреждение. Это помогает избежать дорогостоящих перекрёстных соединений, если они не требуются. Если такое поведение желательно, предупреждение можно отключить, заменив соединение на перекрёстное или явно задав on: true. Если такое поведение нежелательно, следует указать подходящее условие соединения.

Примеры с ключевыми словами

from c in Comment,
  join: p in Post,
  on: p.id == c.post_id,
  select: {p.title, c.text}

from p in Post,
  left_join: c in assoc(p, :comments),
  select: {p, c}

Ключевые слова также можно передавать или интерполировать как часть on:

from c in Comment,
  join: p in Post,
  on: [id: c.post_id],
  select: {p.title, c.text}

Каждый ключ в on применяется к текущему выражению соединения.

Также можно интерполировать запрос Ecto в правой части in. Например, запрос выше можно записать так:

posts = Post
from c in Comment,
  join: p in ^posts,
  on: [id: c.post_id],
  select: {p.title, c.text}

Это особенно полезно для динамического соединения с существующими запросами, например, чтобы динамически выбирать источник или выбирать между общедоступными публикациями и публикациями, недавно опубликованными:

posts =
  if params["drafts"] do
    from p in Post, where: [drafts: true]
  else
    from p in Post, where: [public: true]
  end

from c in Comment,
  join: p in ^posts, on: [id: c.post_id],
  select: {p.title, c.text}

В соединение можно интерполировать только простые запросы с выражениями where.

Примеры выражений

Comment
|> join(:inner, [c], p in Post, on: c.post_id == p.id)
|> select([c, p], {p.title, c.text})

Post
|> join(:left, [p], c in assoc(p, :comments))
|> select([p, c], {p, c})

Post
|> join(:left, [p], c in Comment, on: c.post_id == p.id and c.is_visible == true)
|> select([p, c], {p, c})

Соединение с фрагментами

Для соединения со сложным запросом Ecto поддерживает фрагменты:

Comment
|> join(:inner, [c], p in fragment("SOME COMPLEX QUERY", c.id, ^some_param))

Обратите внимание, что join не обрамляет фрагмент автоматически круглыми скобками, поскольку одни выражения требуют скобок, а другие — нет. Поэтому в таких случаях, как общие табличные выражения, содержимое фрагмента нужно явно заключить в скобки.

Латеральные соединения

Для латеральных соединений требуется подзапрос, ссылающийся на предыдущие привязки. Этого можно добиться с помощью parent_as внутри функции subquery:

Game
|> from(as: :game)
|> join(
  :inner_lateral,
  [],
  subquery(
    GamesSold
    |> where([gs], gs.game_id == parent_as(:game).id)
    |> order_by([gs], gs.sold_on)
    |> limit(2)
  ),
  on: true
)
|> select([g, gs], {g.name, gs.sold_on})

Подсказки

join также поддерживает подсказки для таблиц, используемые в таких базах данных, как MySQL, MSSQL и Clickhouse.

Например, разработчик, использующий MySQL, может написать:

from p in Post,
  join: c in Comment,
  hints: ["USE INDEX FOO", "USE INDEX BAR"],
  where: p.id == c.post_id,
  select: c

Помните, что подсказки следует использовать редко, и не забывайте ознакомиться с оговорками базы данных, касающимися этой функциональности.

Если подсказки для соединения задаются строкой или списком строк, они должны быть статическими строками, известными во время компиляции.

last(queryable, order_by \\ nil)Исходный код

Ограничивает запрос так, чтобы он возвращал последний результат, упорядоченный по первичному ключу.

Порядок сортировки запроса будет автоматически обращён: столбцы ASC станут столбцами DESC (и наоборот), а лимит будет установлен в 1. Если порядок сортировки не задан, запрос будет автоматически упорядочен по первичному ключу по убыванию.

Примеры

Post |> last |> Repo.one
query |> last(:inserted_at) |> Repo.one

limit(query, binding \\ [], expr)Исходный код

Выражение ограничения количества результатов запроса.

Ограничивает количество строк, возвращаемых результатом. Может быть любым выражением, но должно вычисляться в целочисленное значение и не может содержать поля.

Если limit задано дважды, оно заменяет предыдущее значение.

Пример с ключевыми словами

from(u in User, where: u.id == ^current_user, limit: 1)

Пример выражения

User |> where([u], u.id == ^current_user) |> limit(1)

lock(query, binding \\ [], expr)Исходный код

Выражение блокировки запроса.

Предоставляет поддержку пессимистической блокировки на уровне строк с помощью SELECT ... FOR UPDATE или других специфичных для базы данных условий блокировки. expr может быть любым выражением, но должно вычисляться в логическое значение или строку и не может содержать поля.

Если lock используется несколько раз, действует последнее указанное значение.

Ecto также поддерживает оптимистическую блокировку, но не с помощью запросов. Подробнее об оптимистической блокировке см. в описании функции Ecto.Changeset.optimistic_lock/3.

Пример с ключевыми словами

from(u in User, where: u.id == ^current_user, lock: "FOR SHARE NOWAIT")

Пример выражения

User |> where([u], u.id == ^current_user) |> lock("FOR SHARE NOWAIT")

offset(query, binding \\ [], expr)Исходный код

Выражение смещения выборки запроса.

Задаёт смещение для количества строк, выбираемых из результата. Может быть любым выражением, но должно вычисляться в целочисленное значение и не может содержать поля.

Если offset задано дважды, оно заменяет предыдущее значение.

Пример с ключевыми словами

# Get all posts on page 4
from(p in Post, limit: 10, offset: 30)

Пример выражения

Post |> limit(10) |> offset(30)

or_having(query, binding \\ [], expr)Исходный код

Выражение запроса OR having.

Как и having, но объединяет предыдущее выражение с помощью OR. or_having работает с having так же, как or_where работает с where.

Пример с ключевыми словами

# Augment a previous group_by with a having condition.
from(p in query, or_having: avg(p.num_comments) > 10)

Пример выражения

# Augment a previous group_by with a having condition.
Post |> or_having([p], avg(p.num_comments) > 10)

or_where(query, binding \\ [], expr)Исходный код

Выражение запроса OR where.

Работает точно так же, как where, но объединяет его с любым предыдущим выражением с помощью OR. Все выражения должны вычисляться в логическое значение.

or_where также принимает список ключевых слов, в котором каждый ключ — это поле, сравниваемое с заданным значением. Каждая пара «ключ-значение» объединяется с помощью AND, точно так же, как в where.

Пример с ключевыми словами

from(c in City, where: [country: "Sweden"], or_where: [country: "Brazil"])

При интерполяции списков ключевых слов их элементы объединяются операторами AND, а полученное выражение соединяется с любым существующим выражением оператором OR:

filters = [country: "USA", name: "New York"]
from(c in City, where: [country: "Sweden"], or_where: ^filters)

что эквивалентно:

from c in City, where: (c.country == "Sweden") or
                       (c.country == "USA" and c.name == "New York")

Такое поведение предусмотрено специально, чтобы изменения между where и or_where были минимальными. Кроме того, если у вас есть список ключевых слов и вы хотите объединить каждую пару с помощью or, это легко сделать с помощью Enum.reduce/3:

filters = [country: "USA", is_tax_exempt: true]
Enum.reduce(filters, City, fn {key, value}, query ->
  from q in query, or_where: field(q, ^key) == ^value
end)

что будет эквивалентно:

from c in City, or_where: (c.country == "USA"), or_where: c.is_tax_exempt == true

Пример выражения

City |> where([c], c.country == "Sweden") |> or_where([c], c.country == "Brazil")

order_by(query, binding \\ [], expr)Исходный код

Выражение сортировки запроса.

Сортирует поля по одному или нескольким полям. Принимает одно поле или список полей. Направление по умолчанию — по возрастанию (:asc); его можно настроить в списке ключевых слов, указав один из следующих вариантов:

  • :asc
  • :asc_nulls_last
  • :asc_nulls_first
  • :desc
  • :desc_nulls_last
  • :desc_nulls_first

Варианты *_nulls_first и *_nulls_last поддерживаются не всеми базами данных. Все базы данных по умолчанию сортируют по возрастанию, однако выбор между «NULL в начале» и «NULL в конце» зависит от реализации каждой базы данных.

В запросе можно вызывать или перечислять order_by несколько раз. Новые выражения добавляются к существующим.

order_by также принимает список атомов, каждый из которых обозначает поле источника, или список ключевых слов, где ключом является направление сортировки, а значением — поле.

Примеры с ключевыми словами

from(c in City, order_by: c.name, order_by: c.population)
from(c in City, order_by: [c.name, c.population])
from(c in City, order_by: [asc: c.name, desc: c.population])

from(c in City, order_by: [:name, :population])
from(c in City, order_by: [asc: :name, desc_nulls_first: :population])

Список ключевых слов также можно интерполировать:

values = [asc: :name, desc_nulls_first: :population]
from(c in City, order_by: ^values)

Также можно использовать фрагмент:

from c in City, order_by: [
  # A deterministic shuffled order
  fragment("? % ? DESC", c.id, ^modulus),
  desc: c.id,
]

Также можно сортировать по псевдониму столбца или вычисляемому столбцу:

from(c in City,
  select: %{
    name: c.name,
    total_population:
      fragment(
        "COALESCE(?, ?) + ? AS total_population",
        c.animal_population,
        0,
        c.human_population
      )
  },
  order_by: [
    # based on `AS total_population` in the previous fragment
    {:desc, fragment("total_population")}
  ]
)

Примеры выражений

City |> order_by([c], asc: c.name, desc: c.population)
City |> order_by(asc: :name) # Sorts by the cities name
City |> order_by(^order_by_param) # Keyword list

preload(query, bindings \\ [], expr)Исходный код

Предварительно загружает ассоциации в набор результатов.

Предположим, у вас есть схема Post с ассоциацией has_many :comments, и вы выполняете следующий запрос:

Repo.all from p in Post, preload: [:comments]

Пример выше извлечёт из базы данных все публикации, а затем выполнит отдельный запрос, возвращающий все комментарии, связанные с найденными публикациями. Затем комментарии обрабатываются и связываются с каждой возвращённой записью post в поле comments.

Зачастую требуется выбрать и отфильтровать публикации и комментарии в одном запросе. В таких случаях можно явно указать, что существующее соединение нужно предварительно загрузить в набор результатов:

Repo.all from p in Post,
           join: c in assoc(p, :comments),
           where: c.published_at > p.updated_at,
           preload: [comments: c]

В примере выше вместо отдельного запроса для получения комментариев Ecto получит публикации и комментарии одним запросом, а затем отдельным проходом свяжет каждый комментарий с родительской публикацией. Поэтому запрос вернёт не number_of_posts * number_of_comments результатов, как сделал бы join, а только публикации с заполненными полями comments.

Вложенные ассоциации можно предварительно загружать в обоих форматах:

Repo.all from p in Post,
           preload: [:author, comments: :likes]

Repo.all from p in Post,
           join: c in assoc(p, :comments),
           join: l in assoc(c, :likes),
           where: l.inserted_at > c.updated_at,
           preload: [:author, comments: {c, likes: l}]

Выбор между предварительной загрузкой с помощью соединений и отдельными запросами

Выбор между предварительной загрузкой ассоциаций через соединения одним большим запросом (preload: [comments: c]) или отдельными небольшими запросами (preload: [:comments]) зависит от конкретного случая. При выборе учитывайте следующие факторы:

  • Соединения сокращают количество обращений к базе данных: Получение данных одним запросом позволяет уменьшить число обращений к базе данных и потенциально снизить общую задержку.
  • Возможное дублирование данных: При соединениях в наборе результатов могут появиться дублирующиеся данные. Их обработка требует от Ecto дополнительных ресурсов, а передача результатов потребляет больше пропускной способности.
  • Параллельное выполнение отдельных запросов: При выполнении отдельных запросов вне транзакции Ecto может параллельно выполнять запросы предварительной загрузки, что ускоряет операцию в целом.

Как правило, рекомендуется использовать соединения при предварительной загрузке только в том случае, если ассоциации уже соединяются в основном запросе. Например, в последнем запросе раздела выше комментарии и отметки «Нравится» уже соединены, поэтому они включены в предварительную загрузку. Однако автор не соединён в основном запросе, поэтому он загружается отдельным запросом.

Запросы предварительной загрузки

Для предварительной загрузки также можно задавать запросы, что позволяет фильтровать данные или настраивать способ их получения:

comments_query = from c in Comment, order_by: c.published_at
Repo.all from p in Post, preload: [comments: ^comments_query]

Пример выше выполнит два запроса: один для загрузки публикаций, а второй — для загрузки связанных с ними комментариев. Комментарии будут упорядочены по published_at.

При указании запроса предварительной загрузки можно также вкладывать предварительные загрузки. Например, можно загрузить опубликованные записи автора вместе с комментариями к ним следующим образом:

posts_query = from p in Post, where: p.state == :published
Repo.all from a in Author, preload: [posts: ^{posts_query, [:comments]}]

При желании дополнительные предварительные загрузки можно добавить непосредственно в posts_query:

posts_query =
  from p in Post, where: p.state == :published, preload: :related_posts

Примечание: такие операции, как limit и offset в запросе предварительной загрузки, влияют на весь набор результатов, а не на каждую ассоциацию. Например, следующий запрос:

comments_query = from c in Comment, order_by: c.popularity, limit: 5
Repo.all from p in Post, preload: [comments: ^comments_query]

не вернёт самые популярные комментарии для каждой публикации. Вместо этого он вернёт 5 самых популярных комментариев среди всех публикаций. Для решения этой задачи нужно использовать оконную функцию:

ranking_query =
  from c in Comment,
  select: %{id: c.id, row_number: over(row_number(), :posts_partition)},
  windows: [posts_partition: [partition_by: :post_id, order_by: :popularity]]

comments_query =
  from c in Comment,
  join: r in subquery(ranking_query),
  on: c.id == r.id and r.row_number <= 5

Repo.all from p in Post, preload: [comments: ^comments_query]

Для ассоциаций :through, например когда у публикации может быть много авторов комментариев, что записывается как has_many :comments_authors, through: [:comments, :author], запрос, переданный для предварительной загрузки, настраивает связь между комментариями и авторами, даже если загрузка выполняется через публикации. Иными словами, в случае ассоциаций :through запрос, переданный для предварительной загрузки, настраивает последнее соединение в цепочке ассоциаций. Это означает, что предложения order_by в ассоциациях :through влияют только на непосредственную связь между comments и authors, а не между публикациями и комментариями.

Функции предварительной загрузки

Для предварительной загрузки также можно задавать функции. Если арность функции равна 1, она получает только идентификаторы родительской ассоциации. Если арность равна 2, она получает идентификаторы родительской ассоциации первым аргументом, а метаданные ассоциации — вторым. Обе функции должны возвращать связанные данные. Затем Ecto сопоставит эти данные и отсортирует их по ключу связи:

comment_preloader = fn post_ids -> fetch_comments_by_post_ids(post_ids) end
Repo.all from p in Post, preload: [comments: ^comment_preloader]

Это полезно, когда весь набор данных уже загружен или его необходимо явно получить из другого источника. Получаемые функцией предварительной загрузки идентификаторы и возвращаемый результат зависят от типа ассоциации:

  • Для has_many и belongs_to функция получает идентификаторы родительской ассоциации и должна возвращать список карт или структур со связанными записями. Связанная карта или структура должна содержать поле «внешнего ключа». Например, если у публикации много комментариев, при предварительной загрузке комментариев с помощью пользовательской функции ей будет передан список "post_ids", и она должна вернуть карты или структуры, представляющие комментарии. Карты или структуры должны включать поле :post_id

  • Для has_many :through функция работает аналогично обычной has_many, однако следует учитывать, что получаемые идентификаторы относятся к последней ассоциации. Например, у публикации может быть много комментариев, а у каждого комментария — автор. Следовательно, у публикации может быть много авторов комментариев, что записывается как has_many :comments_authors, through: [:comments, :author]. При предварительной загрузке авторов с помощью пользовательской функции через :comments_authors на последнем шаге функция получит идентификаторы авторов

  • Для many_to_many функция получает идентификаторы родительской ассоциации и должна возвращать кортеж, в котором первым элементом является идентификатор родителя, а вторым — карта или структура ассоциации. Например, если у публикации много тегов, при предварительной загрузке тегов с помощью пользовательской функции ей будет передан список "post_ids", и она должна вернуть кортеж в формате {post_id, tag}

Вариант функции с арностью 2 особенно полезен, если вы хотите создать универсальный механизм предварительной загрузки, работающий со всеми ассоциациями. Например, чтобы создать механизм предварительной загрузки для латеральных соединений, который находит самые новые ассоциации, можно сделать следующее:

lateral_preloader = fn ids, assoc -> newest_records(ids, assoc, 5) end

def newest_records(parent_ids, assoc, n) do
  %{related_key: related_key, queryable: queryable} = assoc

  squery =
    from q in queryable,
      where: field(q, ^related_key) == parent_as(:parent_ids).id,
      order_by: {:desc, :created_at},
      limit: ^n

  query =
    from f in fragment("SELECT id from UNNEST(?::int[]) AS id", ^parent_ids), as: :parent_ids,
      inner_lateral_join: s in subquery(squery), on: true,
      select: s

  Repo.all(query)
end

Список доступных метаданных см. в документации модулей типов ассоциаций. Например, см. Ecto.Association.BelongsTo.

Динамическая предварительная загрузка

Ассоциации также можно задавать для предварительной загрузки динамически с помощью макроса dynamic:

  preloads = [comments: dynamic([comments: c], c)]

  Repo.all from p in Post,
             join: c in assoc(p, :comments),
             as: :comments,
             where: c.published_at > p.updated_at,
             preload: ^preloads

Подробнее см. dynamic/2.

Пример с ключевыми словами

# Returns all posts, their associated comments, and the associated
# likes for those comments.
from(p in Post,
  preload: [comments: :likes],
  select: p
)

Примеры выражений

Post |> preload(:comments) |> select([p], p)

Post
|> join(:left, [p], c in assoc(p, :comments))
|> preload([p, c], [:user, comments: c])
|> select([p], p)

prepend_order_by(query, binding \\ [], expr)Исходный код

Выражение сортировки запроса, добавляемое перед существующими выражениями.

Принимает те же входные данные, что и order_by/3, за исключением того, что выражение будет стоять перед всеми ранее заданными выражениями сортировки. Работает только с синтаксисом запросов на основе макросов, но не с синтаксисом на основе ключевых слов.

Например, следующий код создаст запрос, который сначала сортирует по human_population, а затем по name:

City |> order_by([c], c.name) |> prepend_order_by([c], c.human_population)

Соответствующий синтаксис на основе ключевых слов вызовет ошибку:

from c in City, order_by: c.name, prepend_order_by: c.human_population

put_query_prefix(query, prefix)Исходный код

Задаёт указанный префикс для запроса.

recursive_ctes(query, value)Исходный код

Включает или отключает рекурсивный режим для CTE.

Согласно стандарту SQL это влияет на все CTE в запросе, а не на отдельные CTE.

Пример построения запроса с рекурсивным CTE см. в разделе with_cte/3.

reverse_order(query)Исходный код

Обращает порядок сортировки запроса.

Столбцы ASC становятся столбцами DESC (и наоборот). Если в запросе нет order_by, сортировка выполняется по первичному ключу в обратном порядке.

Примеры

query |> reverse_order() |> Repo.one()
Post |> order_by(asc: :id) |> reverse_order() == Post |> order_by(desc: :id)

select(query, binding \\ [], expr)Исходный код

Выражение выборки запроса.

Задаёт поля, выбираемые из схемы, и преобразования, которые нужно выполнить над этими полями. В качестве поля выборки можно использовать любое выражение, допустимое в запросе.

Как показано в примерах ниже, каждое выражение выборки можно заключить в список, кортеж или карту. Также можно выбрать всю схему.

В запросе может быть только одно выражение выборки; если оно не задано, по умолчанию выбирается вся схема. Если select задано более одного раза, возникает ошибка. Используйте exclude/2, если нужно удалить предыдущую выборку перед её заменой, или ознакомьтесь с select_merge/3 — ограниченным вариантом select, который можно компоновать и вызывать несколько раз.

select также принимает список атомов, каждый из которых обозначает выбираемое поле источника.

Примеры с ключевыми словами

from(c in City, select: c) # returns the schema as a struct
from(c in City, select: {c.name, c.population})
from(c in City, select: [c.name, c.county])
from(c in City, select: %{n: c.name, answer: 42})
from(c in City, select: %{c | alternative_name: c.name})
from(c in City, select: %Data{name: c.name})

Также можно выбрать структуру и одновременно ограничить возвращаемые поля:

from(City, select: [:name])

Приведённый выше синтаксис эквивалентен:

from(city in City, select: struct(city, [:name]))

Также можно написать:

from(city in City, select: map(city, [:name]))

Если требуется вернуть карту только с выбранными полями.

Чтобы выбрать структуру, исключив только указанные поля, можно заменить их на nil или другое значение по умолчанию:

from(city in City, select: %{city | geojson: nil, text: "<redacted>"})

Дополнительную информацию см. в документации для Ecto.Query.API.struct/2 и Ecto.Query.API.map/2.

Примеры выражений

City |> select([c], c)
City |> select([c], {c.name, c.country})
City |> select([c], %{"name" => c.name})
City |> select([:name])
City |> select([c], struct(c, [:name]))
City |> select([c], map(c, [:name]))
City |> select([c], %{c | geojson: nil, text: "<redacted>"})

Динамические части

Динамические выражения могут быть частью select в качестве значений карты, которые необходимо интерполировать на корневом уровне:

period = if monthly?, do: dynamic([p], p.month), else: dynamic([p], p.date)
metric = if distance?, do: dynamic([p], p.distance), else: dynamic([p], p.time)

from(c in City, select: ^%{period: period, metric: metric})

select_merge(query, binding \\ [], expr)Исходный код

Объединяемое выражение выборки запроса.

Этот макрос похож на select/3, но его можно указывать несколько раз при условии, что каждая запись является картой. Это полезно для объединения и композиции выборок. Например:

query = from p in Post, select: %{}

query =
  if include_title? do
    from p in query, select_merge: %{title: p.title}
  else
    query
  end

query =
  if include_visits? do
    from p in query, select_merge: %{visits: p.visits}
  else
    query
  end

В примере выше запрос создаётся постепенно, путём объединения данных в итоговую карту. Если оба условия выше истинны, итоговый запрос будет эквивалентен:

from p in Post, select: %{title: p.title, visits: p.visits}

Если вызывается :select_merge и ранее ничего не было выбрано, по умолчанию будет использоваться источник — в примере выше p.

Аргумент, переданный в :select_merge, всегда должен быть картой. Значение, с которым выполняется объединение, должно быть структурой или картой. Если это структура, объединяемые позднее поля должны входить в её состав, иначе возникнет ошибка.

Если аргумент :select_merge — созданная структура (Ecto.Query.API.struct/2) или карта (Ecto.Query.API.map/2), в которой источником для структуры или карты может быть значение nil (как при внешнем соединении), источник возвращается без изменений.

query =
  Post
  |> join(:left, [p], t in Post.Translation,
    on: t.post_id == p.id and t.locale == ^"en"
  )
  |> select_merge([_p, t], map(t, ^~w(title summary)a))

Если для публикации нет перевода на английский, будет возвращена публикация без перевода title, а summary будет равен nil. Если перевод есть, и title, и summary будут содержать значение из Post.Translation.

С помощью select_merge нельзя задавать поля в ассоциациях, поскольку ассоциации всегда загружаются позднее и заменяют любые ранее заданные значения.

Динамические выражения могут быть частью select_merge в качестве значений карты, которые необходимо интерполировать на корневом уровне. Применяются описанные выше правила объединения. Это позволяет объединять динамические значения с ранее выбранными картами и структурами.

subquery(query, opts \\ [])Исходный код

Преобразует запрос в подзапрос.

Если передан подзапрос, возвращает его без изменений. Если передано любое другое значение, оно преобразуется в запрос с помощью Ecto.Queryable и оборачивается в структуру Ecto.SubQuery.

subquery поддерживается в следующих выражениях:

  • from,
  • join,
  • where в форме p.x in subquery(q),
  • select и select_merge в форме %{field: subquery(...)}.

Примеры

# Get the average salary of the top 10 highest salaries
query = from Employee, order_by: [desc: :salary], limit: 10
from e in subquery(query), select: avg(e.salary)

Для подзапроса можно указать префикс, как и для стандартных операций репозитория:

query = from Employee, order_by: [desc: :salary], limit: 10
from e in subquery(query, prefix: "my_prefix"), select: avg(e.salary)

Подзапрос также можно использовать в выражении join.

UPDATE posts
  SET sync_started_at = $1
  WHERE id IN (
    SELECT id FROM posts
      WHERE synced = false AND (sync_started_at IS NULL OR sync_started_at < $1)
      LIMIT $2
  )

Это можно записать как выражение соединения:

subset = from(p in Post,
  where: p.synced == false and
           (is_nil(p.sync_started_at) or p.sync_started_at < ^min_sync_started_at),
  limit: ^batch_size
)

Repo.update_all(
  from(p in Post, join: s in subquery(subset), on: s.id == p.id),
  set: [sync_started_at: NaiveDateTime.utc_now()]
)

Или как условие where:

subset_ids = from(p in subset, select: p.id)
Repo.update_all(
  from(p in Post, where: p.id in subquery(subset_ids)),
  set: [sync_started_at: NaiveDateTime.utc_now()]
)

Если нужно сослаться на родительскую привязку, которая неизвестна при написании подзапроса, можно использовать parent_as, как показано в примерах раздела «Именованные привязки» в документации этого модуля.

Подзапрос также можно напрямую использовать в select и select_merge:

comments_count = from(c in Comment, where: c.post_id == parent_as(:post).id, select: count())
from(p in Post, as: :post, select: %{id: p.id, comments: subquery(comments_count)})

union(query, other_query)Источник

Выражение запроса объединения.

Объединяет наборы результатов нескольких запросов. select каждого запроса должны быть абсолютно одинаковыми, с одинаковыми типами в одинаковом порядке.

Выражение union возвращает только уникальные строки, как если бы каждый запрос возвращал уникальные результаты. Это может привести к снижению производительности. Если вам нужно объединить несколько наборов результатов, не удаляя повторяющиеся строки, рассмотрите возможность использования union_all/2.

Поведение объединения

Необходимо учитывать несколько особенностей поведения запросов с объединением, иначе можно неожиданно получить неверный результат запроса.

Сортировка, лимит и смещение

Выражения order_by, limit и offset родительского запроса применяются к результату всего объединения. order_by необходимо указать одним из следующих способов, поскольку объединение двух или более запросов не получает псевдоним автоматически:

  • Используйте Ecto.Query.API.fragment/1, чтобы передать оператор order_by, который напрямую обращается к полям объединения.
  • Оберните объединение в подзапрос и обратитесь к привязке подзапроса.

Порядок выбора столбцов

Столбцы каждого запроса в объединении должны быть указаны в одном и том же порядке. В противном случае значения одного столбца могут оказаться в другом. Это относится ко всем типам выражений select, включая карты.

Например, в следующем запросе значения имени и города поставщика будут переставлены местами, поскольку именно в таком порядке поля указаны в запросе клиента.

supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
customer_query = from c in Customer, select: %{name: c.name, city: c.city}
union(supplier_query, ^customer_query)

Выбор атомов-литералов

При выборе атома-литерала его значение должно быть одинаковым во всех запросах. В противном случае значение из родительского запроса будет применено ко всем остальным запросам. Это также относится к выбору карт с атомами в качестве ключей.

Примеры с ключевыми словами

# Unordered result
supplier_query = from s in Supplier, select: s.city
from c in Customer, select: c.city, union: ^supplier_query

# Ordered result
supplier_query = from s in Supplier, select: s.city
union_query = from c in Customer, select: c.city, union: ^supplier_query
from s in subquery(union_query), order_by: s.city

Примеры с выражениями

# Unordered result
supplier_query = Supplier |> select([s], s.city)
Customer |> select([c], c.city) |> union(^supplier_query)

# Ordered result
customer_query = Customer |> select([c], c.city) |> order_by(fragment("city"))
supplier_query = Supplier |> select([s], s.city)
union(customer_query, ^supplier_query)

union_all(query, other_query)Источник

Выражение запроса union all.

Объединяет наборы результатов нескольких запросов. select каждого запроса должны быть абсолютно одинаковыми, с одинаковыми типами в одинаковом порядке.

Поведение объединения

Необходимо учитывать несколько особенностей поведения запросов с объединением, иначе можно неожиданно получить неверный результат запроса.

Сортировка, лимит и смещение

Выражения order_by, limit и offset родительского запроса применяются к результату всего объединения. order_by необходимо указать одним из следующих способов, поскольку объединение двух или более запросов не получает псевдоним автоматически:

  • Используйте Ecto.Query.API.fragment/1, чтобы передать оператор order_by, который напрямую обращается к полям объединения.
  • Оберните объединение в подзапрос и обратитесь к привязке подзапроса.

Порядок выбора столбцов

Столбцы каждого запроса в объединении должны быть указаны в одном и том же порядке. В противном случае значения одного столбца могут оказаться в другом. Это относится ко всем типам выражений select, включая карты.

Например, в следующем запросе значения имени и города поставщика будут переставлены местами, поскольку именно в таком порядке поля указаны в запросе клиента.

supplier_query = from s in Supplier, select: %{city: s.city, name: s.name}
customer_query = from c in Customer, select: %{name: c.name, city: c.city}
union_all(supplier_query, ^customer_query)

Выбор атомов-литералов

При выборе атома-литерала его значение должно быть одинаковым во всех запросах. В противном случае значение из родительского запроса будет применено ко всем остальным запросам. Это также относится к выбору карт с атомами в качестве ключей.

Примеры с ключевыми словами

# Unordered result
supplier_query = from s in Supplier, select: s.city
from c in Customer, select: c.city, union_all: ^supplier_query

# Ordered result
supplier_query = from s in Supplier, select: s.city
union_all_query = from c in Customer, select: c.city, union_all: ^supplier_query
from s in subquery(union_all_query), order_by: s.city

Примеры с выражениями

# Unordered result
supplier_query = Supplier |> select([s], s.city)
Customer |> select([c], c.city) |> union_all(^supplier_query)

# Ordered result
customer_query = Customer |> select([c], c.city) |> order_by(fragment("city"))
supplier_query = Supplier |> select([s], s.city)
union_all(customer_query, ^supplier_query)

update(query, binding \\ [], expr)Источник

Выражение запроса обновления.

Обновления используются для изменения отфильтрованных записей. Чтобы применить обновления, необходимо вызвать Ecto.Repo.update_all/3.

Пример с ключевыми словами

from(u in User, update: [set: [name: "new name"]])

Примеры с выражениями

User |> update([u], set: [name: "new name"])
User |> update(set: [name: "new name"])

Интерполяция

new_name = "new name"
from(u in User, update: [set: [name: ^new_name]])

new_name = "new name"
from(u in User, update: [set: [name: fragment("upper(?)", ^new_name)]])

Операторы

Выражение update в Ecto поддерживает следующие операторы:

  • set — устанавливает для указанного поля таблицы заданное значение

    from(u in User, update: [set: [name: "new name"]])
  • inc — увеличивает (или уменьшает, если значение отрицательное) значение указанного поля таблицы на заданную величину

    from(u in User, update: [inc: [accesses: 1]])
  • push — добавляет заданное значение в конец поля-массива

    from(u in User, update: [push: [tags: "cool"]])
  • pull — удаляет заданное значение из поля-массива

    from(u in User, update: [pull: [tags: "not cool"]])

Композиция

Помните, что все выражения запросов допускают композицию, поэтому можно использовать update несколько раз в одном запросе, чтобы объединить выражения обновления:

new_name = "new name"
User
|> update([u], set: [name: fragment("upper(?)", ^new_name)])
|> update([u], set: [age: 42])

Это может быть полезно для составления обновлений из разных функций или при смешивании интерполяции, например set: ^updates, с обычными выражениями запроса, например set: [age: u.age + 1].

where(query, binding \\ [], expr)Источник

Выражение запроса where с оператором AND.

Выражения where используются для фильтрации набора результатов. Если выражений where несколько, они объединяются оператором and. Все выражения where должны вычисляться в логическое значение.

where также принимает список ключевых слов, где поле, указанное в качестве ключа, сравнивается с заданным значением. Поля всегда относятся к источнику, указанному в from.

Пример с ключевыми словами

from(c in City, where: c.country == "Sweden")
from(c in City, where: [country: "Sweden"])

Также можно интерполировать весь список ключевых слов, что позволяет динамически фильтровать источник:

filters = [country: "Sweden"]
from(c in City, where: ^filters)

Примеры с выражениями

City |> where([c], c.country == "Sweden")
City |> where(country: "Sweden")

windows(query, binding \\ [], expr)Источник

Определяет окна, которые можно использовать с Ecto.Query.WindowAPI.

Принимает список ключевых слов, где ключи — это имена окон, а значения — списки ключевых слов с выражениями окна.

Примеры

# Compare each employee's salary with the average salary in his or her department
from e in Employee,
  select: {e.depname, e.empno, e.salary, over(avg(e.salary), :department)},
  windows: [department: [partition_by: e.depname]]

В приведённом выше примере вычисляется средняя зарплата по отделам. :department — это имя окна, данные разделены по e.depname, а avg/1 — функция окна. Дополнительную информацию о функциях окон см. в разделе Ecto.Query.WindowAPI.

Выражения окна

При указании окна разрешены следующие ключи.

:partition_by

Список полей, по которым следует разделить окно, например:

windows: [department: [partition_by: e.depname]]

Для динамического разбиения также можно интерполировать список атомов:

fields = [:depname, :year]
windows: [dynamic_window: [partition_by: ^fields]]

:order_by

Список полей, по которым следует упорядочить окно, например:

windows: [ordered_names: [order_by: e.name]]

Работает точно так же, как вариант запроса с ключевыми словами для order_by/3.

:frame

Фрагмент, определяющий рамку для функций окна.

Примеры

# Compare each employee's salary for each month with his average salary for previous 3 months
from p in Payroll,
  select: {p.empno, p.date, p.salary, over(avg(p.salary), :prev_months)},
  windows: [prev_months: [partition_by: p.empno, order_by: p.date, frame: fragment("ROWS 3 PRECEDING EXCLUDE CURRENT ROW")]]

with_cte(query, name, opts)Источник

Общее табличное выражение (CTE), также известное как выражение WITH.

name должно быть строковым литералом, известным во время компиляции, который используется в качестве имени таблицы для соединения с CTE в основном запросе или рекурсивном CTE.

ВАЖНО! Будьте осторожны при использовании CTE. В необработанном SQL CTE можно использовать для организации запросов, однако в Ecto такой механизм не нужен, поскольку запросы Ecto по определению допускают композицию. Иными словами, если большой запрос нужно разбить на части, используйте все возможности Elixir и этого модуля для структурирования кода. Кроме того, разбиение запроса на CTE может отрицательно сказаться на производительности, поскольку база данных может неэффективно выполнять оптимизацию между CTE. Основной сценарий использования CTE в Ecto — создание рекурсивных определений, которые описаны в следующем разделе. Нерекурсивные CTE часто можно записать в виде соединений или подзапросов, обеспечивающих более высокую производительность.

Параметры

  • :as — сам запрос CTE или фрагмент
  • :materialized — логическое значение, указывающее, следует ли материализовать CTE. Если не задано, используется значение по умолчанию для базы данных (поддерживается только Postgrex среди встроенных адаптеров)
  • :operation — одно из значений :all, :update_all или :delete_all, указывающее тип операции запроса CTE. Если не задано, используется :all, и запрос CTE становится запросом SELECT. (поддерживается только встроенным адаптером Postgres)

Рекурсивные CTE

Используйте recursive_ctes/2, чтобы включить рекурсивный режим для CTE.

В самом запросе CTE используйте то же имя таблицы, чтобы задействовать рекурсию, передав его в аргумент name. Обязательно задайте условие остановки, чтобы избежать бесконечного цикла рекурсии. Как правило, CTE в Ecto следует использовать только для написания рекурсивных запросов.

Примеры выражений

Товары и названия их категорий для хлебных крошек:

category_tree_initial_query =
  Category
  |> where([c], is_nil(c.parent_id))

category_tree_recursion_query =
  Category
  |> join(:inner, [c], ct in "category_tree", on: c.parent_id == ct.id)

category_tree_query =
  category_tree_initial_query
  |> union_all(^category_tree_recursion_query)

Product
|> recursive_ctes(true)
|> with_cte("category_tree", as: ^category_tree_query)
|> join(:left, [p], c in "category_tree", on: c.id == p.category_id)
|> group_by([p], p.id)
|> select([p, c], %{p | category_names: fragment("ARRAY_AGG(?)", c.name)})

Также можно передать фрагмент необработанного SQL:

@raw_sql_category_tree """
SELECT * FROM categories WHERE c.parent_id IS NULL
UNION ALL
SELECT * FROM categories AS c, category_tree AS ct WHERE ct.id = c.parent_id
"""

Product
|> recursive_ctes(true)
|> with_cte("category_tree", as: fragment(@raw_sql_category_tree))
|> join(:inner, [p], c in "category_tree", on: c.id == p.category_id)

Также можно выполнять запросы непосредственно к таблице CTE. В таких случаях можно передать Ecto.Queryable — кортеж модуля, в котором первым элементом является имя таблицы CTE, а вторым — схема Ecto. Это преобразует строки результата в структуры Ecto, если схема Ecto сопоставляет те же поля, что и таблица CTE:

{"category_tree", Category}
|> recursive_ctes(true)
|> with_cte("category_tree", as: ^category_tree_query)
|> join(:left, [c], p in assoc(c, :products))
|> group_by([c], c.id)
|> select([c, p], %{c | products_count: count(p.id)})

Учтите, что в результирующих структурах имя исходной таблицы будет заменено на "category_tree"; они также унаследуют все остальные свойства схемы Category, в том числе @schema_prefix, если оно задано.

В таких случаях эти свойства можно отключить, задав соответствующие параметры:

from(cte in {"category_tree", Category}, prefix: nil)
|> recursive_ctes(true)
|> with_cte("category_tree", as: ^category_tree_query)

или соединить результат CTE с исходной схемой:

Category
|> recursive_ctes(true)
|> with_cte("category_tree", as: ^category_tree_query)
|> join(:inner, [c], tree in "category_tree", on: c.id == tree.id)

Хотя это требует дополнительного соединения, такой подход позволяет использовать структуры в дальнейших операциях изменения данных по всему приложению без необходимости вручную сбрасывать имя исходной таблицы.

Для встроенного адаптера Postgres можно определять CTE-запросы, изменяющие данные:

update_categories_query =
  Category
  |> where([c], is_nil(c.parent_id))
  |> update([c], set: [name: "Root category"])
  |> select([c], c)

{"update_categories", Category}
|> with_cte("update_categories", as: ^update_categories_query, operation: :update_all)
|> select([c], c)

Примечание. Чтобы получить обновлённые строки из запроса CTE, родительский запрос должен выбирать строки из таблицы CTE, а не из таблицы, на которую ссылается запрос CTE. Например, "update_categories" вернёт обновлённые строки для таблицы "category", а выборка непосредственно из таблицы "category" вернёт неизменённые строки. Подробнее см. в документации Postgres о CTE, изменяющих данные, и их взаимодействии со снимками.

Синтаксис с ключевыми словами для этой функции не поддерживается.

Ограничение: CTE для схем с полями источника

Ecto позволяет разработчикам указывать, что таблица в схеме Ecto соответствует другому столбцу в базе данных:

field :group_id, :integer, source: :iGroupId

В настоящее время использование схемы с полями источника в CTE может привести к созданию некорректных запросов. Если вы столкнулись с такой ситуацией, лучше всего использовать фрагмент в качестве CTE.

with_named_binding(query, key, fun)Источник

Применяет функцию обратного вызова к запросу, если он не содержит указанную именованную привязку. В противном случае возвращает исходный запрос.

Функция обратного вызова должна принимать queryable и возвращать структуру Ecto.Query, содержащую предоставленную именованную привязку, иначе будет вызвана ошибка. Она также может принимать второй аргумент — атом, представляющий имя привязки.

Например, эту функцию можно использовать для удобного условного добавления нового именованного соединения к запросу:

if has_named_binding?(query, :comments) do
  query
else
  join(query, :left, [p], c in assoc(p, :comments), as: :comments)
end

С помощью этой функции это можно упростить до:

with_named_binding(query, :comments, fn query, binding ->
  join(query, :left, [p], a in assoc(p, ^binding), as: ^binding)
end)

Дополнительную информацию об именованных привязках см. в разделе «Именованные привязки» документации этого модуля или в описании has_named_binding?/2.

with_ties(query, binding \\ [], expr)Источник

Включает или отключает учёт совпадающих значений для выражений лимита.

Если несколько записей занимают последнее место в отсортированном результате с ограничением, установка этого значения в true вернёт все записи с совпадающими значениями, даже если итоговый результат превысит заданный лимит.

Значение должно быть логическим или вычисляться в логическое значение во время выполнения. Можно применять только к запросам с выражением limit, иначе будет вызвана ошибка. Если limit переопределено, необходимо повторно применить with_ties.

Не все базы данных поддерживают этот параметр, а те, которые поддерживают, могут указывать его в команде FETCH. Для вычисления совпадающих значений базам данных может потребоваться соответствующий оператор order_by.

Пример с ключевыми словами

from(p in Post, where: p.author_id == ^current_user, order_by: [desc: p.visits], limit: 10, with_ties: true)

Пример с выражениями

Post |> where([p], p.author_id == ^current_user) |> order_by([p], desc: p.visits) |> limit(10) |> with_ties(true)

Пакет Hex Предпросмотр Hex Просмотреть llms.txt Скачать версию ePub

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

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.Query.html

Spec-Zone.ru

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