Spec-Zone.ru › Phoenix

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.

fun(result)
merge()
name()

Имя операции в Multi. Может быть любым термином, если он уникален в списке операций; например, :insert_post или {:delete_post, 5}.

run()
t()

Функции

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()

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

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

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

Spec-Zone.ru

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