ElixirでSI開発入門 #1 Ectoで悲観的ロック

（この記事は、「[Elixir or Phoenix Advent Calendar 2017](https://qiita.com/advent-calendar/2017/elixir-or-phoenix "Elixir or Phoenix Advent Calendar 2017")」の4日目です）

昨日は、@piacere さんの[「ExcelからElixirをマスター#1：行の「並べ替え」と「絞り込み」](https://qiita.com/piacere/items/6714e1440e3f25fb46a1)でした
今日は、実際のプロダクト開発のお話です。

# はじめに

Elixirで本格的に開発を初めて２ヶ月がたった。
fukuoka.exでの活動だけでなく、実際のプロダクト開発で実装した経験を元に
SI的なプロダクト開発での実装パターンをElixirでどう実現するか書き記していこうと思う

第１回目はシステム開発の基本　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 にアクセス
<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/bacb10ad-3f3e-4e70-4b50-2d6694220a55.png" width=50% >

---

Userを登録する
http://localhost:4000/users

<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/791c4a10-b64f-5144-f297-fa0f13207d7b.png" width=50% >

<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
<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/f650e1d5-a8fe-979d-ae36-968dc1d01fef.png" width=50% >

<img src="https://qiita-image-store.s3.amazonaws.com/0/243163/4081be1b-35c6-dc79-ae36-3b5858106bcb.png" width=50% >

<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 
```

```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 入門](https://qiita.com/twinbee/items/12a61863ceef794996e0)」です