LoginSignup
218
198

More than 3 years have passed since last update.

AASM - classの状態遷移をスマートに実装するためのgem (Ruby, Active Record, Sequel, Mongoid)

Last updated at Posted at 2014-04-23

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 というクラスには sleepingrunningcleaningという3つの状態があると定義した。この状態を遷移させるイベントは同じく3つで runcleansleepだ。

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

 上記の場合、状態は sleepingrunningの2つである。ここでは running状態から sleeping状態に遷移する前(before_enter)にdo_something が実行される。また、notify_somebodyrun(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以上ではstateenum型で記述することができる。

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]
218
198
2

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
218
198