В этой статье мы рассмотрим основы запросов с помощью Ecto, предметно-ориентированного языка (DSL) для написания запросов и взаимодействия с базами данных в Elixir. Это будет связано с прохождением объединений, ассоциаций, функций агрегации и так далее.
Предполагается базовое знание эликсира , и знание основ Ecto заранее также поможет.
Ectoing Application
Все примеры из этой серии статей могут быть использованы для моего демонстрационного приложения Ectoing. Я настоятельно рекомендую вам настроить это (как описано ниже) и выполнить все запросы, как вы читаете. Это поможет укрепить ваше понимание, поиграв с примерами, чтобы увидеть, что работает, а что нет.
Давайте быстро настроим приложение:
git clone https://github.com/tpunt/ectoing cd ectoing mix deps.get # don't forget to update the credentials in config/config.exs mix ecto.create mix ecto.migrate # populate the database with some dummy data mix run priv/repo/seeds.exs
(Я решил использовать MySQL для этого. Примеры в этой статье должны работать одинаково для всех поддерживаемых баз данных, поэтому, хотя зависимость Mariaex можно переключить на использование другой базы данных, я бы посоветовал против этого. следующая статья будет содержать MySQL-зависимый код.)
Структура базы данных выглядит следующим образом:
Основные запросы
Давайте начнем с некоторых основных запросов, чтобы понять, как запрашивает DSL Ecto.
Обратите внимание, что хотя все примеры могут быть выполнены в оболочке Elixir (через iex -S mix
в базовом каталоге ectoing
), модуль Ecto.Query
должен быть импортирован первым. Это сделает все запрашивающие макросы DSL (например, from
) доступными нам во время работы в оболочке.
Давайте начнем с самых тривиальных запросов — выбора всех пользователей с их полными записями:
SELECT * FROM users;
query = Ectoing.User Ectoing.Repo.all query
(Во всех примерах сначала будет показан синтаксис SQL, а затем его преобразование в синтаксис Ecto-запроса.)
Чтобы получить полные записи для всех пользователей, мы просто выполняем запрос по нужной модели (в данном случае это Ectoing.User
). Это работает, потому что Ecto по умолчанию вернет все поля, определенные в определении схемы соответствующей модели, если предложение select
опущено. Repo.all/2
затем используется для выполнения запроса, где она получает список результатов (поскольку мы ожидаем более одного результата от запроса). Давайте кратко рассмотрим одну из этих возвращенных записей:
[%Ectoing.User{__meta__: #Ecto.Schema.Metadata<:loaded>, firstname: "Thomas", friends_of: #Ecto.Association.NotLoaded<association :friends_of is not loaded>, friends_with: #Ecto.Association.NotLoaded<association :friends_with is not loaded>, id: 1, inserted_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, messages: #Ecto.Association.NotLoaded<association :messages is not loaded>, surname: "Punt", updated_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, username: "tpunt"}, ... ]
Была возвращена структура типа Ectoing.User
, где члены структуры соответствуют именам полей для модели Ectoing.User
. Примечательно, что у нас также есть не загруженные ассоциации ( friend
, friends
и messages
), которые были встроены в них. Позже мы рассмотрим ассоциации Ecto более подробно, но сейчас просто отметим, что они являются решением Ecto для управления загрузкой отношений внешних ключей между моделями.
Впрочем, довольно часто мы не хотим получать всю запись. Итак, давайте просто выберем имя и фамилию пользователя:
SELECT firstname, surname FROM users;
query = from u in Ectoing.User, select: [u.firstname, u.surname] Ectoing.Repo.all query
В макросе from
мы указываем модель ( Ectoing.User
) для выбора данных и используем Ectoing.User
переменную ( u
) для ссылки на нее. Предложение select
затем используется для выбора столбцов u.firstname
и u.surname
в качестве списка, делая результирующее значение списком из двух элементов:
[["Thomas", "Punt"], ["Liam", "Mann"], ["John", "Doe"], ["Jane", "Doe"], ["Bruno", "Škvorc"]]
Предложение select
позволяет нам сопоставлять шаблоны внутри него, поэтому, хотя квадратные скобки использовались выше для возврата столбцов в виде списка, мы могли бы довольно легко вместо этого возвратить список кортежей или список карт:
query = from u in Ectoing.User, select: {u.firstname, u.surname} Ectoing.Repo.all query # result [{"Thomas", "Punt"}, {"Liam", "Mann"}, {"John", "Doe"}, {"Jane", "Doe"}, {"Bruno", "Škvorc"}] query = from u in Ectoing.User, select: %{firstname: u.firstname, surname: u.surname} Ectoing.Repo.all query # result [%{firstname: "Thomas", surname: "Punt"}, %{firstname: "Liam", surname: "Mann"}, %{firstname: "John", surname: "Doe"}, %{firstname: "Jane", surname: "Doe"}, %{firstname: "Bruno", surname: "Škvorc"}]
Запросы стилей API
До сих пор мы использовали синтаксис запроса ключевых слов для наших запросов. Как правило, это наиболее распространенный синтаксис, но иногда вы увидите альтернативный синтаксис API запросов Ecto: синтаксис макросов. Давайте рассмотрим этот альтернативный синтаксис, переведя приведенный выше запрос, чтобы выбрать имена и фамилии всех пользователей:
query = (Ectoing.User |> select([u], [u.firstname, u.surname])) Ectoing.Repo.all query
(Скобки, инкапсулирующие весь запрос, не нужны, но были включены, потому что они позволяют легко копировать код и вставлять его непосредственно в IEx.)
На этот раз мы передаем модель в качестве первого аргумента в select/3
, где второй аргумент указывает переменную привязки для модели (в данном случае это u
). Третий аргумент выбирает столбцы, которые (снова) могут быть сопоставлены с шаблоном для возврата кортежей или карт вместо списков.
Чтобы мы могли привыкнуть к обоим стилям синтаксиса, теперь я буду демонстрировать оба вместе с кодом SQL для каждого запроса.
Ограничительный запрос и настройка результирующих наборов
Почти всегда мы хотим выбрать только подмножество записей из базы данных. Это может быть сделано с помощью ряда функций, которые очень похожи на их аналоги в SQL. К ним относятся where
(в сочетании с операторами сравнения in
и тому like
(есть или ilike
, хотя в Ecto 2.0 это устарело)), limit
, offset
и distinct
.
Например, мы можем выбрать всех пользователей с фамилиями, равными «doe», с помощью следующего:
SELECT * FROM users WHERE surname = "doe";
surname = "doe" # Keywords query syntax query = from u in Ectoing.User, where: u.surname == ^surname # Macro syntax query = (Ectoing.User |> where([u], u.surname == ^surname)) Ectoing.Repo.all query
Приведенное выше сравнение не учитывает регистр и возвращает как пользователей John Doe, так и Jane Doe. Обратите внимание на использование оператора pin для переменной surname
: это делается для (явного) интерполяции переменных в запросе. Для таких интерполированных переменных их значения автоматически приводятся к базовому типу столбца, определенному в определении схемы модели.
Давайте попробуем что-нибудь более сложное, выбрав все разные фамилии, упорядочив их, а затем ограничив набор результатов:
SELECT DISTINCT surname FROM users LIMIT 3 ORDER BY surname;
# Keywords query syntax query = from u in Ectoing.User, select: u.surname, distinct: true, limit: 3, order_by: u.surname # Macro syntax query = (Ectoing.User |> select([u], u.surname) |> distinct(true) |> limit(3) |> order_by([u], u.surname)) Ectoing.Repo.all query # ["Doe", "Mann", "Punt"]
distinct/3
функция distinct/3
приведенная выше, будет выбирать различные значения в соответствии со столбцами, указанными в функции select/3
. distinct/3
также можно напрямую передавать столбцы (т. е distinct: u.surname
), что позволяет определенным образом выбирать определенные столбцы, а затем возвращать альтернативные столбцы (с помощью команды « select/3
). Так как MySQL не поддерживает синтаксис DISTINCT ON
, мы не можем сделать это при использовании адаптера MySQL (например, адаптер Postgres позволяет). Окончательный order_by/3
затем упорядочивает набор результатов (в порядке возрастания по умолчанию) в соответствии с переданными столбцами.
Полный список операторов и функций, поддерживаемых в API запросов Ecto, можно найти в его документации . Кроме того, полный список литералов, которые можно использовать в запросах Ecto, см. В документации по выражениям запросов .
Запросы агрегации
Ecto предоставляет нам ряд функций агрегирования, которые мы обычно встречаем в SQL, в том числе: group_by
, having
, count
, avg
, sum
, min
и max
.
Давайте попробуем пару из них, выбрав пользователей, которые имеют средний рейтинг 4 или выше от своих друзей:
SELECT friend_id, avg(friend_rating) AS avg_rating FROM friends GROUP BY friend_id HAVING avg_rating >= 4 ORDER BY avg_rating DESC;
# Keywords query syntax query = from f in Ectoing.Friend, select: %{friend_id: f.friend_id, avg_rating: avg(f.friend_rating)}, group_by: f.friend_id, having: avg(f.friend_rating) >= 4, order_by: [desc: avg(f.friend_rating)] # Macro syntax query = (Ectoing.Friend |> select([f], %{friend_id: f.friend_id, avg_rating: avg(f.friend_rating)}) |> group_by([f], f.friend_id) |> having([f], avg(f.friend_rating) >= 4) |> order_by([f], [desc: avg(f.friend_rating)])) Ectoing.Repo.all query # [%{avg_rating: #Decimal<4.0000>, friend_id: 3}, # %{avg_rating: #Decimal<4.0000>, friend_id: 5}]
Это немного менее элегантно с точки зрения Ecto, поскольку мы не можем использовать псевдоним столбца среднего рейтинга, что требует от нас одного и того же вычисления столбца в запросе три раза. Но, как и в предыдущих примерах, запросы DSL в Ecto очень близко соответствуют исходному SQL, что делает его довольно простым в использовании.
Вывод
В этой статье мы рассмотрели абсолютные основы запросов DSL к Ecto и продемонстрировали, насколько тесно он сопоставляется с необработанным SQL. В следующей статье Ecto Querying DSL от Elixir: помимо основ мы расскажем о более сложных темах, в том числе об объединениях, составных запросах, внедрении фрагментов SQL, загрузочных ассоциациях и префиксах запросов.