1. tuchiro

    No comment

    tuchiro
Changes in body
Source | HTML | Preview
@@ -1,279 +1,280 @@
(この記事は、「[Elixir or Phoenix Advent Calendar 2017](https://qiita.com/advent-calendar/2017/elixir-or-phoenix "Elixir or Phoenix Advent Calendar 2017")」の4日目です)
# はじめに
Elixirで本格的に開発を初めて2ヶ月がたった。
実際のプロダクト開発で実装した経験を元に
SI的なプロダクト開発での実装パターンをElixirでどう実現するか書き記していこうと思う
第1回目はシステム開発の基本 DB操作のうち、排他制御に関するお話を前後編でお送りします。
---
# 楽観的ロックと悲観的ロック
まずは代表的な排他制御の概念だが・・・
すでに大変丁寧に説明いただいている投稿があるのでそちらを参照させていただこうと思う。
[排他制御(楽観ロック・悲観ロック)の基礎](https://qiita.com/NagaokaKenichi/items/73040df85b7bd4e9ecfc "排他制御(楽観ロック・悲観ロック)の基礎")
---
# Ecto.Multiでトランザクション制御
ではElixirのORマッパーであるEctoを使って悲観的ロックの実装をしてみよう。
悲観的ロックを実装するためには、更新対象をロックしてから全ての更新SQLが終わるまでロックを維持するためトランザクション制御が必要となる。
しかしサンプルなどでよく見るEctoの実装では、SQL毎に自動的にCOMMITが実行されており、
ロックを維持することができない。
よって、複数のSQLを同一トランザクションで実行する為、Ecto.Multiを使用する。
---
# 実装の前提
今回は以下の実装を例に考える
- 残高(credit)をもつユーザー(User)
- 単価(price)と数量(amount)をもつStock(在庫)
- ユーザーが商品を購入すると、
1. Userのcashを減算する
2. Stockのamountを減算する
- 上記2つの更新は同一トランザクションで実行する必要がある
また、以下の環境で実装した
- Elixir     v1.6.1
- Phoenix v1.3.2
- Ecto      v2.2.10
- PostgreSQL v10.2
---
# 開発手順 
## プロジェクト〜モデルの作成
まずはPhoenixプロジェクトを作成する
```
> mix phx.new ecto_multi_sample --no-brunch
> cd ecto_multi_sample
```
リポジトリの設定を確認しDBスキーマを作成
```Ruby:config/dev.ex
# Configure your database
config :ecto_multi_sample, EctoMultiSample.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",
  password: "postgres",
  database: "ecto_multi_sample_dev",
  hostname: "localhost",
  pool_size: 10
```
```
> mix ecto.create
```
---
モデルを作成してマイグレーションを実行
```
> mix phx.gen.html Accounts User users name:string age:integer mail:string    credit:integer
> mix phx.gen.html Items Stock stocks category:string name:string  price:integer amount:integer
> mix ecto.migrate
```
ルーティングを追加
```Ruby:ib/ecto_multi_sample_web/router.ex
defmodule EctoMultiSample.Router do
use EctoMultiSample.Web, :router
scope "/", EctoMultiSample do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/users", UserController
resources "/stocks", StockController
end
end
```
---
Phoenixを起動
```
> mix phx.server
```
http://localhost:40000 にアクセス
-![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/bacb10ad-3f3e-4e70-4b50-2d6694220a55.png)
+<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/bacb10ad-3f3e-4e70-4b50-2d6694220a55.png" width=50% >
---
Userを登録する
http://localhost:4000/users
-![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/791c4a10-b64f-5144-f297-fa0f13207d7b.png)
-![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/3c0c7091-94c8-2177-0b85-a5a5268629d6.png)
+<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/791c4a10-b64f-5144-f297-fa0f13207d7b.png" width=50% >
-![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/7c2df772-37ac-1d72-4dd6-9d88e41f55b3.png)
+<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/3c0c7091-94c8-2177-0b85-a5a5268629d6.png" width=50% >
+
+<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/7c2df772-37ac-1d72-4dd6-9d88e41f55b3.png" width=50% >
---
Stockを登録する
http://localhost:4000/stocks
-![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/f650e1d5-a8fe-979d-ae36-968dc1d01fef.png)
+<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/f650e1d5-a8fe-979d-ae36-968dc1d01fef.png" width=50% >
-![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/4081be1b-35c6-dc79-ae36-3b5858106bcb.png)
+<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/4081be1b-35c6-dc79-ae36-3b5858106bcb.png" width=50% >
-![image.png](https://qiita-image-store.s3.amazonaws.com/0/243163/343d9ee8-b37f-4071-a72c-d21cb3788c4c.png)
+<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/343d9ee8-b37f-4071-a72c-d21cb3788c4c.png" width=50% >
悲観的ロックの実装モジュールを作成
```Ruby:lib/ecto_multi_sample/cart_action.ex
defmodule EctoMultiSample.CartAction do

 import Ecto.Query, warn: false
alias EctoMultiSample.Repo
alias EctoMultiSample.Accounts.User
alias EctoMultiSample.Items.Stock
alias Ecto.Multi
alias CartAction

 def submit(order) do
#Multi使わない実装
end
def submit_multi(order) do
#Multiを使った実装
end

end
```
## Multiを使わなかった場合(トランザクション制御なし)
まずはEcto.Multiを使わな買った場合を確認する
```Ruby:lib/ecto_multi_sample/cart_action.ex
def submit(order) do
user = Repo.get(User, order.user_id)
stock = Repo.get(Stock, order.stock_id)
# 所持金を引く
uesr_credit = user.credit - ( order.amount * stock.price )
# 在庫を減らす
stock_amount = stock.amount - order.amount
user_changeset = User.changeset(user, %{credit: uesr_credit})
stock_changeset = Stock.changeset(stock, %{amount: stock_amount})
case Repo.update(user_changeset) do
{:ok, user} ->
IO.puts("# User updated successfully.")
{:error, changeset} ->
IO.puts("# User updated error.")
end
case Repo.update(stock_changeset) do
{:ok, stock} ->
IO.puts("# Stock updated successfully.")
{:error, changeset} ->
IO.puts("# Stock updated error.")
end
end
```
---
実行時のSQLトレースは以下のようになる
```
> iex -S mix
iex(1) > alias EctoMultiSample.CartAction
iex(2) > order = %{amount: 2, stock_id: 1, user_id: 1}
iex(3) > CartAction.submit(order)
```
```
2018-04-16 22:13:11.820 PDT [56416] LOG:  execute ecto_34: SELECT u0."id", u0."name", u0."age", u0."mail", u0."credit", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1)
2018-04-16 22:13:11.820 PDT [56416] DETAIL:  parameters: $1 = '1'
2018-04-16 22:13:11.827 PDT [56418] LOG:  execute ecto_66: SELECT s0."id", s0."category", s0."name", s0."price", s0."amount", s0."inserted_at", s0."updated_at" FROM "stocks" AS s0 WHERE (s0."id" = $1)
2018-04-16 22:13:11.827 PDT [56418] DETAIL:  parameters: $1 = '1'
2018-04-16 22:13:11.832 PDT [56417] LOG:  execute <unnamed>: UPDATE "users" SET "credit" = $1, "updated_at" = $2 WHERE "id" = $3
2018-04-16 22:13:11.832 PDT [56417] DETAIL:  parameters: $1 = '660', $2 = '2018-04-17 05:13:11.82837', $3 = '1'
2018-04-16 22:13:11.837 PDT [56419] LOG:  execute <unnamed>: UPDATE "stocks" SET "amount" = $1, "updated_at" = $2 WHERE "id" = $3
2018-04-16 22:13:11.837 PDT [56419] DETAIL:  parameters: $1 = '48', $2 = '2018-04-17 05:13:11.83358', $3 = '1'
```
毎回トランザクションIDが変わっているため、同一トランザクションで実行できていなことがわかる
## Multiを使った場合(トランザクション制御あり)
次に本題のMultiを使った悲観的ロックの実装ケースを見てみよう
```Ruby:lib/ecto_multi_sample/cart_action.ex
def submit_multi(order) do
result = Ecto.Multi.new
|> Multi.run( :get_order, EctoMultiSample.CartAction, :buy, [order] )
|> Repo.transaction()
case result do
{:ok, value} ->
IO.puts("# User & Stock updated successfully.")
{:error, changeset} ->
IO.puts("# User & Stock updated error.")
end
end
```
-```Ruby:lib/cart_action.ex
+```Elixir:lib/cart_action.ex
def buy(%{}, order) do
[user] = User
|> where([u], u.id == ^order.user_id)
|> lock("FOR UPDATE")
|> Repo.all()
[stock] = Stock
|> where([s], s.id == ^order.stock_id)
|> lock("FOR UPDATE")
|> Repo.all()

 # 所持金を引く
uesr_credit = user.credit - ( order.amount * stock.price )
# 在庫を減らす
stock_amount = stock.amount - order.amount
user_changeset = User.changeset(user, %{credit: uesr_credit})
stock_changeset = Stock.changeset(stock, %{amount: stock_amount})
Repo.update(user_changeset)
Repo.update(stock_changeset)
{:ok, "update OK."}
end
```
実行結果を見てみよう
```
> iex -S mix
iex(1) > alias EctoMultiSample.CartAction
iex(2) > order = %{amount: 2, stock_id: 1, user_id: 1}
iex(3) > CartAction.submit_multi(order)
```
```
2018-04-16 22:13:25.177 PDT [56420] LOG:  execute POSTGREX_BEGIN: BEGIN
2018-04-16 22:13:25.181 PDT [56420] LOG:  execute ecto_549: SELECT u0."id", u0."name", u0."age", u0."mail", u0."credit", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) FOR UPDATE
2018-04-16 22:13:25.181 PDT [56420] DETAIL:  parameters: $1 = '1'
2018-04-16 22:13:25.184 PDT [56420] LOG:  execute ecto_2: SELECT s0."id", s0."category", s0."name", s0."price", s0."amount", s0."inserted_at", s0."updated_at" FROM "stocks" AS s0 WHERE (s0."id" = $1) FOR UPDATE
2018-04-16 22:13:25.184 PDT [56420] DETAIL:  parameters: $1 = '1'
2018-04-16 22:13:25.185 PDT [56420] LOG:  execute <unnamed>: UPDATE "users" SET "credit" = $1, "updated_at" = $2 WHERE "id" = $3
2018-04-16 22:13:25.185 PDT [56420] DETAIL:  parameters: $1 = '320', $2 = '2018-04-17 05:13:25.184953', $3 = '1'
2018-04-16 22:13:25.187 PDT [56420] LOG:  execute <unnamed>: UPDATE "stocks" SET "amount" = $1, "updated_at" = $2 WHERE "id" = $3
2018-04-16 22:13:25.187 PDT [56420] DETAIL:  parameters: $1 = '46', $2 = '2018-04-17 05:13:25.186421', $3 = '1'
2018-04-16 22:13:40.190 PDT [56420] LOG:  execute POSTGREX_COMMIT: COMMIT
```
トランザクション制御(BEGIN〜COMMIT)が実行されており、
トランザクションIDが全て同一であることが確認できる。
## まとめ
- 悲観的ロックの実装にはトランザクション制御が必要
- トランザクション制御を行うためEcto.Multiを使う
- ロックを撮るにはlock("FOR UPDATE")を使う
次回は、もう一つの代表的な排他制御である楽観的ロックを実装する予定です。
明日は、@twinbee さんの「Elixir GenStage #1 入門」です