Статьи

Ecto Querying DSL от Elixir: не только основы

эликсир логотип

Эта статья основана на основах Ecto, о которых я рассказывал в статье « Понимание Ecto Querying DSL от Elixir: основы» . Теперь я рассмотрю более продвинутые функции Ecto, включая составление запросов, объединения и ассоциации, внедрение фрагментов SQL, явное приведение и динамический доступ к полям.

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

Состав запроса

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

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

SELECT id, username FROM users; SELECT id, username FROM users WHERE username LIKE "%tp%"; SELECT id, username FROM users WHERE username LIKE "%tp%" LIMIT 10, 0; 
 offset = 0 username = "%tp%" # Keywords query syntax get_users_overview = from u in Ectoing.User, select: [u.id, u.username] search_by_username = from u in get_users_overview, where: like(u.username, ^username) paginate_query = from search_by_username, limit: 10, offset: ^offset # Macro syntax get_users_overview = (Ectoing.User |> select([u], [u.id, u.username])) search_by_username = (get_users_overview |> where([u], like(u.username, ^username))) paginate_query = (search_by_username |> limit(10) |> offset(^offset)) Ectoing.Repo.all paginate_query 

Версия SQL довольно повторяющаяся, но версия Ecto, с другой стороны, довольно СУХАЯ. Первый запрос ( get_users_overview ) — это просто общий запрос для получения основной пользовательской информации. Второй запрос ( search_by_username ) строит первый, фильтруя имена пользователей в соответствии с некоторыми именами пользователей, которые мы ищем. Третий запрос ( paginate_query ) строится из второго, где он ограничивает результаты и извлекает их из определенного смещения (чтобы обеспечить основу для разбивки на страницы).

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

Объединения и ассоциации

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

Проще говоря, ассоциации позволяют разработчикам обрабатывать отношения таблиц (реализованные как внешние ключи) в моделях. Они определяются в схемах для каждой модели с использованием has_one/3 и has_many/3 (для моделей, содержащих другие модели) и belongs_to/3 (для моделей, которые не входят в состав других моделей — тех, которые имеют внешние ключи) ,

Глядя на наше приложение Ectoing, мы можем увидеть один пример связи между моделью Ectoing.Message моделью Ectoing.Message . Схема, определенная в Ectoing.User определяет следующую связь:

 has_many :messages, Ectoing.Message 

Мы видим, что у одного пользователя есть много сообщений ( Ectoing.Message ), и мы называем эту ассоциацию :messages .

В модели Ectoing.Message мы определяем следующие отношения ассоциации:

 belongs_to :user, Ectoing.User 

Здесь мы говорим, что модель Ectoing.Message принадлежит модели Ectoing.User . Мы также назвали ассоциацию как :user . По умолчанию Ecto добавит belongs_to к belongs_to ассоциации belongs_to и использует его в качестве имени внешнего ключа (поэтому здесь это будет :user_id ). Это поведение по умолчанию можно изменить, указав имя внешнего ключа вручную, указав параметр foreign_key . Например:

 # Ectoing.Message belongs_to :user, Ectoing.User, foreign_key: some_other_fk_name 

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

 SELECT * FROM users u INNER JOIN messages m ON u.id = m.user_id WHERE u.id = 4; 
 # Keywords query syntax query = from u in Ectoing.User, join: m in Ectoing.Message, on: u.id == m.user_id, where: u.id == 4 # Macro syntax query = (Ectoing.User |> join(:inner, [u], m in Ectoing.Message, u.id == m.user_id) |> where([u], u.id == 4)) Ectoing.Repo.all query 

Возвращаемое значение:

 [%Ectoing.User{__meta__: #Ecto.Schema.Metadata<:loaded>, firstname: "Jane", friends_of: #Ecto.Association.NotLoaded<association :friends_of is not loaded>, friends_with: #Ecto.Association.NotLoaded<association :friends_with is not loaded>, id: 4, inserted_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, messages: #Ecto.Association.NotLoaded<association :messages is not loaded>, surname: "Doe", updated_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, username: "jane_doe"}, %Ectoing.User{__meta__: #Ecto.Schema.Metadata<:loaded>, firstname: "Jane", friends_of: #Ecto.Association.NotLoaded<association :friends_of is not loaded>, friends_with: #Ecto.Association.NotLoaded<association :friends_with is not loaded>, id: 4, inserted_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, messages: #Ecto.Association.NotLoaded<association :messages is not loaded>, surname: "Doe", updated_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, username: "jane_doe"}] 

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

 results = Ectoing.Repo.all query Ectoing.Repo.preload results, :messages 

Загрузка ассоциаций из запроса может быть выполнена с использованием комбинации функций assoc и preload :

 SELECT * FROM users u INNER JOIN messages m ON u.id = m.user_id WHERE u.id = 4; 
 # Keywords query syntax query = from u in Ectoing.User, join: m in assoc(u, :messages), where: u.id == 4, preload: [messages: m] # Macro syntax query = (Ectoing.User |> join(:inner, [u], m in assoc(u, :messages)) |> where([u], u.id == 4) |> preload([u, m], [messages: m])) Ectoing.Repo.all query 

Теперь у нас есть ассоциация сообщений, загруженная в результате:

 [%Ectoing.User{__meta__: #Ecto.Schema.Metadata<:loaded>, firstname: "Jane", friends_of: #Ecto.Association.NotLoaded<association :friends_of is not loaded>, friends_with: #Ecto.Association.NotLoaded<association :friends_with is not loaded>, id: 4, inserted_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, messages: [%Ectoing.Message{__meta__: #Ecto.Schema.Metadata<:loaded>, id: 5, inserted_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, message_body: "Message 5", updated_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 4}, %Ectoing.Message{__meta__: #Ecto.Schema.Metadata<:loaded>, id: 6, inserted_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, message_body: "Message 6", updated_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 4}], surname: "Doe", updated_at: #Ecto.DateTime<2016-05-15T20:23:58Z>, username: "jane_doe"}] 

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

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

Внедрение фрагмента SQL

В то время как Ecto предоставляет нам множество функциональных возможностей, он предоставляет функции только для общих операций в SQL (он не нацелен на эмуляцию всего языка SQL). Когда нам нужно вернуться обратно в необработанный SQL, мы можем использовать функцию fragment/1 , позволяющую напрямую вводить код SQL в запрос.

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

 SELECT username FROM users WHERE username LIKE BINARY '%doe'; 
 username = "%doe" # Keywords query syntax query = from u in Ectoing.User, select: u.username, where: fragment("? LIKE BINARY ?", u.username, ^username) # Macro syntax query = (Ectoing.User |> select([u], u.username) |> where([u], fragment("? LIKE BINARY ?", u.username, ^username))) Ectoing.Repo.all query 

(Выше приведен специфичный для MySQL SQL. Если вы используете другую базу данных, это не сработает.)

Функция fragment/1 принимает код SQL как строку, которую мы хотели бы вставить в качестве первого параметра. Это позволяет привязывать столбцы и значения к фрагменту кода SQL. Это делается с помощью заполнителей (в виде вопросительных знаков) в строке, а последующие аргументы, передаваемые fragment , связываются с каждым заполнителем соответственно.

Явный Кастинг

Еще один способ, которым Ecto использует определения схемы моделей, заключается в автоматическом приведении интерполированных выражений в запросах к соответствующим типам полей, определенным в схеме. Эти интерполированные выражения приводятся к типу поля, с которым они сравниваются. Например, если у нас есть фрагмент запроса, такой как u.username > ^username , где u.username определено как field :username, :string ,: field :username, :string в схеме, переменная username будет автоматически преобразована в строку с помощью Ecto.

Иногда, однако, мы не всегда хотим, чтобы Ecto приводил интерполированные выражения к определенным типам полей. И в других случаях Ecto не сможет вывести тип для приведения выражения (обычно это происходит, когда задействованы фрагменты кода SQL). В обоих случаях мы можем использовать функцию type/2 чтобы указать выражение и тип, к которому оно должно быть приведено.

Давайте рассмотрим первый случай желания привести выражение к другому типу, поскольку это более интересный сценарий. В нашем приложении Ectoing мы использовали макрос Ecto.Schema.timestamps чтобы добавить два дополнительных поля в каждую из наших таблиц: updated_at и inserted_at . Макрос по умолчанию устанавливает тип этих полей, чтобы иметь тип Ecto.DateTime . Теперь, если мы хотим увидеть, сколько пользователей зарегистрировалось в текущем месяце, мы могли бы использовать простой запрос, подобный следующему:

 Ectoing.Repo.all from u in Ectoing.User, select: count(u.id), where: u.inserted_at >= ^Ecto.Date.from_erl({2016, 05, 01}) 

Это, однако, даст нам Ecto.CastError , поскольку структуру Ecto.Date нельзя привести к структуре Ecto.DateTime (поскольку мы сравниваем интерполированные выражения Ecto.Date с полем типа Ecto.DateTime ). В этом случае мы можем либо создать структуру Ecto.DateTime , либо указать Ecto, что мы хотели бы Ecto.Date выражение в Ecto.Date вместо Ecto.DateTime :

 Ectoing.Repo.all from u in Ectoing.User, select: count(u.id), where: u.inserted_at >= type(^Ecto.Date.from_erl({2016, 05, 01}), Ecto.Date) 

Теперь Экто с радостью принимает запрос. После операции приведения он переводит интерполированное выражение Ecto.Date в базовый тип :date , который затем позволяет базовой базе данных (в данном случае MySQL) обрабатывать сравнение между датой и датой-временем.

Доступ к динамическому полю

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

 search_by_username = from u in get_users_overview, where: like(u.username, ^username) 

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

 query = Ectoing.User query_by_field = :username username = "%tp%" # Keywords query syntax search_by_field = from table in query, where: like(field(table, ^query_by_field), ^username) # Macro syntax search_by_field = (query |> where([table], like(field(table, ^query_by_field), ^username))) Ectoing.Repo.all search_by_field 

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

Вывод

Как в этой, так и в моей предыдущей статье об использовании DSL в Ecto мы рассмотрели достаточно много возможностей. Упомянутые функции должны охватывать подавляющее большинство случаев, возникающих при использовании Ecto в приложениях. Но есть еще некоторые темы, которые не были рассмотрены (например, префикс запроса). В долгожданном выпуске Ecto 2.0 есть все новые функции, включая подзапросы, запросы на агрегацию и ассоциации «многие ко многим». Эти, а также другие функции, не относящиеся к запросам DSL Экто, будут рассмотрены в следующих статьях — так что следите за обновлениями!