AASM を使ってみた。主に Callback。
STATESMAN
を使ってみたかったのだけど、使いたかったシーンが state を持ったテーブル1つだったので、ちょっと STATESMAN
だと冗長だったので断念。
AASM
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 に関しては利用用途が無かったのだが、いろいろできそうなのでまた調べたい。