AASM を使ってみた。主に Callback。

STATESMAN を使ってみたかったのだけど、使いたかったシーンが state を持ったテーブル1つだったので、ちょっと STATESMAN だと冗長だったので断念。

AASM

github.com

The Ruby Toolbox - State Machines

state machine カテゴリでは 2 位で、今でも開発は行われているようなので良さそうに見える。

日本語の解説記事も発見。

使ってみる

対象

ActiveRecord Enum を使って status というカラムを列挙型として使っているモデルがあるとします。

class Task < ActiveRecord::Base
  enum status: {opened: 0, assigned: 1, rejected: 2, finished: 3}
end

このステータスは、それぞれ遷移する順序があるという場合です。

state の設定

https://github.com/aasm/aasm#activerecord-enums

AASM は標準で ActiveRecord Enum を上手く扱えます。ポイントは column: enum名 と指定するところです。今回は enum が integer のカラムなので enum: true というオプションを省略できる。

class Task < ActiveRecord::Base
  include AASM

  enum status: {opened: 0, assigned: 1, rejected: 2, finished: 3}

  aasm column: :status do
    state :opened, initial: true
    state :assigned
    state :rejected
    state :finished
  end
end

event と transition の指定

class Task < ActiveRecord::Base
  include AASM

  enum status: {opened: 0, assigned: 1, rejected: 2, finished: 3}

  aasm column: :status do
    state :opened, initial: true
    state :assigned
    state :rejected
    state :finished

    event :assign do
      transitions from: :opened, to: :assigned
    end

    event :reject do
      transitions from: %i(opened assigned), to: :rejected
    end

    event :finish do
      transitions from: :assigned, to: :finished
    end
  end
end

ここは特に説明不要ですかね。

Callback を指定してみる

https://github.com/aasm/aasm#callbacks

以下の順序と関連を押さえておけばなんとなく大丈夫かと。

begin
  event           before
  event           guards
  transition      guards
  old_state       before_exit
  old_state       exit
  transition      after
  new_state       before_enter
  new_state       enter
  ...update state...
  event         success             # if persist successful
  old_state       after_exit
  new_state       after_enter
  event           after
rescue
  event           error
end

aasm/callbacks_spec.rb at master · aasm/aasm · GitHub

Callback のテストを見ると、順序、設定場所、渡ってくる引数がわかりやすい。

class Task < ActiveRecord::Base
  include AASM

  enum status: {opened: 0, assigned: 1, rejected: 2, finished: 3}

  aasm column: :status do
    state :opened, initial: true
    state :assigned
    state :rejected, after_enter: :after_enter_rejected
    state :finished, after_enter: :after_enter_finished

    event :assign do
      transitions from: :opened, to: :assigned do
        after do |user|
          self.user = user
          self.assigned_at = Time.zone.now
        end
      end
    end

    event :reject do
      transitions from: %i(opened assigned), to: :rejected
    end

    event :finish do
      transitions from: :assigned, to: :finished
    end
  end

  def after_enter_rejected
     # reject 時の処理
  end

  def after_enter_finished
    # finish 時の処理
  end
end

今回の特殊な点として、assign 時に一緒にアサインされたユーザーと、アサイン時刻を記録したかったので、transition に対する after callback を指定した。

transition after callback は名前は after だが、status の値の更新前に実行されるので、結果的に UPDATE 文が1つにできる。もちろん、status の更新が不可能な状態ならば実行されない。

task = Task.create
task.opened? # => true
task.assign!(current_user) 

task.assigned? # => true
task.user # => current_user 

# ダメな場合
task.update(status: 3, user_id: nil)
task.may_assign? # => false
task.assign!(current_user) # => AASM::InvalidTransition
task.user # => nil

Transaction 使えばもっとキレイにかけるのかなぁ。 https://github.com/aasm/aasm#transaction-support

結局もう一度 save するのか。

aasm/validator.rb at 44a17aa5357d60e20cbd1a5da4a27063e113ffa2 · aasm/aasm · GitHub

#

今回は Guards に関しては利用用途が無かったのだが、いろいろできそうなのでまた調べたい。