WeBlog

Webに関する情報を中心に発信してるブログ

enumについて

enumとは?

enumとは、列挙型とも呼ばれ、一連の整数値に対して複数の変数名をつけれる仕組みを指します。

要は整数と名前を紐づけることができるということです。

enumを使う準備をする

カラムを用意する

enumを使うにはテーブルにカラムを準備します。

class AddRoleToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :role, :integer, default: 0, null: false
  end
end

今回はusersテーブルにroleというカラムを用意したいと思います。

roleというのは「役割」という意味です。

ユーザーを「管理者」と「一般ユーザー」という役割に分けたいと思います。

f:id:weblog_tec:20210505223607p:plain

enumはint形で指定します。(boolean型で指定することもできる)

なので今回の場合は「0」は一般ユーザー、「1」は管理者とします。

デフォルトは「0(一般ユーザー)」とします。

モデルにenumを定義する

enumを使うにはモデルにenumの記載が必要です。

class User < ApplicationRecord
    # enum カラム名: { key: value, key: value }
  enum role: { general: 0, admin: 1 }

書き方は上記のようになります。

これでgeneralと「0」、adminと「1」が紐付きます。

enumを使ってカラムを更新してみる

f:id:weblog_tec:20210505223638p:plain

現状roleカラムに「1、0、0、0」という形で入っています。

上から2つ目のユーザーのroleカラムを「1」に変えてみます。

> user = User.second
  User Load (0.4ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, last_name: "ユーザー", first_name: "B", created_at: "2021-04-21 09:42:53", updated_at: "2021-04-24 12:50:02", role: "general">

> user.update(role: 'admin')
   (0.1ms)  begin transaction
  User Update (1.4ms)  UPDATE "users" SET "updated_at" = ?, "role" = ? WHERE "users"."id" = ?  [["updated_at", "2021-05-02 15:35:44.557897"], ["role", 1], ["id", 2]]
   (2.9ms)  commit transaction
=> true

f:id:weblog_tec:20210505223659p:plain

まずuserを取得してみます。

この時のrole: "general" になっています。

enumでは取得するときに「0」や「1」といった数値ではなく、「general」や「admin」といった文字で取得されます。

更新するときはuser.update(role: 'admin')のように「0」や「1」ではなく、「general」や「admin」といった文字で指定します。

SQLのUPDATE文をみると、更新するときは"role", 1となっています。

このようにenumを使うと、テーブル上では「0」「1」のような数値になっていますが、取得するときは文字になります。

enumを使うと何がいいのか?

先ほどenumの使う方法を書きました。

しかし、これの何がいいのかよくわかりませんね。

enumの良いところは、enumを定義することで便利なメソッドが使えるという部分です。

例えば、ユーザーが管理者かどうかを判定したい場合、

enum未定義の場合は

if @user.role == 1
    # 実行したい処理 
end

と言った記述をしないといけません。

この@user.role == 1をパッとみて何を判定しているのかわかりにくいです。

一方で、enumが定義されていると、

enum定義済の場合

if @user.admin?
  # 実行したい処理 
end

と言った具合に、「取得してユーザーはadminですか?」と自然に問いかけることができます。

コードを読んだ段階でどのような処理をしているのか予測が立ちやすく、簡潔なコードで記述することができます。

他にもいろんなenumのメソッドがあります。

# モデル名.enumのカラム名(複数形)
> User.roles
=> {"general"=>0, "admin"=>1}

モデル名.enumのカラム名(複数形)とすることで、enumの定義を取得できます。

> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, last_name: "ユーザー", first_name: "A", created_at: "2021-04-21 09:39:08", updated_at: "2021-04-28 07:00:32", role: "admin">

# ユーザーオブジェクトのroleカラムのenumのkeyを取得する
> user.role  
=> "admin"

# ユーザーオブジェクトがadminかどうかを確認する
> user.admin?
=> true

# ユーザーオブジェクトがgeneralかどうかを確認する
> user.general?
=> false

取得したユーザーオブジェクトに対して「あなたはadminですか?」と問い合わせたり、

ユーザーオブジェクトのroleカラムのenumのkeyを取得するなんてことができます。

# roleカラムがgeneralの人を取得する
> User.general
  User Load (3.3ms)  SELECT  "users".* FROM "users" WHERE "users"."role" = ? LIMIT ?  [["role", 0], ["LIMIT", 11]]

# roleカラムがadminの人を取得する
> User.admin
  User Load (1.2ms)  SELECT  "users".* FROM "users" WHERE "users"."role" = ? LIMIT ?  [["role", 1], ["LIMIT", 11]]

# roleカラムのenumの値を取得する
> User.all.pluck(:role)
   (2.0ms)  SELECT "users"."role" FROM "users"
=> ["admin", "admin", "general", "general"]

adminユーザーだけ取得する。

generalユーザーだけ取得する。

こんなこともできます。

# roleカラムは「general」と「admin」の人を取得する
> User.where(role: [:general, :admin])
  User Load (3.1ms)  SELECT  "users".* FROM "users" WHERE "users"."role" IN (?, ?) LIMIT ?  [["role", 0], ["role", 1], ["LIMIT", 11]]

# roleカラムは「general」ではない人を取得する
> User.where.not(role: :general)
  User Load (6.0ms)  SELECT  "users".* FROM "users" WHERE "users"."role" != ? LIMIT ?  [["role", 0], ["LIMIT", 11]]

またwhereでSQLを作成して、条件を書いて取得することもできます。

enumの書き方は2通りある

enumの書き方は下記2種類あります。

どちらも同じです。

# hash形式で数値を指定
class User < ApplicationRecord
  enum role: { general: 0, admin: 1 }
end

> User.roles
=> {"general"=>0, "admin"=>1}
# 配列で数値を指定しない
class User < ApplicationRecord
  enum role: [:general, :admin]
end

> User.roles 
=> {"general"=>0, "admin"=>1}

後者の数値を明示をしない書き方でも、generalには0が、adminには1が割り振られています。

数値を指定しても、数値を指定しなくても同じ気がします。

でも、明示的に数値を指定した方が良いです

なぜ数値を書いた方がいいのでしょうか?

その前に、「0」「1」などの数値は何を表しているのでしょうか?

この数値は、DBに保存される値です。

generaladminなどの文字列ではなく、01などの整数がDBに保存されます。

なので、roleカラムを追加するmigrationでも、integer型を指定します。

class AddRoleToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :role, :integer, null: false, default: 0
  end
end

でもただの数値だと扱いづらいので、ActiveRecordenumマッピングをみてDBから取得した数値を文字列に変換して自身に値を持ちます。

数値が何を表しているのかをわかりやすくするために数値と文字を紐づけるenumの形式で書きました。

では、なぜ数値を明示し他方がいいのでしょうか?

それは、「必ず」0generalと、1adminと紐付いている状態にするためです。

例えば、generaladmin以外に、readという権限を持つユーザーを追加する場合です。

まずは何も問題が起きないパターンから。

# hash形式で数値を明示
class User < ApplicationRecord
  enum role: { general: 0, admin: 1, read:2 }
end

User.roles 
=> {"general"=>0, "admin"=>1, "read"=>2}
# 配列で数値を明示しない
class User < ApplicationRecord
  enum role: [:general, :admin, :read]
end

User.roles 
=> {"general"=>0, "admin"=>1, "read"=>2}

enum role: { general: 0, admin: 1, read:2 }enum role: [:general, :admin, :read]のようにreadを1番後ろに追加した場合は特に問題ないです。

では、もし誰かが、generaladminの間にreadを書いてしまったらどうなりますか?

# hash形式で数値を明示(解答例と同じ)
class User < ApplicationRecord
  enum role: { general: 0, read:2, admin: 1 }
end

# rails c
User.roles # => {"general"=>0, "admin"=>1, "read"=>2}
# 配列で数値を明示しない
class User < ApplicationRecord
  enum role: [:general, :read, :admin]
end

# rails c
User.roles # => {"general"=>0, "read"=>1, "admin"=>2}

数値を明示的に指定すると何も問題ないです。

しかし、数値を明示しない場合は、1readと紐付いています。

テーブルで1が保存されていたデータを、ActiveRecordreadと認識するということです。

なので、今までadminだった人がreadになってしまうということです。

複数人で開発していると、誰かが何かの拍子に変なとこにenumの定義を追加してしまうかもしれません。

そんな時に数値を明示しておけば、不具合になることはありません。

なので数字を明示した方がいいです。

https://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html

https://railsguides.jp/active_record_querying.html#enums

https://pikawaka.com/rails/enum