Ruby on Railsで状態を扱うStatefulEnum gem
StatefulEnumというgemを紹介されたので使ってみました。他のgemと比較して違いや使い心地も伝えたいと思います。Railsでステータス管理したいなら https://t.co/uP5W7eFsU4 がシンプルで、コードも読める量でおすすめです #omotesandorb
— 神速 (@sinsoku_listy) 2019年2月7日
以前書いた類似gemの紹介記事はこちら ngmt83.hatenablog.com
例
SNSのアカウントを想定したAccountモデルを例に実装します。
リポジトリはこちらです。 github.com
クラス図
状態遷移図
クラス図や状態遷移図はPlantUMLで作成しています。 ngmt83.hatenablog.com
ActiveRecord::Enumで状態を設定する
Accountモデルにinteger
のstatus
カラムを追加
ActiveRecord::Enumでは状態を表すinteger
型のカラムが必要です。カラムがなければマイグレーションで作成しましょう。railsコマンドであれば次のコマンドでカラムが追加できます。
rails g migration AddColumnToAccount status:integer
実際に作成したマイグレーションファイルはこちらです。
class AddColumnToAccount < ActiveRecord::Migration[5.2] def change add_column :accounts, :status, :integer, null: false, default: 0 add_index :accounts, :status end end
status
を持たないものは存在しない前提でnull: false
と初期状態として0を指定し、status
による絞り込みを想定してインデックスを追加しています。*1
状態を定義
次のような記述方法で状態を定義します。
enum status: { registered: 0, active: 1, suspended: 2, banned: 3, inactive: 4 }
ActiveRecord::Enumはステータスを整数に変換してDBに保存するため、どのステータスが整数で何にあたるのかを定義しています。 *2
この定義により、次のようなメソッドが使用可能になります。
# account は Accountのインスタンス account.suspended! # statusをsuspendedに変更する account.suspended? # statusがsuspendedかどうかを真偽値で返す account.status # statusを文字列で返す
課題
ActiveRecord::Enumのみでは次のような課題が存在します。
- 状態遷移を表すメソッド(
hoge!
)の名前が動詞節にならない(hoge
は状態を表す単語であり過去分詞形か形容詞) - 状態遷移の前と後の状態をわかりやすく制限できない
- 状態遷移の条件を設定できない
これらを解決できるのがStatefulEnum gemです。
StatefulEnum gemを導入する
gemインストール
Gemfileに下記の通り記述し、
gem 'stateful_enum'
bundlerでインストールしましょう。
bundle install
状態遷移の定義
状態遷移を伴う操作をevent
とし、transitions
で状態遷移を記述します。
has_one :active_suspension, lambda { where(removed_at: nil) }, class_name: 'AccountSuspension' # 中略 event :suspend do before do suspensions.create! end transition :active => :suspended, unless: -> { active_suspension } end
このコードを解説すると次のとおりです。
「active
状態で、active_suspension
がfalseとなる場合のみsuspend
というイベントを実行可能。実行時すると関連モデルsuspension
を生成し、suspended
状態に遷移する。」
定義されるメソッド
# account は Accountのインスタンス account.suspend # suspendイベントを実行する。事前条件を満たさない場合falseを返す account.suspend! # suspendイベントを実行する。事前条件を満たさない場合<RuntimeError: Invalid transition>をraiseする account.can_suspend? # suspendを実行できるかどうか(事前条件の結果)を真偽値で返す account.suspend_transition # suspend時に遷移する状態を返す
事前条件は次のとおり
account.active? && !account.active_suspension
使えなくなるメソッド
account.suspended! # NoMethodError
標準のEnumが定義する状態遷移メソッドは使用できなくなっています。動詞節のメソッドで状態遷移したいがためにStatefulEnumを利用しているので、むしろありがたいです。
比較
次の2つのgemを比較しました。
StatefulEnum gemのメリット
- Enumを拡張したのみでシンプルなため、Rails wayから外れにくい
- gemのコード量が少ない
- amatsuda (Akira Matsuda) · GitHubさんがRuby, Railsコミッターであり、継続的なサポートが期待できる*4
AASM gemのメリット
- モデル内で状態の初期値を設定できる
include AASM
したモデルでのみ有効化できる- 状態を
string
で保持することもできる - コールバック・ロック・トランザクションなど豊富な詳細設定が可能
所感(使い分け方)
どちらも使い心地はよかったです。
とはいえ、基本方針としてRails wayに乗って継続的に開発するために、可能な限りシンプルなgemのみを使用した方がいいと考えているため、豊富な詳細設定が不要であればStatefulEnumがいいかと思います。また、コールバック・ロック・トランザクションを細かく取り扱おうとするとモデルにすべておさまるとは思えません。*5そのため、細かく取り扱う場合はドメインロジックを別クラスに移譲することになると思うので、詳細設定が必要でもシンプルなStatefulEnumでいいのではないかと思います。
例外として、状態をstringで保持したい場合にはStatefulEnumでは対応できないため、AASMを用いていいかと思います。*6
*1:https://github.com/nagamoto/state-machine-sample/blob/a8d701446476f27ad7ac2ac54ecf8c4a8fcf937c/db/migrate/20190211150628_add_column_to_account.rb
*2:https://github.com/nagamoto/state-machine-sample/blob/05c16fe03276b80e76dd857e12bea12927de9923/app/models/account.rb#L9
*4:日本人エンジニア的には何かあったときに聞きやすい
*5:ファットモデルは避けたい
*6:DB上でも状態がわかりやすいのは大きなメリット。その代わりパフォーマンスが多少落ちる可能性はある