業務システムを構築していると、データの状態遷移を管理することがあります。
例えば、下記のような状態遷移があった場合
「未承認」から「承認済」へと遷移するような操作が行われようとした場合は、
プログラム上できないように制御する必要が出てきます。
独自に状態を管理する制御を記載してもよいですが、
今回は、aasmというgemを使用して、状態遷移の管理を行ってみようと思います。
開発環境
Rails 5.2.0
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]
gemファイルに、以下の記述を追加します。
gem 'aasm'
企画モデルを作成し、状態を持たせます。
aasm用のモデルを作成します。
コマンドはrails generate aasm NAME [COLUMN_NAME]
NAME:モデル名 COLUMN_NAME:状態管理するカラム名(デフォルトはaasm_stateのようです )
実行結果
注意$ docker-compose exec app bundle exec rails generate aasm project status invoke active_record create db/migrate/20190201133201_aasm_create_projects.rb create app/models/project.rb invoke rspec create spec/models/project_spec.rb invoke factory_bot create spec/factories/projects.rb insert app/models/project.rb
ここで、作成したdb/migrate/20190201133201_aasm_create_projects.rbが
私の環境だと、クラス名が AASMCreateProjects という名前で作成されていて、
命名規約にあっていなくmigrateの 実行時にエラーになりました。
そのため、クラス名を以下のように変更しました。
AasmCreateProjects
projectモデルに必要な処理を記載します。
class Project < ApplicationRecord include AASM # 状態の説明 # 未承認(unapproved) # 承認中(pending) # 承認済(approved) # 却下(rejection) aasm :column => 'status' do state :unapproved, :initial => true state :pending, :approved, :rejection # 未承認=>承認中 event :check do transitions :from => :unapproved, :to => :pending end # 承認中=>承認済 event :approve do transitions :from => :pending, :to => :approved end # 承認中=>未承認 event :remand do transitions :from => :pending, :to => :unapproved end # 承認中=>却下 event :reject do transitions :from => :pending, :to => :rejection end end end
stateには、「状態」を記載します。initialで初期状態を設定
eventは、状態遷移させる際のイベントを定義します。
例:check では、未承認から、承認中に遷移させるイベントを定義しています。
rails consoleで実行してみます。
project = Project.new # 初期状態の確認 project.status => "unapproved"
指定した状態かどうか確認するには下記のメソッド使用します。
# 初期状態の確認 project.status => "unapproved" project.unapproved? => true project.pending? => false project.approved? => false project.rejection? => false
指定したイベントを実行できるか確認する際は、
may_XXX?のメソッドを使用します。(XXXはイベント名)
# 初期状態の確認 project.status => "unapproved" project.may_check? => true project.may_approve? => false project.may_remand? => false project.may_reject? => false
上記例では、未承認の状態で使用できるのは、
checkイベントだけのため、may_check?のみtrueとなっています。
状態遷移は、各イベントを実行すると行われます。
ここでは、未承認(unapproved)=>承認中(pending)に状態遷移させます。
# 初期状態の確認 project.status => "unapproved" # 状態の遷移(checkイベントを実行) project.check => true # 承認中になっていることを確認 project.status=> "pending"
状態遷移は、各イベントを実行すると行われます。
ここでは、未承認(unapproved)=>承認中(pending)に状態遷移させます。
# 初期状態の確認 project.status => "unapproved" # 状態の遷移(approveイベントを実行) project.approve # 例外が投げられる Traceback (most recent call last): 1: from (irb):36 AASM::InvalidTransition (Event 'approve' cannot transition from 'unapproved'.)
aasmで使用できる一部のコールバックを紹介します。
before_all_events
名前が示すように定義されているすべてのイベントの実行前に呼ばれます。
before
before_all_eventsの後に実行されます。 ここでは、checkイベントに記載します。
after
after_all_eventsの前に実行されます。 ここでは、checkイベントに記載します。
after_all_events
名前が示すように定義されているすべてのイベントの実行後に呼ばれます。 ※ここでは、エラー処理に関しては考慮しません。
class Project < ApplicationRecord include AASM # 状態の説明 # 未承認(unapproved) # 承認中(pending) # 承認済(approved) # 却下(rejection) aasm :column => 'status' do state :unapproved, :initial => true state :pending, :approved, :rejection before_all_events :log_event_start after_all_events :log_event_end # 未承認=>承認中 event :check do before do before_anything end after do after_anything end transitions :from => :unapproved, :to => :pending end # 承認中=>承認済 event :approve do transitions :from => :pending, :to => :approved end # 承認中=>未承認 event :remand do transitions :from => :pending, :to => :unapproved end # 承認中=>却下 event :reject do transitions :from => :pending, :to => :rejection end end def log_event_start puts "before_all_events changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})" end def before_anything puts "checkのbefore処理" end def after_anything puts "checkのafter処理" end def log_event_end puts "after_all_events changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})" end end
実行結果
※aasm.from_stateとaasm.to_stateはafterかafter_all_eventsでないと、# checkイベントを実行 project.check # 実行結果 before_all_events changing from to (event: check) checkのbefore処理 checkのafter処理 after_all_events changing from unapproved to pending (event: check) # 続けてremandイベントを実行 project.remand # 実行結果 before_all_events changing from unapproved to pending (event: remand) after_all_events changing from pending to unapproved (event: remand)
正しく状態を取得できないようです。(公式にも記載あり)
引用 During the transition’s :after callback (and reliably only then, or in the global after_all_transitions callback) you can access the originating state (the from-state) and the target state (the to state)
aasmには、特定の条件下でのみ状態遷移を許可したい場合に備えて、
Guardsという仕組みが備わっています。
ここでは、approveイベントにGuards を設定し、
特定の条件は、 some_condition? に記載します。(ここでは、false固定)
class Project < ApplicationRecord include AASM # 状態の説明 # 未承認(unapproved) # 承認中(pending) # 承認済(approved) # 却下(rejection) aasm :column => 'status' do state :unapproved, :initial => true state :pending, :approved, :rejection before_all_events :log_event_start after_all_events :log_event_end # 未承認=>承認中 event :check do before do before_anything end after do after_anything end transitions :from => :unapproved, :to => :pending end # 承認中=>承認済 event :approve do transitions :from => :pending, :to => :approved, :guard => :some_condition? end # 承認中=>未承認 event :remand do transitions :from => :pending, :to => :unapproved end # 承認中=>却下 event :reject do transitions :from => :pending, :to => :rejection end end # 何かしらの条件(ここでは、falseを固定で記載します) def some_condition? false end def log_event_start puts "before_all_events changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})" end def before_anything puts "checkのbefore処理" end def after_anything puts "checkのafter処理" end def log_event_end puts "after_all_events changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})" end end
実行結果
# 状態 project.status => "pending" # 承認中から承認済に状態遷移を行う project.approve # 実行結果 before_all_events changing from unapproved to pending (event: approve) Traceback (most recent call last): 1: from (irb):7 AASM::InvalidTransition (Event 'approve' cannot transition from 'pending'. Failed callback(s): [:some_condition?].)
aasmの基本だけ記載しましたが、いかがでしたでしょうか?
コードを見たときにどの状態がどの状態に遷移できるか?がわかるような構造になっているので、
読みやすく、不正な状態遷移も防ぐための仕組みも備わっているので、独自で実装するよりも良いかと思いました。