Ecto 2.0.0-betaを試す (3)
今回はEcto.Multi
です。
なにそれ
Ecto.Multi
とはそもそも何かというと、1つのトランザクション内で行われるべき複数のRepo
操作をまとめるデータ構造です。DBを操作する前にどのような変更がなされるのか覗き見することができます。これの導入に伴い、before_insert
などのcallbackが全てdeprecatedになり、callback地獄が解消されるそうです。
使う
使い方を簡単に見てみます。Ecto.Multi.new/0
で構造体を初期化します。そしてEcto.Multi.insert/4
やEcto.Multi.update/4
を使って操作を追加していきます。追加した順に実行されますので、下の例ではUser
をつくってからログに記録します。
multi = Ecto.Multi.new |> Ecto.Multi.insert(:new_user, User.changeset(%User{}, user_params)) |> Ecto.Multi.insert(:log, Log.changeset(%Log{}, log_params))
トランザクション内で実行するには、Repo.transaction/2
に渡します。
Repo.transaction(multi)
変更を取得する
ドキュメントには
We can introspect changesets and query to see if everything is as expected
と書かれており、どんな変更が行われるのか、つまりchangesetが見れるとあります(そもそもchangesetをこちらから渡しているので当然といえば当然ですが)。これにはEcto.Multi.to_list/1
を使います。直接%Ecto.Multi{}
を扱うのは推奨されないので、こういう関数を使いましょう。前々回で作ったUser
を例にしてみます。
alias Ecto.Multi user1 = %User{} |> User.changeset(%{name: "Jack", password: "password"}) user2 = %User{} |> User.changeset(%{name: "", password: "password"}) multi = Multi.new |> Multi.insert(:user1, user1) |> Multi.insert(:user2, user2) operations = Multi.to_list(multi)
User
を2人追加します。第2引数の:user1
と:user2
は操作に名前を与えているだけなので、一意であれば何でもいいです。で、Ecto.Multi.to_list/1
はその名前をキーにしたキーワードリストを返してくれるので、どの操作がエラーになるのかがわかるという感じです。
[user1: { :insert, #Ecto.Changeset< action: :insert, changes: %{ name: "Jack", password: "password", password_hash: "hogehogefugafuga" }, errors: [], data: #PlayEcto.User<>, valid?: true >, []}, user2: { :insert, #Ecto.Changeset< action: :insert, changes: %{ name: "", password: "password", password_hash: "hogehogefugafuga" }, errors: [name: "can't be blank"], data: #PlayEcto.User<>, valid?: false >, []} ]
任意の処理を挟む
Ecto.Multi.run/3,5
を使うと、insertやupdate以外の処理をトランザクションの内側で行うことができます。あんまり良い例が思いつかないですが、例えば正しく受注情報がinsertされたらメールを送ることを考えると以下のようになります。
Ecto.Multi.new |> Ecto.Multi.insert(:order, Order.changeset(%Order{}, order_params)) |> Ecto.Multi.run(:send_mail, fn changes -> case send_mail(changes.order) do :ok -> {:ok, changes} _ -> {:error, changes} end )
第3引数の無名関数には、run
より前のinsert
やupdate
で行われる変更が渡ってきます(changesetのリスト)。orderのinsertが失敗すればメールは飛びませんし、メール送信が失敗して無名関数が{:error, changes}
を返すと、insertがロールバックされて注文が無かったことになります。この挙動はどうなんだという感じですが、例なので。
組み合わせる
Ecto.Multi.append/2, prepend/2
を使うと、2つの%Ecto.Multi{}
をガッチャンコできます。ただ繋げるだけです。
Ecto.Multi.append(m1, m2)
とEcto.Multi.prepend(m2, m1)
は等価です。
サービス
最後に、Ecto.Multi
のmoduledocにおすすめの使い方が書いてあったので紹介。Service
という層を設けます。
defmodule Service do alias Ecto.Multi import Ecto def password_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, assoc(account, :sessions)) end end
この層では%Ecto.Multi{}
を作るまでを担当し、Repo.transaction/2
に渡すのは別の場所でやるとのこと。例えばPhoenixアプリなら、lib/services
ディレクトリを切るかモデルにサービス層を書き、SQL発行はコントローラで、という分け方でしょうか。このようにRepo.transaction
は呼ばずに%Ecto.Multi{}
を返す関数を作ると、DBに触らないので単体テストがしやすくなるという利点があります。また、Ecto.Multi.append/2, prepend/2
で組み合わせやすくなってcomposableになるという点も特徴の1つです。
ではでは、この辺で。