複数のEcto.Multiを一つのトランザクション内で実行するには？

Ecto 2.0から導入された`Ecto.Multi`は、複数のタグ付けされたオペレーションを一つのトランザクション内で実行出来る機能です。

José Valimさんの[こちら](https://github.com/elixir-lang/ecto/issues/1114)のアナウンスにおけるコードをそのままコピペさせて頂きますと、下記のようになります。

```ex
user_changeset = User.changeset(%User{}, params)
log_changeset = Log.changeset(%Log{}, event: "user updated")

multi =
  Ecto.Multi.new
  |> Ecto.Multi.update(:user, user_changeset)
  |> Ecto.Multi.insert(:log, log_changeset)
  |> Ecto.Multi.run(:charge_credit_card, fn changes_so_far -> {:ok, _} | {:error _} end)

case Repo.transaction(multi) do
  {:ok, %{user: user, log: log}} ->
    ...
  {:error, key_that_errored, %{user: user_changeset}} ->
    ...
end
```

複数のModelに対して一つのトランザクション内で処理を行う上では色々とメリットの大きい機能なので、既にEcto 2.0を使ってらっしゃる方は重宝してらっしゃると思うのですが、下記のように**`Ecto.Multi`自体が既に複数定義されている場合**に、それぞれのオペレーションを一つのトランザクション内で実行するにはどうすればよいでしょう？

```ex
defmodule MyApp.EntryService do
  def insert(params) do
    Ecto.Multi.new
    |> Ecto.Multi.insert(:entry, Entry.changeset(%Entry{}, params))
    |> Ecto.Multi.insert(:log, Log.changeset(%Log{}, event: "entry inserted"))
  end
end

defmodule MyApp.SectionService do
  def insert(params) do
    Ecto.Multi.new
    |> Ecto.Multi.insert(:section, Entry.changeset(%Section{}, params))
    |> Ecto.Multi.insert(:log, Log.changeset(%Log{}, event: "section inserted"))
  end
end
```

# 考察1

`Ecto.Multi`には`append`や`prepend`という、複数の`Ecto.Multi`構造体を一つの`Ecto.Multi`構造体にまとめることが出来る関数が用意されています。

しかし、ドキュメントを読んでみると

> All names must be unique between both structures.

という記述があります。要するに各オペレーションのタグ名は他と重複してはいけないというルールになっています。

上記のコードの場合、例えば`:log`という重複するタグ名を`:entry_log`と`:section_log`に変更すればうまくいきそうですが、この方式は**それぞれのMultiを一回だけ実行する場合にしか**適用出来ません。

例えば複数のSectionモデルのinsert処理を一つのトランザクション内で実行しようとすると、`append`や`prepend`ではタグ名が重複してしまいますので、この方式ではそういったケースに対応することが出来ません。

タグ名を動的に変更する(連番を付加する)等の荒技もあり得ますが、タグ名は`atom`で指定することが必要なため、連番数がいくつになるか不明な場合には大量の`atom`を動的に生成してしまうことになり非常に危険です。

http://elixir-lang.org/getting-started/mix-otp/genserver.html

> **we should never convert user input to atoms.** This is because atoms are not garbage collected. Once an atom is created, it is never reclaimed. Generating atoms from user input would mean the user can inject enough different names to exhaust our system memory!

# 考察2

もう一つ別の手法として、**Ecto.Repoによるトランザクションをネストする**という方式が用意されています。

[Ecto.Repo.transaction/2](https://hexdocs.pm/ecto/Ecto.Repo.html#c:transaction/2)

> If transaction/2 is called inside another transaction, the function is simply executed, without wrapping the new transaction call in any way. If there is an error in the inner transaction and the error is rescued, or the inner transaction is rolled back, the whole outer transaction is marked as tainted, guaranteeing nothing will be comitted.

上記はEcto 1.1のドキュメントになりますが、Ecto 2.0でもこの記述は有効です。

ということで、上述の`EntryService`と`SectionService`の`insert/1`関するから返される2種類のEcto.Multiを、`Ecto.Repo.transaction/2`関数のネストに含めてみると、下記のようなコードになります。

```ex
Repo.transaction(fn ->
  case Repo.transaction(EntryService.insert(entry_params)) do
    {:ok, _} ->
      Enum.each(entry_params["sections"], fn(section_params) ->
        case Repo.transaction(SectionService.insert(section_params)) do
          {:ok, _} -> :ok
          {:error, _, _, _} -> raise "section save failed."
        end
      end)
    {:error, _, _, _} -> raise "entry save failed."
  end
end)
```

説明のためにかなり簡略化してあります。外側のトランザクションでは`Ecto.Repo.transaction/2`関数の第一引数に無名関数を渡し、内側のトランザクションでは`Ecto.Multi`構造体を渡しているところがポイントです。内側のトランザクション内で何らかのエラーが発生すれば、外側のトランザクションもロールバックされます。

# まとめ

少々ゴチャついたコードになってしまうのが難点ですが、色々調べてみた限り、とりあえず現時点では考察2のような方法で対応しておくしかないかな〜という印象です。

なにか他に良い方法をご存知の方がいらっしゃいましたら、コメント等頂けますと大変有り難いですm(__)m