🤖

AASM の状態遷移時に関連属性を更新する

2022/04/22に公開

Leaner 開発チームの黒曜(@kokuyouwind)です。

ゴールデンウィーク明けの 5 月 10 日(火) に開催される AWS Starup Community スタートアップ事例祭り 〜監視・モニタリング・セキュリティ編〜 で登壇させていただくので、ぜひ聞きに来てください!

https://aws-startup-community.connpass.com/event/241721/

ネタバレですが「AWS Copilot CLI を使っておけば VPC とかセキュリティルールは大体いい感じになるんじゃない?」みたいな話をする予定です。

さて、今回はステートマシンを表現する AASM gem を導入した話と、状態遷移時に付随する属性を更新するための Tips をまとめます。

AASM gem について

導入経緯

アプリケーション内で申請機能を実装する際、ステートマシンを使いたくなるシチュエーションがありました。大まかには「ユーザーが提出した申請に対し、承認者が承認か却下のいずれかのアクションを取れる」という機能です。[1]

これを表したものが以下の状態遷移図です。

この際、「提出前の申請は承認や却下できない」「一度承認や却下された申請はいかなる操作も受け付けない」などの制約が存在することに注意が必要です。

こうした処理を状態ごとに分岐処理で書くとコードが複雑になるため、ステートマシンの定義をそのまま記述できるAASMを利用しています。[2]

https://github.com/aasm/aasm

申請のステートマシン定義

AASM を利用すると、申請のステートマシンを以下のように定義できます。

class Request
  include AASM
  enum status: [ :draft, :reviewing, :approved, :rejected ]

  aasm column: :status, enum: true do
    state :draft, initial: true, display: '下書き'
    state :reviewing, display: '審査中'
    state :approved, display: '承認済み'
    state :rejected, display: '却下済み'

    event :submit { transitions from: :draft, to: :reviewing }
    event :approve { transitions from: :reviewing, to: :approved }
    event :reject { transitions from: :reviewing, to: :rejected }
  end
end

状態遷移図に出てくる 4 種類の状態と 3 種類の遷移がそのまま DSL で定義されていますね。なお AASM では statedisplay キーワード引数を渡しておくことで human_state から参照できます。

この Request は以下のように利用できます。

request = Request.new

puts request.aasm.human_state #=> 下書き

request.submit
puts request.aasm.human_state #=> 審査中

request.approve
puts request.aasm.human_state #=> 承認済み

request.reject #=> raise AASM::InvalidTransition

イベント名のメソッドを呼び出すだけで状態遷移が行われ、遷移できない場合は例外が発生します。便利ですね!

イベントに追加の属性を渡したい

ところで申請時に「XXX の件です」とコメントを入れてあげられると親切そうです。これを実現するため Request に属性を増やし、申請提出時に comment を保存することにしましょう。

利用する側としては、以下のようなインターフェイスで呼び出せると良さそうです。

request = Request.new

# 新規リクエストをコメントとともに提出する
# (永続化のために bang メソッドを呼び出している)
request.submit!(comment: 'よろしくおねがいします!')

# 永続化確認のためリロード
request.reload
puts request.comment #=> 「よろしくおねがいします!」になっていてほしい

しかしながら、 submit! は AASM が定義しているメソッドなので直接手を加えることはできません。

AASM Callbacks

こうした処理は AASM の Callbacks を利用すれば実現できます。

class Request
  include AASM
  enum status: [ :draft, :reviewing, :approved, :rejected ]

  aasm column: :status, enum: true do
    # 省略
    event :submit, before: Proc.new { self.comment = _1[:comment] } do
      transitions from: :draft, to: :reviewing
    end

このように event :submit の定義時に before Callback 内で comment を設定することで、 status の更新と合わせて永続化されます。コールバックに渡す Proc にはイベントメソッドの引数がそのまま渡るため、上記のように取り出すことができます。

Callback の種類は Lifecycle の節にまとめられています。この一覧のうち update state より前に属性を更新しておくことで AASM の状態更新と合わせて UPDATE クエリが発行されて永続化されます。今回は event についてのコールバックなので beforeguards のいずれかを選択する必要がありますが、 guards は遷移可否を返すための Callback なので、副作用が目的である今回は before を使うのが適切ですね。

なお、上述の before の定義を以下のように書き換えてしまうと正しく動かないため注意が必要です。

# これは動かない!
event :submit, before: Proc.new { |comment:| self.comment = comment } do

これは Ruby 3.0 でキーワード引数が位置引数から分離されたためで、以下の Issue でも取り上げられています。

https://github.com/aasm/aasm/issues/672

まとめ

AASM を利用することでステートマシンを綺麗に実装できますが、状態を表す属性以外を更新するときに困る事がありました。

AASM Callback を使うとこういった処理が綺麗に扱えることがわかったので、今後も使い倒していきます!

宣伝

Leaner Technologies ではステートマシンを使いこなしたいエンジニアを募集しています!

https://careers.leaner.co.jp/engineering

脚注
  1. 実際には差し戻しや多段承認を考慮したもう少し複雑なものを作っていますが、ここでは例として単純化しています。 ↩︎

  2. ステートマシンを表せる gem は他にも何種類か存在します。今回はRails で使える StateMachine Gem 3 つをしらべてみたなどを参考に、 ActiveRecord enum を使いたかったので AASM を選定しました。 ↩︎

リーナーテックブログ

Discussion