[Rails] enumを用いてユーザー属性を追加
enumを利用して管理者・一般を表す
enum
を用いてユーザーの属性(管理者か一般)を管理し、ユーザーの属性によって、ログイン後のページ遷移をコントロールします。
enum(列挙型)とは
数値のカラムに対してプログラム上で扱える別名を与えます。
一つのカラムに指定した複数個の定数を保存できるようにする為もののことです。
名前に数値を割り当てることで、コードの可読性を上げて、かつ不要な不具合を防いでくれるものです。
主に下記のような特徴があります。
- 名前(文字列)のリストに一意の値が割り当てられる
- 定義の順番通りに値が割り当てられる
- enum型の定数は定義されている以外の値を受け付けない(例外が発生)
usersテーブルにroleカラムを追加
以下の手順で実装します。
- usersテーブルに
enum
用のカラムを用意する - モデルに
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
メソッドでkey
とvalue
を入れ替えられます。よって、下記のように値である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である。 -
ransack
はenum
に対応していないので、[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
_before_type_cast
enumの整数値を取得する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