Транзакции в Elixir при помощи Ecto.Multi
Приходилось ли вам составлять цепочку запросов к базе данных, в которой каждый новый запрос зависел от успеха выполнения предыдущего? Вообще говоря, каждый раз программно проверять результат выполнения запроса — не самое приятное дело: это обеспечит наличие большого количества вложенных операторов и значительно усложнит код. Кроме того, если одна из последних операций завершится неудачей, все предыдущие операции придётся откатить.
Одна из приятнейших особенностей Ecto 2.0 — модуль Ecto.Multi
. Он разработан специально для того, чтобы предоставить разработчикам возможность объединения многочисленных запросов к базе данных в одну транзакцию, которая либо выполнялась бы целиком, либо вовсе не выполнялась. Можно сказать, этот модуль — своего рода реализация принципа «всё или ничего». Ecto.Multi
поможет решить обозначенную выше проблему, избавив от необходимости использования многоуровневых вложений и обеспечив очень удобный API. Давайте скорее опробуем его в деле!
Описание задачи
Пусть имеется приложение, отслеживающее процесс эксплуатации двери, за которой, возможно, скрывается многомиллионное серверное оборудование или обычная, ничем не примечательная комната. У каждого человека, имеющего доступ к помещению за этой дверью, есть что-то вроде ключа, который содержит информацию о личности человека, позволяет ему открыть дверь и пройти внутрь.
Хотелось бы отметить, что в данном приложении не самым лучшим способом реализовано взаимодействие с базой данных, так как оно прежде всего предназначено для демонстрации работы модуля Ecto.Multi
, а не для иллюстрации чистого и хорошо структурированного кода.
Что ж, теперь перейдём к делу.
Что мы будем отслеживать?
Приложение и база данных оперируют следующими абстракциями:
-
Дверь (Doors)
-
Человек (People)
-
Количество проходов человека в помещение (Entries)
-
Запись в журнале о количестве проходов (Logs)
В нашем случае в простеньком демо-приложении реализуется определённая последовательность действий: всякий раз, когда человек (Person) пытается открыть дверь (Door), отмечается проход (Entry), обновляется кэшированное прежде значение количества проходов для этого человека и делается запись этого значения в журнал (Log). Если на каком-то из этих этапов возникнет ошибка, то система вернётся к прежнему состоянию. Например, если не удалось сделать запись в журнал, то проход не отметится, а общее число проходов не обновится.
Как это делалось раньше
Один из способов реализации транзакций до появления Ecto.Multi
упрощённо выглядел так:
Писали ли вы когда-нибудь что-то подобное? Быть может, в контроллере в Phoenix? Лично я — да. Признаюсь даже, что такая схема со множеством вложений реализована в одном из разработанных мной действующих приложений.
Как это делает Ecto.Multi
Попробуем спасти положение с помощью Ecto.Multi
. Для начала, посмотрим на код, после чего обсудим его подробнее.
Прежде всего разберёмся с функцией entry_transaction. Модуль Ecto.Multi
содержит функции, аналогичные функциям модуля Ecto.Repo
: insert
, delete
, update
и многие другие. Однако в качестве второго аргумента этих функций выступает определённый в name
. Он позволяет присвоить имя некоторому набору действий, чтобы потом с ним можно было провести сопоставление с образцом. В приведённом выше примере они названы в соответствии с той записью в базе даных, к которой относятся. Важно, чтобы в пределах транзакции эти имена были уникальными. Поговорим об этом подробнее, когда будем рассматривать функцию enter_door
.
На каждом этапе транзакции дальнейшим операциям передаётся результат Multi.new
, а затем Repo уже осуществляет запросы к базе данных.
Если на любом из этапов в наборе изменений возникла ошибка или по какой-либо причине не выполнился запрос к базе данных, изменения в базу данных внесены не будут. Запрос, в ходе выполнения которого произошла ошибка, возвратится в кортеже вида {:error, name_of_call, failed_value, changes_that_succeeded}
. Переменная failed_value
содержит ошибки набора изменений (если он используется) или другие ошибочные значения, возвращаемые запросом, а changes_that_succeeded
содержит результаты всех предыдущих успешных операций. Тем не менее, согласно документации «необходимо произвести откат всех успешных операций», если не удалось выполнить всю транзакцию целиком.
Теперь перейдём к функции enter_door
. Итак, если во время выполнения транзакции произойдёт ошибка, возвратится кортеж вида {:error, ...}
. Если же всё пройдёт успешно, возвратится кортеж {:ok, map}
. По заданному нами имени (name
) операции в map можно получить доступ к значениям каждой из составляющих транзакцию операций. В текущем примере ключ :entry
в map
соотносится с результатом операции:
В связи с этим можно сделать вывод, что запросы ведут себя как одна целостная единица: неудачен один запрос, неудачны и все остальные.
Использование Ecto.Multi.run для выполнения произвольного кода
Ещё одно занимательное действие с Ecto.Multi
— использование функции run/3
для исполнения произвольного кода, содержащего в себе результаты предыдущих успешных операций. Рассмотрим пример.
Допустим, установим такое ограничение, что человек не может пройти в помещение более 10 раз. Обычно (и, возможно, так правильно) это делается внутри набора изменений для Person, но сейчас попробуем воспользоваться функцией run/3
, чтобы проверить, сколько раз человек уже открывал эту дверь:
Функция Ecto.Multi.run
на входе ожидает кортеж, содержащий либо {:ok, message}
, чтобы продолжить выполнение транзакции, либо {:error, message}
, чтобы откатить все изменения.
Можно видеть, что проводится сопоставление с образцом по Person, которое содержит результат успешного выполнения предыдущей операции update. Если человек уже прошёл в помещение 10 раз, то после успешного выполнения операции это значение обновится на 11, и возникнет ошибка. Сообщение об ошибке передастся в блок сопоставления с образцом оператора case.
Мощный функционал с простым API
Должен признаться, Ecto.Multi
и его возможности впечатляют. Он позволяет забыть об откате успешно выполненных операций, если зависимая операция по какой-то причине завершится неудачей. Кроме того, сопоставление содержащих ошибки транзакций с образцом — отличная возможность, позволяющая принимать дальнейшие меры в зависимости от того, какая из операций завершилась неудачей.
Советую вам активно использовать этот модуль в будущих разработках, а также совершенствовать с его помощью предыдущие.