どうもこんにちは。
コード理解を進めている中でAASMというライブラリが何者なのか知る必要がありそうなので、調べてみました。
AASMについて
ChatGPTに聞いてみたところ、以下のような返答が返ってきました。
AASM(Acts As State Machine)
は、オブジェクトの状態を管理し、状態間の遷移を定義するためのRubyライブラリです。このライブラリを使用すると、モデルの状態遷移を宣言的に記述でき、コールバックや条件付き遷移などの高度な機能を利用することができます。
ちょっとよくわからなかったですが、よく調べてみると、
データの状態を変更する前と後で明示的に処理を分けることができてメソッド化できるライブラリということです。
これは実際にコードを書いて理解するのがわかりやすそうです。
modelを用意
以下のようにテーブル,モデルを用意します。
contentsテーブル
カラム名 | 型 |
---|---|
id | integer |
title | string |
status | integer |
content | text |
class Content < ApplicationRecord
include AASM
# statusカラムはenumを定義
enum status: {
initial: 1, # 初期状態
pending: 2, # 未承認
approved: 3, # 承認
rejected: 4, # 却下
draft: 5 # 下書き
}
# AASMを使用してイベントを定義
aasm column: :status, enum: true do
# 状態の定義(AASMを使用するにはこれが必要です)
state :initial, initial: true
state :pending, :approved, :rejected, :draft
# イベントメソッドを定義
## 下書き状態で保存する
event :to_draft do
# ステータスは「初期状態」または「下書き」から「下書き」への遷移のみ受け付ける
transitions from: %i[initial draft], to: :draft
end
## 未承認状態から承認状態へ保存する
event :to_pending do
# ステータスは「未承認」から「承認」への遷移のみ受け付ける
transitions from: :pending, to: :approved
end
## 下書き状態から承認状態へ変更
event :draft_to_approved do
# ステータスは「下書き」から「承認」への遷移のみ受け付ける
transitions from: :draft, to: :approved
end
end
end
補足
カラムの定義
aasm column: :status, enum: true do
部分のcolumn
を定義することを忘れてしまった場合、aasm_state
というカラムがメモリ上で自動的に作成され、メモリ上での状態遷移が実行されます。
ただし、メモリ上での状態遷移のため、画面をリロードすると初期状態に戻ります。
状態の定義
state
を使用した状態の定義は忘れずに記述してください。
あくまでもAASMは「状態遷移」ライブラリなので、state
を定義していないと正常に動作しません。
Railsコンソールで試してみる
# 仮のコンテンツを作成
pry(main)> content = Content.create(title: 'テストコンテンツ', status: 'initial', content: '内容だよ')
# コンテンツがDBに登録されていることを確認
pry(main)> Content.last
=> < id: 1, title: 'テストコンテンツ', status: 'initial', content: '内容だよ' ... >
# initial→draftへ遷移
pry(main)> content.to_draft
# DBに保存
pry(main)> content.save
上記の例では、saveメソッドを明示的に呼び出してデータを保存しましたが、to_draftのあとに「!」をつければ自動的にDBに変更が反映されます。
# 仮のコンテンツを作成
pry(main)> content = Content.create(title: 'テストコンテンツ_2', status: 'initial', content: '内容2だよ')
# コンテンツがDBに登録されていることを確認
pry(main)> Content.last
=> < id: 2, title: 'テストコンテンツ_2', status: 'initial', content: '内容2だよ' ... >
# initial→draftへ遷移 & DBに保存
pry(main)> content.to_draft!
今回はRailsコンソールで試してみましたが、コントローラで実行することも可能です。
AASMで使用できる便利なオプション
よく使われるであろうオプションを一部紹介します。
- before
- after
- after_commit
モデルに定義します。
class Content < ApplicationRecord
include AASM
# statusカラムはenumを定義
enum status: {
initial: 1, # 初期状態
pending: 2, # 未承認
approved: 3, # 承認
rejected: 4, # 却下
draft: 5 # 下書き
}
# AASMを使用してイベントを定義
aasm column: :status, enum: true do
# 状態の定義
state :initial, initial: true
state :pending, :approved, :rejected, :draft
# イベントメソッドを定義
## 下書き状態で保存する
event :to_draft, before: :log_start, after: :log_finish do
# ステータスは「初期状態」または「下書き」から「下書き」への遷移のみ受け付ける
transitions from: %i[initial draft], to: :draft
end
## 未承認状態から承認状態へ保存する
event :to_pending do
# ステータスは「未承認」から「承認」への遷移のみ受け付ける
transitions from: :pending, to: :approved, after_commit: :send_mail_pending
end
end
def log_start
Rails.logger.info "状態遷移開始: #{Time.Current}"
end
def log_finish
Rails.logger.info "状態遷移終了: #{Time.Current}"
end
def send_mail_pending
title = self.title
# メール送信メソッド(だいたいapp/mailers/notification_mailer.rbとかに定義されている)
mail_content = '承認が必要なコンテンツが登録されました。'
NotificationMailer.send_mail(title, mail_content)
end
end
上記の例では、to_draft
メソッドが実行された前にlog_start
が実行され、実行された後にlog_finish
メソッドが実行されます。
また、to_pending
メソッドが実行されてDBにデータが保存された後でsend_mail_pending
メソッドが実行されます。
ただし、after_commit
として定義されているので、to_pending!
として実行されるか明示的にsave
やupdate
が実行された時でないと実行されません。
引数も渡せる
AASMでは引数も渡せます。以下のようにto_draft
メソッドが実行されるとします。
@content.to_draft!(current_user)
この時、to_draft
メソッドにはself
である@content
と引数であるcurrent_user
が渡されます。
引数は以下のようにbefore
やafter
などで使用することができます。
class Content < ApplicationRecord
include AASM
# statusカラムはenumを定義
enum status: {
initial: 1, # 初期状態
pending: 2, # 未承認
approved: 3, # 承認
rejected: 4, # 却下
draft: 5 # 下書き
}
# AASMを使用してイベントを定義
aasm column: :status, enum: true do
# 状態の定義
state :initial, initial: true
state :pending, :approved, :rejected, :draft
# イベントメソッドを定義
## 下書き状態で保存する
event :to_draft do
before do |user|
log_start(self.title, self.status, user.name)
@before_status = self.status
end
after do |user|
log_finish(self.title, @before_status, self.status, user.name)
end
# ステータスは「初期状態」または「下書き」から「下書き」への遷移のみ受け付ける
transitions from: %i[initial draft], to: :draft
end
end
def log_start(title, status, user_name)
Rails.logger.info "#{Time.Current.strftime('%y/%m/%d %H:%M:%S')}にコンテンツ「#{title}」が#{user_name}によって変更されます。"
end
def log_finish(title, before_status, status, user_name)
Rails.logger.info "#{Time.Current.strftime('%y/%m/%d %H:%M:%S')}にコンテンツ「#{title}」が#{user_name}によって#{before_status}から#{status}へ変更されました。"
end
end
上記のように書くことで引数を使用することができます。
また、beforeブロック内でインスタンス変数として状態遷移前の状態を保存しておくと、afterブロックなどの他のブロックでも遷移前のデータを使用することができます。
まとめ
AASMがわかりやすい人には、使いやすいGemだと思いますが、使いづらい人はモデルやコントローラーでガチガチにコードを記述してもいいかもしれないですね。
以上