AASM - Ruby state machines
ここを参考にしました。
https://github.com/aasm/aasm/blob/master/README.md
https://github.com/aasm/aasm/blob/master/CHANGELOG.md
※2014年に本記事を投稿してから、細々と継続的にアクセスがあるようです。AASMの仕様は投稿当時と最新版でもそれほど変わっていないようですが、ご利用にあたっては必ず上記GitHubのREADMEとCHANELOGの確認をお願いします。
概要
AASMはRubyのclassに有限オートマトンを追加するライブラリ。2014年10月現在でも精力的に開発が進められている。以下の内容は gem version ~~~3.4.0~~~ 4.9.0 に準拠する。
http://techblog.heartrails.com/2011/02/rails-aasm.html
↑のブログ記事でAASMを見かけたヒトも多いと思うが、現在ではメソッド名などの仕様が変更されているので注意。
さらにいうと、version 4.0.0 でDSLも変更になったので注意。
用途
オブジェクトの状態遷移管理は大事。それを簡単に実装できるgemがAASM。
http://www.slideshare.net/tricknotes/rails-possiblestory
↑にあるような、「レコードを論理削除したら、機能がつかえなくなっちゃいました〜」みたいなポカを、オブジェクトの状態・遷移・遷移条件を明確にすることで防げる。(オブジェクトの状態とRDBのレコードの状態は別物、という話題はさておき)
インストール
RubyGems.org から手動で###
% gem install aasm
Bundler を使って###
# Gemfile
gem 'aasm'
自分でビルドする
% rake build
% sudo gem install pkg/aasm-x.y.z.gem
Generators
After installing Aasm you can run generator:
% rails generate aasm NAME [COLUMN_NAME]
NAME には任意のModel名を入力する。COLUMN_NAME は optional です(デフォルトは 'aasm_state')。
This will create a model (if one does not exist) and configure it with aasm block.
For Active record orm a migration file is added to add aasm state column to table.
使い方
##状態・イベント・遷移を定義する
状態遷移を管理したい Class に AASM モジュールを include して state(状態) と event(イベント) と **transitions(遷移)**を定義するだけでよい。
下記では、Job
というクラスには sleeping
、running
、cleaning
という3つの状態があると定義した。この状態を遷移させるイベントは同じく3つで run
、clean
、sleep
だ。
class Job
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
end
event :sleep do
transitions :from => [:running, :cleaning], :to => :sleeping
end
end
end
この定義により Job
クラスのインスタンスに以下のようなpublicメソッドが追加される。追加されるメソッドは3種類で、インスタンスの状態を確認するもの、イベントが実行可能か確認するもの、イベントを実行するものの3種類だ。
job = Job.new
job.sleeping? # => true
job.may_run? # => true
job.run
job.running? # => true
job.sleeping? # => false
job.may_run? # => false
job.run # => raises AASM::InvalidTransition
**event(イベント)**が実行不可にも関わらず実行をした場合(上記のjob.run
の参照)、AASM::InvalidTransition
が発生する。ただし下記のようにaasm メソッドの第一引数で:whiny_transitions => false
することで、例外の代わりにboolean
型の戻り値を受け取ることも出来るようになる。
class Job
...
aasm :whiny_transitions => false do
...
end
end
job.running? # => true
job.may_run? # => false
job.run # => false
イベント実行時にブロックを渡す事が出来る。このブロックは**transitions(遷移)**が成功するときのみ実行される。
job.run do
job.user.notify_job_ran # Will be called if job.may_run? is true
end
遷移にコールバックを追加する
**transitions(遷移)**にコールバックを定義する事が出来る。コールバックは「特定の状態に遷移した場合のみ」など、任意の基準を満たした場合に実行される。
class Job
include AASM
aasm do
state :sleeping, :initial => true, :before_enter => :do_something
state :running
after_all_transitions :log_status_change
event :run, :after => :notify_somebody do
before do
log('Preparing to run')
end
transitions :from => :sleeping, :to => :running, :after => Proc.new {|*args| set_process(*args) }
transitions :from => :running, :to => :finished, :after => LogRunTime
end
event :sleep do
after do
...
end
error do |e|
...
end
transitions :from => :running, :to => :sleeping
end
end
def log_status_change
puts "changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})"
end
def set_process(name)
...
end
def do_something
...
end
def notify_somebody(user)
...
end
end
class LogRunTime
def call
log "Job was running for X seconds"
end
end
上記の場合、状態は sleeping
と running
の2つである。ここでは running
状態から sleeping
状態に遷移する前(before_enter
)にdo_something
が実行される。また、notify_somebody
は run
(from sleeping to running) イベントの完了後に呼ばれる。
AASM will also initialize LogRunTime and run the call method for you after the transition from runnung to finished in the example above. You can pass arguments to the class by defining an initialize method on it, like this:
class LogRunTime
# optional args parameter can be omitted, but if you define initialize
# you must accept the model instance as the first parameter to it.
def initialize(job, args = {})
@job = job
end
def call
log "Job was running for #{@job.run_time} seconds"
end
end
また、イベントにパラメーターを渡す事も出来る。下記の場合は set_process
が呼ばれ引数 :defragmentation
が渡される。
job = Job.new
job.run(:running, :defragmentation)
もしイベントの処理中にエラーが発生した場合、エラーはrescueされ :error
コールバックが実行される。:error コールバックでは、処理をおこなうことも、例外を発生させることもできる。
:on_transition
コールバックの中では遷移元の状態(aasm.from_state
)と遷移先の状態(aasm.to_state
)両方にアクセスできる。
def set_process(name)
logger.info "from #{aasm.from_state} to #{aasm.to_state}"
end
また、コールバックの実行中に、aasm.current_event
を実行することで、発火中のイベント名を取得することが出来る
# 上記のJob#do_somethingに追記
#do_somethingはsleepイベントを実行する前に呼ばれる。
def do_something
puts "triggered #{aasm.current_event}"
end
job = Job.new
job.sleep # => triggered :sleep
job.sleep! # => triggered :sleep!
遷移にガードを追加する
特定の条件を満たした場合にのみ状態遷移を許可するようにしたいとき、遷移に「ガード」を定義する事が出来る。ガードは遷移の実行前に実行される。ガードがfalseを返した場合、遷移は拒否される。(AASM::InvalidTransition
例外が発生するか、 false
を返す)
class Job
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
end
event :sleep do
transitions :from => :running, :to => :sleeping, :guard => :cleaning_needed?
end
end
def cleaning_needed?
false
end
end
job = Job.new
job.run
job.may_sleep? # => false
job.sleep # => raises AASM::InvalidTransition
下記のように複数のガードを定義する事も出来る。
def walked_the_dog?; ...; end
event :sleep do
transitions :from => :running,
:to => :sleeping,
:guards => [:cleaning_needed?, :walked_the_dog?]
end
すべての遷移にガードを定義したい場合は、下記のようにする。
event :sleep,
:guards => [:walked_the_dog?] do
transitions
:from => :running,
:to => :sleeping,
:guards => [:cleaning_needed?]
transitions
:from => :cleaning,
:to => :sleeping
end
##O/Rマッパー連携
ActiveRecord
AASM はActiveRecord においてデータベースでのオブジェクト永続化を可能にしている。
class Job < ActiveRecord::Base
include AASM
aasm do # default column: aasm_state
state :sleeping, :initial => true
state :running
event :run do
transitions :from => :sleeping, :to => :running
end
event :sleep do
transitions :from => :running, :to => :sleeping
end
end
end
objectをauto-saveしたりしなかったりできる。
job = Job.new
job.run # not saved
job.run! # saved
saveするときには Job
classならJob
クラスのvalidationが走るが、状態については下記のように記述する事でvalidationをスキップすることができる。
class Job < ActiveRecord::Base
include AASM
aasm :skip_validation_on_save => true do
state :sleeping, :initial => true
state :running
event :run do
transitions :from => :sleeping, :to => :running
end
event :sleep do
transitions :from => :running, :to => :sleeping
end
end
end
また、Rails 4.1以上ではstate
をenum型で記述することができる。
class Job < ActiveRecord::Base
include AASM
enum state: {
sleeping: 5,
running: 99
}
aasm :column => :state, :enum => true do
state :sleeping, :initial => true
state :running
end
end
enum
を使用したい場合、aasm
の引数として:enum => true
を渡すこと。もしDBのカラムの型がIntegerの場合(Railsでenumを使用した場合、大抵そうであるはずだが)は、:enum => true
は不要である、もし、何らかの問題があって enumを使用させたくない場合、aasm
の引数で :enum => false
を渡せばよい。
Sequel
AASM は Sequel に対応している。
ただし、Sequel 向けの機能は、automatic-scopesがまだ実装されていないなど、ActiveRecord むけの機能と比較してまだ不十分なところがある。
class Job < Sequel::Model
include AASM
aasm do # default column: aasm_state
...
end
end
Mongoid
AASM は Mongoidを利用することでMongodb にも対応している。ただし、利用の場合は Mongoid::Document
を AASMより前にincludeする必要がある。
class Job
include Mongoid::Document
include AASM
field :aasm_state
aasm do
...
end
end
トランザクション
AASMは バージョン 3.0.13 からActiveRecordのtransactionsに対応している。そのため、状態遷移の失敗などが発生した場合、データベースレコードへのすべての変更はロールバックされる。トランザクションがコミットされた後にだけなんらかの処理を実施したい場合は、after_commit
コールバックを利用する。
class Job < ActiveRecord::Base
include AASM
aasm do
state :sleeping, :initial => true
state :running, :after_commit => :notify_about_running_job
event :run do
transitions :from => :sleeping, :to => :running
end
end
def notify_about_running_job
...
end
end
AASMが作成するカラムの名称を変更する
AASMはデフォルトでは状態の管理に aasm_state
というカラムを使う。カラム名称は :column
で定義する事が出来る。
class Job < ActiveRecord::Base
include AASM
aasm :column => 'my_state' do
...
end
end
注意すべきは、カラム名称がなんであろうと、マイグレーションへのカラムの追加を忘れない事。その際の型は string
とすること。
class AddJobState < ActiveRecord::Migration
def self.up
add_column :jobs, :aasm_state, :string
end
def self.down
remove_column :jobs, :aasm_state
end
end
##オートマチック・スコープ
AASM はmodelの各 **state(状態)**と同名の scope メソッドを自動作成する。
class Job < ActiveRecord::Base
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :cleaning
end
def sleeping
"This method name is in already use"
end
end
class JobsController < ApplicationController
def index
@running_jobs = jobs.running
@recent_cleaning_jobs = jobs.cleaning.where('created_at >= ?', 3.days.ago)
# @sleeping_jobs = jobs.sleeping #=> "This method name is in already use"
end
end
scopeが不要に場合は、下記のように記述する。
class Job < ActiveRecord::Base
include AASM
aasm :create_scopes => false do
state :sleeping, :initial => true
state :running
state :cleaning
end
end
インスペクション
クラスにどのような状態、イベントが定義されているか調査するメソッドも用意されている。
job = Job.new
job.aasm.states.map(&:name)
=> [:sleeping, :running, :cleaning]
job.aasm.states(:permissible => true).map(&:name)
=> [:running]
job.run
job.aasm.states(:permissible => true).map(&:name)
=> [:cleaning, :sleeping]
job.aasm.events
=> [:run, :clean, :sleep]