🐟

[Rails] enumを用いてユーザー属性を追加

2021/10/14に公開

enumを利用して管理者・一般を表す

enumを用いてユーザーの属性(管理者か一般)を管理し、ユーザーの属性によって、ログイン後のページ遷移をコントロールします。

enum(列挙型)とは

数値のカラムに対してプログラム上で扱える別名を与えます。

一つのカラムに指定した複数個の定数を保存できるようにする為もののことです。

名前に数値を割り当てることで、コードの可読性を上げて、かつ不要な不具合を防いでくれるものです。

主に下記のような特徴があります。

  • 名前(文字列)のリストに一意の値が割り当てられる
  • 定義の順番通りに値が割り当てられる
  • enum型の定数は定義されている以外の値を受け付けない(例外が発生)

usersテーブルにroleカラムを追加

以下の手順で実装します。

  1. usersテーブルにenum用のカラムを用意する
  2. モデルにenumの定義をする

enumを使うために、usersテーブルにroleカラムを追加します。

enumはデータ型についてはinteger型になります。

  • general・・・一般
  • admin・・・管理者

マイグレーションファイルを作成して編集します。

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

ポイント

  • 一般もしくは管理者か、必ずデータが入るので、null: false
  • 初期値は一般にしたいので、default: 0

モデルの定義

次に、useモデルにenumを使って複数個の定数リストを登録します。

enumの値は、間隔を空けて定義したほうが、後で追加するときに楽になる。

例)

# NG
enum: { draft: 0, in_review: 1, published: 2, archived: 3 }

# この場合、ステータスを追加する際には数字を増やしたものを定義するしかない。
enum: { draft: 0, in_review: 1, published: 2, archived: 3, review_requested: 4, change_requested: 5 }

# GOOD
enum: { draft: 0, in_review: 10, published: 20, archived: 30 }

# 間に定義することができるので、追加するenumと他の値の関係性を理解しやすい
enum: { draft: 0, review_requested: 5, in_review: 10, change_requested: 15, published: 20, archived: 30 }

今回は1と0で定義する。

app/models/user.rb

class User < ApplicationRecord

	enum role: { general: 0, admin: 1 }

	# もしくは
	enum role: %i[general edmin]

end

roleカラムにこの数値が保存されます。

数値によって一般か管理者か区別します。

※定数リストの取得メソッド

ちなみにモデルクラス.enumカラム名の複数形で、定義した定数リストをハッシュ形式で取得できます。

[16] pry(main)> User.roles
=> {"general"=>0, "admin"=>1}

また、keyだけ欲しい場合・valueだけ欲しい場合は下記の様に書くと取得できます。

[7] pry(main)> User.roles
=> {"general"=>0, "admin"=>1}
[8] pry(main)> User.roles.keys
=> ["general", "admin"]
[11] pry(main)> User.roles.values
=> [0, 1]

Adminユーザーの作成

管理者権限を持つユーザーをseedファイルで作成します。

db/seeds.rb

User.create!(last_name: "あどみん",
  first_name: "あどみん",
  email: "admin@admin.com",
  password: "admin",
  password_confirmation: "admin",
  role: 1
)

確認します。

% rails c
id: 11,
  email: "admin@admin.com",
  crypted_password:
   "$2a$10$4AkXbdWoZVO79blLtYzC4eL46eFEiKOdOYfSQbRnC6ISKrfjBq.9G",
  salt: "kKHyQYuyDd2XcohiVWso",
  last_name: "あどみん",
  first_name: "あどみん",
  created_at: Fri, 16 Jul 2021 16:28:55 JST +09:00,
  updated_at: Fri, 16 Jul 2021 16:28:55 JST +09:00,
  avatar: nil,
  reset_password_token: nil,
  reset_password_token_expires_at: nil,
  reset_password_email_sent_at: nil,
  access_count_to_reset_password_page: 0,
  role: "admin">]

roleカラムに1を保存することによって、adminとして認識されています。

form_withでenumのプルダウン形式フォームを作る(i18n日本語化)

前提

usersテーブルのroleカラムをenumを用いたinteger型を例にします。

ユーザーが一般か管理者できるようにします。

db/schema.rb

create_table "users", force: :cascade do |t|
    t.integer "role"
end

app/models/user.rb

enum role: { general: 0, admin: 1 }

プルダウン形式のフォーム

プルダウン形式のフォームは通常、下記のようにf.selectを用います。

<%= f.select カラム名, {'選択肢': DBに保存する値}, {オプション}, {classなどの要素} %>

enumを用いたカラムでf.selectを用いる

次にenumを用いたカラムでプルダウン形式のフォームを作成します。

keyをそのまま使う場合 to_a

<%= f.select :role, User.roles.keys.to_a, {}, class: 'form-control' %>

ポイント

  • User.rolesは定数リスト取得メソッドです。
    モデルクラス.enumカラム名の複数形でハッシュ形式で取得できます。

    User.roles
    => {"general"=>0, "admin"=>1}
    
  • .keysでハッシュのキーを取り出す。

  • .to_aメソッドはハッシュを配列に変換するメソッドです。

enum_helpの導入

i18nで日本語化する場合は書き方が変わります。

eenum_helpとはenumで定義した値をi18n化させることができるgemです

また、ransackではenumに対応していないのでenum_helpをつかって、自分でenumが指定している定数とintegerのペアを作ってあげる必要があります。

まずはenum_helpを導入します。

下記のような返り値を日本語化します。

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

まずはインストール。

gem 'enum_help'
bundle install

config/locales/activerecord/ja.yml

ja:
  enums:
    user:
      role:
        general: '一般'
        admin: '管理者'

呼び出し方

日本語で呼び出すには、カラムの後に_i18nを付けます。

ただ、モデルに対して呼び出すときはカラムは複数形。

モデルのインスタンスなどを呼び出すときのカラムは単数形。

User.roles_i18n
=> {"general"=>"一般", "admin"=>"管理者"}

@user.role_i18n
=> "管理者"

このように、valueである数値(0と1)が日本語になりました。

日本語化したenumカラムでf.selectを用いる

先程のenum_helpを利用して書き直すと下記のようになります。

<%= f.select :role, User.roles_i18n.invert, {}, class: 'form-control' %>

=> {"一般"=>"general", "管理者"=>"admin"}

ポイント

  • User.roles_i18n.invertによって{"一般"=>"general", "管理者"=>"admin"}が取得できる。

  • invertメソッドでkeyvalueを入れ替えられます。

    よって、下記のように値である0(一般)や1(管理者)がDBに保存されます。

もし、invertメソッドを使わないと、、、下記のような返り値となるので、

<%= f.select :role, User.roles_i18n.invert, {}, class: 'form-control' %>

=> {"general"=>"一般", "admin"=>"管理者"}

このようにキーが保存されることになるので、エラーが発生してしまします。

ransackを用いた場合

先程はプルダウンのフォームだけでしたが、ransackも用いて検索フォームにする方法です。

roleカラムに対して検索熟語(Predicate)は_eq。つまり、完全一致で検索します。

<%= f.select :role_eq, User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}, { include_blank: t('defaults.unspecified') }, { class: 'form-control mr-1' } %>
配列の入った変数.map {|変数名| 処理内容 }
User.roles_i18n.invert.map{|key, value| ['表示させる値', '送信する値' ]}

User.roles_i18n.invert.map{|key, value| [key, User.roles[value]]}

ポイント

  • User.roles_i18n.invertによって{"一般"=>"general", "管理者"=>"admin"}という文字列ハッシュを取得。
  • しかしenumにより実際にDBに保存されているのは、対応づけられたintegerである。
  • ransackenumに対応していないので、[key, value]とすると文字列と整数を比較することになりエラーとなる。
  • なので、User.roles[value]でモデルに定義したenum role: { general: 0, admin: 1}のvalue(enumの数値)を呼び出します。

mapメソッド

配列の要素を変数に入れて繰り返し変換処理をして新たな配列を作成します。

配列.map{|変数|変換処理}

Enumによって使えるインスタンスを操作するメソッド

例として下記のようなenumが定義されているとします。

Article(記事)のステータスがeunmで定義されています。

app/models/article.rb

class Article < ApplicationRecord

	enum state: { draft: 0, published: 1, publish_wait: 2 }

end

まずはインスタンス作成

[9] pry(main)> article = Article.new(state: 0)
=> #<Article:0x00007fb0853c60a8
 id: nil,
 state: "draft",

インスタンスのenumの値を確認

[10] pry(main)> article.state
=> "draft"

インスタンスが特定のenumの値かどうか確認

enumの値に?を付けると、そのインスタンスが指定の値を持っているか確認できます。

trueやfalseで返すので、if文などの条件分岐などで、使いやすいです。

[12] pry(main)> article.draft?
=> true

[13] pry(main)> article.published?
=> false

インスタンスのenumを更新

enumの値に!を付けると、その指定の値に更新してくれます。

[13] pry(main)> article.published!
=> "published"

データベースから検索 検索メソッド

モデルクラスに対してモデルクラス.enumの値の形で使用します。

enumの値で保存されているデータを全て取得するメソッドです

[2] pry(main)> Article.publish_wait
   (0.2ms)  select sql from (select * from sqlite_master where type='table' union select * from sqlite_temp_master where type='table') where tbl_name = 'articles'
  Article Load (0.2ms)  SELECT "articles".* FROM "articles" WHERE "articles"."state" = ?  [["state", 2]]
=> []

[3] pry(main)> Article.published
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" WHERE "articles"."state" = ?  [["state", 1]]
=> [#<Article:0x00007fdfd6c420d0
  id: 11,
  category_id: 1,
  author_id: nil,
  slug: "slug",
  title: "hhkjy",
  description: "asdf",
  body: "",
  state: "published"

[4] pry(main)> Article.published.count
   (0.3ms)  SELECT COUNT(*) FROM "articles" WHERE "articles"."state" = ?  [["state", 1]]
=> 1

enumの整数値を取得する_before_type_cast

enumに登録されている、整数値の方を取得するには下記の様にします。

enumカラム名_before_type_cast

例)

enum evaluation: { strongly_disagree: 1, disagree: 2, neutral: 3, agree: 4, strongly_agree: 5 }
evaluarion = Evaluation.find_by(evaluation: 1)

#=> evaluation: 'strongly_disagree'
evaluation.evaluation__before_type_cast
#=> 1

Discussion