Ecto.Multi
Исходный кодEcto.Multi — это структура данных для группировки нескольких операций Repo.
Ecto.Multi позволяет объединить операции, которые должны выполняться в одной транзакции базы данных, и предоставляет способ изучить ожидающие выполнения операции, не выполняя их. Каждой операции присваивается уникальное имя, по которому можно определить её результат в случае успеха или сбоя.
Если Multi действителен (то есть все содержащиеся в нём changeset действительны), все операции будут выполнены в том порядке, в котором они были добавлены.
Структуру Ecto.Multi следует считать непрозрачной. Можно использовать %Ecto.Multi{} для сопоставления с типом, но обращаться к полям или напрямую изменять их не рекомендуется.
Ecto.Multi.to_list/1 возвращает каноническое представление структуры, которое можно использовать для её изучения.
Когда использовать Ecto.Multi?
Ecto.Multi особенно полезен, когда набор выполняемых операций определяется динамически. В большинстве других случаев проще использовать обычное управление потоком внутри Repo.transact(fun) и возвращать {:ok, result} или {:error, reason}.
Changeset
Если Multi содержит операции, принимающие changeset (например, insert/4, update/4 или delete/4), они будут проверены до начала транзакции. Если в каком-либо changeset есть ошибки, транзакция не будет запущена, а ошибка будет немедленно возвращена.
Примечание: варианты insert/4, update/4, insert_or_update/4 и delete/4, принимающие функцию, не выполняют эти проверки, поскольку функции запускаются после начала транзакции.
Выполнение
Multi позволяет запускать произвольные функции в рамках транзакции с помощью run/3 и run/5. Это особенно полезно, когда операция зависит от значения предыдущей операции. Поэтому функция, переданная в качестве callback для run/3 и run/5, получает репозиторий первым аргументом, а все выполненные к этому моменту операции Multi — вторым аргументом в виде карты.
Функция, переданная в run, должна возвращать в качестве результата {:ok, value} или {:error, value}. Возврат ошибки прервёт выполнение остальных операций и приведёт к сбою Multi.
Пример
Рассмотрим пример определения и использования: сброс пароля. Нам нужно обновить учётную запись, указав правильные данные, записать запрос в журнал и удалить все текущие сеансы:
defmodule PasswordManager do
alias Ecto.Multi
def reset(account, params) do
Multi.new()
|> Multi.update(:account, Account.password_reset_changeset(account, params))
|> Multi.insert(:log, Log.password_reset_changeset(account, params))
|> Multi.delete_all(:sessions, Ecto.assoc(account, :sessions))
end
end
Затем мы можем выполнить эту операцию на интеграционном уровне с помощью Repo:
Repo.transact(PasswordManager.reset(account, params))
Сопоставляя результат с шаблоном, мы можем различать разные ситуации:
case result do
{:ok, %{account: account, log: log, sessions: sessions}} ->
# The Multi was successful. We can access results , which are as
# we would get from running the corresponding Repo functions, under
# keys we used for naming the operations.
{:error, failed_operation, failed_value, changes_so_far} ->
# One of the operations failed. We can access the operation's failure
# value (such as a changeset for operations on changesets) to prepare a
# proper response. We also get access to the results of any operations
# that succeeded before the indicated operation failed. (However,
# successful operations were rolled back.)
end
Мы также можем легко написать модульный тест для транзакции, не запуская её. Поскольку changeset может использовать данные в памяти, мы можем использовать и учётную запись, созданную в памяти, не сохраняя её в базе данных:
test "dry run password reset" do
account = %Account{password: "letmein"}
multi = PasswordManager.reset(account, params)
assert [
{:account, {:update, account_changeset, []}},
{:log, {:insert, log_changeset, []}},
{:sessions, {:delete_all, query, []}}
] = Ecto.Multi.to_list(multi)
# We can introspect changesets and query to see if everything
# is as expected, for example:
assert account_changeset.valid?
assert log_changeset.valid?
assert inspect(query) == "#Ecto.Query<from a in Session>"
end
Имя каждой операции не обязательно должно быть атомом. Это особенно полезно, если нужно одновременно обновить набор changeset и отслеживать ошибки для каждого из них отдельно:
accounts = [%Account{id: 1}, %Account{id: 2}]
Enum.reduce(accounts, Multi.new(), fn account, multi ->
Multi.update(
multi,
{:account, account.id},
Account.password_reset_changeset(account, params)
)
end) Сводка
- changes()
Карта изменений, выполненных к настоящему моменту в текущей транзакции. Для любого Multi, возвращающего
{:ok, result}, егоname/0добавляется в качестве ключа, а результат — в качестве значения.- failure()
Результат неудачной транзакции с использованием Multi.
- name()
Имя операции в Multi. Может быть любым термином, если он уникален в списке операций; например,
:insert_postили{:delete_post, 5}.
Типы
- all(multi, name, queryable_or_fun, opts \\ [])
Выполняет запрос и сохраняет все результаты в Multi.
- append(lhs, rhs)
Добавляет второй Multi в конец первого.
- delete(multi, name, changeset_or_struct_fun, opts \\ [])
Добавляет в Multi операцию удаления.
- delete_all(multi, name, queryable_or_fun, opts \\ [])
Добавляет в Multi операцию
delete_all.- error(multi, name, value)
Вызывает сбой Multi с указанным значением.
- exists?(multi, name, queryable_or_fun, opts \\ [])
Проверяет, существует ли запись, соответствующая заданному запросу, и сохраняет в Multi логическое значение.
- insert(multi, name, changeset_or_struct_or_fun, opts \\ [])
Добавляет в Multi операцию вставки.
- insert_all(multi, name, schema_or_source, entries_or_query_or_fun, opts \\ [])
Добавляет в Multi операцию
insert_all.- insert_or_update(multi, name, changeset_or_fun, opts \\ [])
Вставляет или обновляет changeset в зависимости от того, был ли он сохранён.
- inspect(multi, opts \\ [])
Выводит результаты Multi.
- merge(multi, merge)
Объединяет Multi, динамически возвращённый анонимной функцией.
- merge(multi, mod, fun, args)
Объединяет Multi, динамически возвращённый вызовом
moduleиfunctionсargs.- new()
Возвращает пустую структуру
Ecto.Multi.- one(multi, name, queryable_or_fun, opts \\ [])
Выполняет запрос, ожидающий один результат, и сохраняет результат в Multi.
- prepend(lhs, rhs)
Добавляет второй Multi в начало первого.
- put(multi, name, value)
Добавляет значение к уже накопленным изменениям под указанным именем.
- run(multi, name, run)
Добавляет функцию для выполнения в рамках Multi.
- run(multi, name, mod, fun, args)
Добавляет функцию для выполнения в рамках Multi.
- to_list(multi)
Возвращает список операций, сохранённых в Multi.
- update(multi, name, changeset_or_fun, opts \\ [])
Добавляет в Multi операцию обновления.
- update_all(multi, name, queryable_or_fun, updates, opts \\ [])
Добавляет в Multi операцию
update_all.
Функции
Типы
changes()Исходный код
@type changes() :: map()
Карта изменений, выполненных к настоящему моменту в текущей транзакции. Для любого Multi, возвращающего {:ok, result}, его name/0 добавляется в качестве ключа, а результат — в качестве значения.
failure()Исходный код
@type failure() ::
{:error, failed_operation :: name(), failed_value :: any(),
changes_so_far :: %{required(name()) => any()}} Результат неудачной транзакции с использованием Multi.
fun(result)Исходный код
@type fun(result) :: (changes() -> result)
merge()Исходный код
@type merge() :: (changes() -> t()) | {module(), atom(), [any()]} name()Исходный код
@type name() :: any()
Имя операции в Multi. Может быть любым термином, если он уникален в списке операций; например, :insert_post или {:delete_post, 5}.
run()Исходный код
@type run() :: (Ecto.Repo.t(), changes() -> {:ok | :error, any()}) t()Исходный код
@type t() :: %Ecto.Multi{names: names(), operations: operations()} Функции
all(multi, name, queryable_or_fun, opts \\ [])Исходный код
@spec all( t(), name(), queryable :: Ecto.Queryable.t() | (changes() -> Ecto.Queryable.t()), opts :: Keyword.t() ) :: t()
Выполняет запрос и сохраняет все результаты в Multi.
name должен быть уникальным в пределах Multi.
Остальные аргументы и параметры совпадают с Ecto.Repo.all/2.
Пример
Ecto.Multi.new() |> Ecto.Multi.all(:all, Post) |> MyApp.Repo.transact() Ecto.Multi.new() |> Ecto.Multi.all(:all, fn _changes -> Post end) |> MyApp.Repo.transact()
append(lhs, rhs)Исходный код
@spec append(t(), t()) :: t()
Добавляет второй Multi в конец первого.
Все имена в обеих структурах должны быть уникальными.
Пример
iex> lhs = Ecto.Multi.new() |> Ecto.Multi.run(:left, fn _, changes -> {:ok, changes} end)
iex> rhs = Ecto.Multi.new() |> Ecto.Multi.run(:right, fn _, changes -> {:error, changes} end)
iex> Ecto.Multi.append(lhs, rhs) |> Ecto.Multi.to_list |> Keyword.keys
[:left, :right] delete(multi, name, changeset_or_struct_fun, opts \\ [])Исходный код
@spec delete( t(), name(), Ecto.Changeset.t() | Ecto.Schema.t() | (changes() -> Ecto.Changeset.t() | Ecto.Schema.t()), Keyword.t() ) :: t()
Добавляет в Multi операцию удаления.
name должен быть уникальным в пределах Multi.
Остальные аргументы и параметры совпадают с Ecto.Repo.delete/2.
Пример
post = MyApp.Repo.get!(Post, 1)
Ecto.Multi.new()
|> Ecto.Multi.delete(:delete, post)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.delete(:delete, fn %{post: post} ->
# Others validations
post
end)
|> MyApp.Repo.transact() delete_all(multi, name, queryable_or_fun, opts \\ [])Исходный код
@spec delete_all( t(), name(), Ecto.Queryable.t() | (changes() -> Ecto.Queryable.t()), Keyword.t() ) :: t()
Добавляет в Multi операцию delete_all.
Принимает те же аргументы и параметры, что и Ecto.Repo.delete_all/2.
Пример
queryable = from(p in Post, where: p.id < 5)
Ecto.Multi.new()
|> Ecto.Multi.delete_all(:delete_all, queryable)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.delete_all(:delete_all, fn %{post: post} ->
# Others validations
from(c in Comment, where: c.post_id == ^post.id)
end)
|> MyApp.Repo.transact() error(multi, name, value)Исходный код
@spec error(t(), name(), error :: term()) :: t()
Вызывает сбой Multi с указанным значением.
При выполнении Multi в транзакции предыдущие шаги не выполняются, а возвращается значение первой добавленной ошибки.
exists?(multi, name, queryable_or_fun, opts \\ [])Исходный код
@spec exists?( t(), name(), queryable :: Ecto.Queryable.t() | (changes() -> Ecto.Queryable.t()), opts :: Keyword.t() ) :: t()
Проверяет, существует ли запись, соответствующая заданному запросу, и сохраняет в Multi логическое значение.
name должен быть уникальным в пределах Multi.
Остальные аргументы и параметры совпадают с Ecto.Repo.exists?/2.
Пример
Ecto.Multi.new() |> Ecto.Multi.exists?(:post, Post) |> MyApp.Repo.transact() Ecto.Multi.new() |> Ecto.Multi.exists?(:post, fn _changes -> Post end) |> MyApp.Repo.transact()
insert(multi, name, changeset_or_struct_or_fun, opts \\ [])Исходный код
@spec insert( t(), name(), Ecto.Changeset.t() | Ecto.Schema.t() | (changes() -> Ecto.Changeset.t() | Ecto.Schema.t()), Keyword.t() ) :: t()
Добавляет в Multi операцию вставки.
name должен быть уникальным в пределах Multi.
Остальные аргументы и параметры совпадают с Ecto.Repo.insert/2.
Пример
Ecto.Multi.new()
|> Ecto.Multi.insert(:insert, %Post{title: "first"})
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.insert(:post, %Post{title: "first"})
|> Ecto.Multi.insert(:comment, fn %{post: post} ->
Ecto.build_assoc(post, :comments)
end)
|> MyApp.Repo.transact() insert_all(multi, name, schema_or_source, entries_or_query_or_fun, opts \\ [])Исходный код
@spec insert_all(
t(),
name(),
schema_or_source(),
entries_or_query_or_fun ::
[map() | Keyword.t()]
| (changes() -> [map() | Keyword.t()])
| Ecto.Query.t(),
Keyword.t()
) :: t() Добавляет в Multi операцию insert_all.
Принимает те же аргументы и параметры, что и Ecto.Repo.insert_all/3.
Пример
posts = [%{title: "My first post"}, %{title: "My second post"}]
Ecto.Multi.new()
|> Ecto.Multi.insert_all(:insert_all, Post, posts)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.insert_all(:insert_all, Comment, fn %{post: post} ->
# Others validations
entries
|> Enum.map(fn comment ->
Map.put(comment, :post_id, post.id)
end)
end)
|> MyApp.Repo.transact() insert_or_update(multi, name, changeset_or_fun, opts \\ [])Исходный код
@spec insert_or_update( t(), name(), Ecto.Changeset.t() | (changes() -> Ecto.Changeset.t()), Keyword.t() ) :: t()
Вставляет или обновляет changeset в зависимости от того, был ли он сохранён.
name должен быть уникальным в пределах Multi.
Остальные аргументы и параметры совпадают с Ecto.Repo.insert_or_update/2.
Пример
changeset = Post.changeset(%Post{}, %{title: "New title"})
Ecto.Multi.new()
|> Ecto.Multi.insert_or_update(:insert_or_update, changeset)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
{:ok, repo.get(Post, 1) || %Post{}}
end)
|> Ecto.Multi.insert_or_update(:update, fn %{post: post} ->
Ecto.Changeset.change(post, title: "New title")
end)
|> MyApp.Repo.transact() inspect(multi, opts \\ [])Исходный код
@spec inspect(t(), Keyword.t()) :: t()
Выводит результаты Multi.
По умолчанию имя отображается как метка при выводе. Для задания пользовательских меток используется параметр label IO.inspect/2.
Параметры
Поддерживаются все параметры IO.inspect/2, а также:
-
:only— поле или список полей для вывода; по умолчанию выводится вся карта.
Примеры
Ecto.Multi.new() |> Ecto.Multi.insert(:person_a, changeset) |> Ecto.Multi.insert(:person_b, changeset) |> Ecto.Multi.inspect() |> MyApp.Repo.transact()
Выводит:
%{person_a: %Person{...}, person_b: %Person{...}}
Чтобы ограничить список выводимых полей, можно использовать параметр :only:
Ecto.Multi.new() |> Ecto.Multi.insert(:person_a, changeset) |> Ecto.Multi.insert(:person_b, changeset) |> Ecto.Multi.inspect(only: :person_a) |> MyApp.Repo.transact()
Выводит:
%{person_a: %Person{...}} merge(multi, merge)Исходный код
@spec merge(t(), (changes() -> t())) :: t()
Объединяет Multi, динамически возвращённый анонимной функцией.
Эта функция полезна, когда объединяемому Multi требуется информация из исходного Multi. Второй аргумент — анонимная функция, получающая накопленные на данный момент изменения Multi. Анонимная функция должна возвращать другой Multi.
Если нужно просто объединить два Multi, см. append/2 или prepend/2.
Повторяющиеся операции не допускаются.
Пример
multi =
Ecto.Multi.new()
|> Ecto.Multi.insert(:post, %Post{title: "first"})
multi
|> Ecto.Multi.merge(fn %{post: post} ->
Ecto.Multi.new()
|> Ecto.Multi.insert(:comment, Ecto.build_assoc(post, :comments))
end)
|> MyApp.Repo.transact() merge(multi, mod, fun, args)Исходный код
@spec merge(t(), module(), function, args) :: t() when function: atom(), args: [any()]
Объединяет Multi, динамически возвращённый вызовом module и function с args.
Подобно merge/2, но позволяет передать имя модуля, функцию и аргументы. Функция должна возвращать Ecto.Multi и получает накопленные изменения первым аргументом (добавленным перед аргументами, переданными при вызове функции).
Повторяющиеся операции не допускаются.
new()Исходный код
@spec new() :: t()
Возвращает пустую структуру Ecto.Multi.
Пример
iex> Ecto.Multi.new() |> Ecto.Multi.to_list() []
one(multi, name, queryable_or_fun, opts \\ [])Исходный код
@spec one( t(), name(), queryable :: Ecto.Queryable.t() | (changes() -> Ecto.Queryable.t()), opts :: Keyword.t() ) :: t()
Выполняет запрос, ожидающий один результат, и сохраняет результат в Multi.
name должен быть уникальным в пределах Multi.
Остальные аргументы и параметры совпадают с Ecto.Repo.one/2.
Пример
Ecto.Multi.new()
|> Ecto.Multi.one(:post, Post)
|> Ecto.Multi.one(:author, fn %{post: post} ->
from(a in Author, where: a.id == ^post.author_id)
end)
|> MyApp.Repo.transact() prepend(lhs, rhs)Исходный код
@spec prepend(t(), t()) :: t()
Добавляет второй Multi в начало первого.
Все имена в обеих структурах должны быть уникальными.
Пример
iex> lhs = Ecto.Multi.new() |> Ecto.Multi.run(:left, fn _, changes -> {:ok, changes} end)
iex> rhs = Ecto.Multi.new() |> Ecto.Multi.run(:right, fn _, changes -> {:error, changes} end)
iex> Ecto.Multi.prepend(lhs, rhs) |> Ecto.Multi.to_list |> Keyword.keys
[:right, :left] put(multi, name, value)Исходный код
@spec put(t(), name(), any()) :: t()
Добавляет значение к уже накопленным изменениям под указанным именем.
Указанное value добавляется в Multi до начала транзакции. Если нужно запускать произвольные функции в рамках транзакции, см. run/3 или run/5.
Пример
Предположим, что из базы данных была получена существующая схема компании. Её можно добавить в Multi как изменение с помощью put/3:
Ecto.Multi.new() |> Ecto.Multi.put(:company, company) |> Ecto.Multi.insert(:user, fn changes -> User.changeset(changes.company) end) |> Ecto.Multi.insert(:person, fn changes -> Person.changeset(changes.user, changes.company) end) |> MyApp.Repo.transact()
В приведённом выше примере добавление company в Multi не даёт существенных преимуществ, поскольку к переменной company можно было бы обратиться напрямую внутри анонимной функции.
Однако преимущество put/3 проявляется при объединении Ecto.Multi. Если операции вставки выше определены в другом модуле, можно использовать put(:company, company) для передачи изменений, к которым смогут обращаться другие функции в цепочке, избавившись от необходимости передавать значения multi и company.
run(multi, name, run)Исходный код
@spec run(t(), name(), run()) :: t()
Добавляет функцию для выполнения в рамках Multi.
Функция должна возвращать {:ok, value} или {:error, value}. Она получает репозиторий первым аргументом, а накопленные изменения — вторым.
Пример
Ecto.Multi.run(multi, :write, fn _repo, %{image: image} ->
with :ok <- File.write(image.name, image.contents) do
{:ok, nil}
end
end) run(multi, name, mod, fun, args)Исходный код
@spec run(t(), name(), module(), function, args) :: t() when function: atom(), args: [any()]
Добавляет функцию для выполнения в рамках Multi.
Подобно run/3, но позволяет передать имя модуля, функцию и аргументы. Функция должна возвращать {:ok, value} или {:error, value}. Она получает репозиторий первым аргументом, а накопленные изменения — вторым (переданным перед аргументами, указанными при вызове функции).
to_list(multi)Исходный код
@spec to_list(t()) :: [{name(), term()}] Возвращает список операций, сохранённых в Multi.
Всегда используйте эту функцию, когда нужно получить доступ к операциям, определённым в Ecto.Multi. Не рекомендуется напрямую изучать внутреннее устройство структуры Ecto.Multi.
update(multi, name, changeset_or_fun, opts \\ [])Исходный код
@spec update( t(), name(), Ecto.Changeset.t() | (changes() -> Ecto.Changeset.t()), Keyword.t() ) :: t()
Добавляет в Multi операцию обновления.
name должен быть уникальным в пределах Multi.
Остальные аргументы и параметры совпадают с Ecto.Repo.update/2.
Пример
post = MyApp.Repo.get!(Post, 1)
changeset = Ecto.Changeset.change(post, title: "New title")
Ecto.Multi.new()
|> Ecto.Multi.update(:update, changeset)
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.insert(:post, %Post{title: "first"})
|> Ecto.Multi.update(:fun, fn %{post: post} ->
Ecto.Changeset.change(post, title: "New title")
end)
|> MyApp.Repo.transact() update_all(multi, name, queryable_or_fun, updates, opts \\ [])Исходный код
@spec update_all( t(), name(), Ecto.Queryable.t() | (changes() -> Ecto.Queryable.t()), Keyword.t(), Keyword.t() ) :: t()
Добавляет в Multi операцию update_all.
Принимает те же аргументы и параметры, что и Ecto.Repo.update_all/3.
Пример
Ecto.Multi.new()
|> Ecto.Multi.update_all(:update_all, Post, set: [title: "New title"])
|> MyApp.Repo.transact()
Ecto.Multi.new()
|> Ecto.Multi.run(:post, fn repo, _changes ->
case repo.get(Post, 1) do
nil -> {:error, :not_found}
post -> {:ok, post}
end
end)
|> Ecto.Multi.update_all(:update_all, fn %{post: post} ->
# Others validations
from(c in Comment, where: c.post_id == ^post.id, update: [set: [title: "New title"]])
end, [])
|> MyApp.Repo.transact()
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.Multi.html