Railsのenumを使いこなす方法(翻訳)
Railsのenumについて
enum(enumeration: 列挙)は、名前を整数の定数に割り当てるのに使われるデータ型です。名前は言語の定数として振る舞う識別子なので、整数を直に扱う場合よりもプログラムの読みやすさとメンテナンス性が向上します。
ActiveRecord::Enum
はRails 4.1で導入されました。enumの属性値はデータベース内の整数に対応付けられますが、クエリでは名前で参照できます。enumを使うと、データのステートを非常に高速に変更できるようになります。enumはRailsで手軽に利用可能で、enumが提供する動的メソッドによって開発時間を大幅に短縮できます。
データベースにenum用のカラムを作成する
Railsのモデルでは、テーブルにinteger型のカラムを追加するというかなりシンプルな形でenumを追加できます。
ここで、Postモデルを持つRailsアプリケーションを考えてみましょう。1件のpostにはdraft
(下書き)、published
(公開中)、archived
(アーカイブ)、 trashed
(ゴミ箱)というステートがあるとします。こうしたステートをPostのテーブルに文字列として書き込む代わりに、0
や1
や2
といった整数を利用できます。
アプリケーションにPostのテーブルが既に存在していると仮定すれば、status
をenumとして追加するDBマイグレーションは以下のようになります。
class AddStatusToPosts < ActiveRecord::Migration[7.0]
def change
add_column :posts, :status, :integer, default: 0
end
end
原注: このマイグレーションでは、デフォルト値に0を指定しています。つまり、postのステータスはデフォルトでdraft
になります。
モデルでenumを定義するさまざまな方法
rake db:migrate
でマイグレーションを実行したら、以下のコード例のようにPostモデルでenumを定義する必要があります。enum
メソッドの第1パラメータにはカラム属性名を渡し、第2パラメータにはpostのステータスにしたい値のリストを渡します。
# app/models/post.rb
class Post < ApplicationRecord
enum :status, [ :draft, :published, :archived, :trashed ]
end
以下のように%i()
形式でもenumを宣言できます。
# app/models/post.rb
class Post < ApplicationRecord
enum :status, %i(draft published archived trashed)
end
enumの宣言で配列を利用したので、第1要素のdraft
はデータベース上の0に対応付けられ、第2要素のpublished
は1に対応付けられる、という具合に名前が整数に対応付けられます。
以下のように、配列の代わりにdraft
やpublished
などのキーを持つハッシュも渡せます。この場合、enumの値は開発者が指定できます。
# app/models/post.rb
class Post < ApplicationRecord
enum :status, { draft: 0, published: 1, archived: 2, trashed: 3 }
end
配列よりもハッシュがおすすめです。配列は値の順序が変わるとRailsアプリ内部のロジック全体が壊れるからです。
enumの動作
以下のようにカラム名を複数形にすることで、enumのすべての値をフェッチすることも、enumの特定の値をフェッチすることもできます。
Post.statuses
#=> { "draft" => 0, "published" => 1, "archived" => 2, "trashed" => 3 }
Post.statuses[:archived]
#=> 2
Post.statuses["trashed"]
#=> 3
Post.statuses[:unarchived]
#=> nil
ステータスがpublished
のpostを作成する
post = Post.create(title: "First post", description: "First post description...")
post.status
#=> "draft"
post = Post.create(title: "Second post", description: "Second post description...", status: :published)
post.status
#=> "published"
上で最初に作成したstatus
が未指定のpostには、デフォルトでdraft
が設定されます。status
カラムに保存されているのは整数値ですが、キーの値として:published
のようにシンボルも渡せます。Railsはこのstatus
カラムがenumであることを認識して、内部でシンボルを整数値に置き換えます。
postのステータスを照合する
Railsは、特定のpostのステータスを照合する動的メソッドを提供しています。
postを1件作成して公開するときは、post.status == 'published'
を用いて照合するのが普通です。enumを使うと、Railsが提供するenumヘルパーで以下のように照合できます。
post.published?
#=> true
post.draft? || post.trashed?
#=> false
postのステータスを更新する
?
付きのステータスでpostを照合するときと同様に、Railsのenumにはenum値を更新するときのヘルパーも用意されています。post.update(status: :archived)
と書く代わりに、以下のように!
付きのメソッドで更新できます。
post.archived!
post.published?
#=> false
post.archived?
#=> true
enumでスコープを使う
このRailsアプリのPostモデルにはさまざまなステータスがあるので、そのうち指定のステータスのレコードだけを取り出したくなるでしょう。Railsにはこのクエリを解決する動的なメソッドが追加されています。
たとえばステータスがpublished
のpostをすべてフェッチするなら、RailsのコントローラでPost.where(status: "published")
のように書くことも一応可能です。
そのように書く代わりに、Postでpublished
メソッドをスコープとして使えます。Railsは、enumにあるすべてのステータスごとに、ステータスと同じ名前のクラスメソッドを動的に追加します。この場合、Postモデルに#draft
、#published
、#archived
、#trashed
メソッドが生成されます。
Post.published
select "posts".* from "posts" where "posts"."status" = $1 [["status", 1]]
Rails 6では、enumのスコープの否定条件も利用できます。ステータスがpublished
でないpostをすべてフェッチするには、以下のようにpublished
メソッド名の冒頭にnot_
を追加できます。
Post.not_published
select "posts".* from "posts" where "posts"."status" != $1 [["status", 1]]
モデルで定義されたenumにscopes: false
オプションを渡してenumのスコープを無効にすることも可能です。無効にしたスコープを使うとNoMethodError
がraiseされます。
# app/models/post.rb
class Post < ApplicationRecord
enum :status, { draft: 0, published: 1, archived: 2, trashed: 3 }, scopes: false
end
Post.published
=> NoMethodError: undefined method `published' for #<Class:0x009hg431k236t8>
enum値にプレフィックスやサフィックスを追加する
enumの値だけだと意味がわからないこともあるので、enum名にプレフィックスやサフィックスを適用する方がよいでしょう。ここでは、アプリケーションでenum
として定義されたstatus
カラムを持つUserモデルを考えてみましょう。
# app/models/user.rb
class User < ApplicationRecord
enum :status, { invited: 0, active: 1, deactivated: 2 }
end
たとえばuser.active?
メソッドは、user.active_status?
のように正確に書けます。他のenum名についても同様です。こう書けるようにするには、以下のようにenumのオプションにsuffix: true
を追加します。
# app/models/user.rb
class User < ApplicationRecord
enum :status, { invited: 0, active: 1, deactivated: 2 }, suffix: true
end
更新された動的メソッドを用いて、以下のように等価チェック、更新、userオブジェクトやモデルへのクエリを実行できるようになります。
user.invited_status?
user.active_status!
User.deactivated_status
prefix: true
オプションを渡すと、上のメソッドは以下のように変わります。
user.status_invited?
user.status_active!
User.status_deactivated
プレフィックスやサフィックスは、enum値を持つカラムがモデルに2つある場合に便利です。たとえばPostモデルにenumのcategory
カラムがあり、free
またはpremium
という値を持つとします。
# app/models/post.rb
class Post < ApplicationRecord
enum :status, { draft: 0, published: 1, archived: 2, trashed: 3 }
enum :category, { free: 0, premium: 1 }
end
プレフィックスやサフィックスのないenumを定義すると、Post.free
やpost.published?
やpost.premium!
がどちらのカラムを参照しているかがわかりにくくなり、開発者が戸惑ってしまいます。
代わりに、以下のようにプレフィックスやサフィックスをメソッドに追加して、必要なメソッドを呼び出せるようにできます。
# app/models/post.rb
class Post < ActiveRecord::Base
enum :status, { draft: 0, published: 1, archived: 2, trashed: 3 }, prefix: true
enum :category, { free: 0, premium: 1 }, suffix: true
end
Post.free_category
post.status_published?
post.premium_category!
メモ
Rails 7ではenumの新しい構文が導入されました。詳しくは以下の過去記事をご覧ください。
概要
元サイトの許諾を得て翻訳・公開いたします。