Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsのenumを使いこなす方法(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

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のテーブルに文字列として書き込む代わりに、012といった整数を利用できます。

アプリケーションに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に対応付けられる、という具合に名前が整数に対応付けられます。

以下のように、配列の代わりにdraftpublishedなどのキーを持つハッシュも渡せます。この場合、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.freepost.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の新しい構文が導入されました。詳しくは以下の過去記事をご覧ください。

関連記事

Rails 7のenumに新しい構文が導入(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。