13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ElixirでSI開発入門 #2 Ectoで楽観的ロック

Last updated at Posted at 2018-05-03

(この記事は、「Elixir or Phoenix Advent Calendar 2017」の9日目です)

fukuoka.exのメンバーにより連載されています。

昨日は、@piacere さんの「ExcelでElixirマスター2回目:「列の抽出」と「Web表示」でした
今日は、実際のプロダクト開発のお話2回目です。

はじめに

Elixirで実際にプロダクト開発した経験からサンプルコードを交えて解説する本連載
基本となるデータベース排他制御の後編は、
Webサービスなどでより一般的な楽観的ロックです。

悲観的ロックと楽観的ロックに関する一般的な解説は例によってこちらの記事をお勧めします。
-> 排他制御(楽観ロック・悲観ロック)の基礎

本連載の前回記事はこちら
|> ElixirでSI開発入門 #1 Ectoで悲観的ロック


Ecto.Changeset.optimistic_lock()で楽観的ロック

前回の悲観的ロックでは、Ecto.Multiによるトランザクション制御とReop.lock()による更新ロックを組み合わせて実装する必要がありましたが、最新のEcto v2.2系では標準で楽観的ロックがサポートされています。

Ecto 2.2.10 > Ecto.Changeset > optimistic_lock

公式にサンプルコードまで用意されているのですが、phoenixで使う為には若干アレンジが必要です。


実装の前提

今回は以下の実装を例に考えます

  • コメントを投稿するモデルを実装する
  • 同時に複数のタブで更新した場合、後からの更新で編集内容が上書きされないように楽観的ロック制御を行う必要がある

また、以下の環境で実装しました

  • Elixir     v1.6.1
  • Phoenix v1.3.2
  • Ecto      v2.2.10
  • PostgreSQL v10.2

開発手順 

プロジェクト〜モデルの作成

PhoenixプロジェクトとDBを作成

> mix phx.new ecto_optimistic_lock_sample --no-brunch
> cd ecto_optimistic_lock_sample
> mix ecto.create

モデルを作成

> mix phx.gen.html Posts Comment comments name comment lock_version:integer

ルーティングを追加

lib/ecto_optimistic_lock_sample_web/router.ex

defmodule EctoOptimisticLockSampleWeb.Router do
  use EctoOptimisticLockSampleWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", EctoOptimisticLockSampleWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/comments", CommentController # <-- 作成したモデルのルーティングを追加
  end

end

楽観的ロックの実装を追加

作成したモデルのスキーマ実装を修正する
※追加するchangesetの実装は引数で呼び分けても関数名を変えてもどうちらでも良い
(Elixirの慣例としてここでは引数で呼び分ける形としている)

lib/ecto_optimistic_lock_sample/posts/comment.ex
defmodule EctoOptimisticLockSample.Posts.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :comment, :string
    field :lock_version, :integer, default: 1 # <-- ロック制御用のカウンターに初期値1を設定
    field :name, :string

    timestamps()
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [:name, :comment, :lock_version])
    |> validate_required([:name, :comment, :lock_version])
  end

  def changeset(comment, attrs, :update) do # <-- 更新時用のchangesetの実装を追加
    comment
    |> cast(attrs, [:name, :comment, :lock_version])
    |> validate_required([:name, :comment, :lock_version])
    |> optimistic_lock(:lock_version) # <-- Ecto.Changeset.optimistic_lock()を追加
  end

end

更新関数を修正する。
※スキーマにchangesetを追加する際にサンプル実装の様に第一引数で呼び分けてしまうと、デフォルトのupdate実装の様にパイプで処理を繋ぐことができなくなる為、第二引数以降を追加して呼び分けることをお勧めする。

lib/ecto_optimistic_lock_sample/posts/posts.ex
defmodule EctoOptimisticLockSample.Posts do
  @moduledoc """
  The Posts context.
  """

  import Ecto.Query, warn: false
  alias EctoOptimisticLockSample.Repo

  alias EctoOptimisticLockSample.Posts.Comment

  〜中略〜

  def update_comment(%Comment{} = comment, attrs) do
    comment
    |> Comment.changeset(attrs, :update) # <-- 更新用に追加したchangesetを使用する
    |> Repo.update()
  end

  〜中略〜
end

動かしてみる

マイグレーションを実行して、Phoenixサーバーを起動する。

> mix ecto.migrate
> mix phx.server

ブラウザから作成したページURLにアクセスし、コメントを新規登録する
http://localhost:4000/comments


一つ目のタブからコメントを編集モードで開く。


一つ目のブラウザで更新をSubmitする前に、二つ目のタブで同じコメントを編集し、Submitする。

先にSubmitした二つ目のタグでの更新が反映される
※この時lock_versionがインクリメントされていることが確認できる。


編集モードで開いておいた一つ目のタブでSubmitするとEcto.StaleEntryErrorが発生する。


これで楽観的ロック制御(つまりは先勝ち後負け)の実装が確認できた。
ちなみに、今回は入力項目にlock_versionをつけているので、ここでlock_version=2(二つ目のタブで更新後の値)を入力してしまえば二つ目のタブの更新内容を無視して無理やり更新することもできる。


Ecto.changeset.optimistic_lock()は引数で指定された項目の、更新する側が申告する値と現在保存されている値が一致しない場合に情報が古いと判断しているのだ。
※更新後の値より大きい値(例えばlock_version=3)を入力してもちゃんと叱ってくれる


まとめ

- 楽観的ロックはEcto.changeset.optimistic_lock()でサポートされている
- 対象となるスキーマに更新時用のchangesetの実装を追加する
- 更新処理で使用するchangesetを更新時用のものに変更する
- パイプを使う為、変数でchangesetを呼び分ける場合は第二引数以降で呼び分ける

いかがだったでしょうか。
2回に渡って排他制御の実装例をみてきました。

明日は、@twinbee さんの「ElixirのGenStageに入門する#2 バックプレッシャーを理解する」です

13
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?