#はじめに
RubyやRailsで、どのようにEnumが使えるか色々試してみました。
結果を忘れないうちに整理するための振り返りメモです。
間違っている点などあれば、ご指摘・指南頂ければ幸いです!!!
#Enumとは?
「Enum」は「列挙型」のこと。
この列挙型を扱う機能としてRuby on Rails4.1からActiveRecord :: Enum
と言うモジュールが追加となりました。
この機能は、モデルの数値カラムに対して文字列による名前定義をマップすることができます。また、データ操作用の便利なメソッドも提供してくれます。
上手く説明できないですが、列挙型を扱うのに何か便利そうな機能です(0ω0)←
【参考】Ruby on Rails 4.1 リリースノート
【参考】Ruby on Rails 5.2.3 ActiveRecord::Enum
※ほぼコレを見るれば事足りる感が否めない笑
#Enum定義対象について
色々と弄ってみた結果の個人的な見解です。
便利とは言え、カラムにEnumを適用するかどうかは判断基準を設けた方が良いように思います。何故かと言うと、例えば以下のようなカラムがあったとします。
①頻繁に更新が発生する
②名前定義が中規模〜大規模
③複数モデルで利用されている
文字列として定義していたらタイポを誘発しそうですし、複数モデルで同じような定義が分散していたら修正工数は増大しそうです。それなら、カラム自体を単独クラスとして切り出してアソシエーションを組んだ方がメンテナンス性が向上すると思います。
じゃあ、scopeのように共通化…と思うかもですが、ActiveRecordを継承しているモジュールなので、AcctiveSupport::Concern使えないのではって、ふわっと思ったりします。(思考停止
あと、クラス化する場合はactive_hash
と言う便利なgemも用意されてたりして、尚更こちらかと!
【参考】Github:active_hash
【参考】Railsのgem 'active_hash'で都道府県データを作成してみた
と、クラス化推した後で、Enumに移ります!←
#定義方法
モデルに対してenumを定義(宣言?)する必要があります。
定義方法は、ザクっと2通りあります。(他にもあるかもしれません…
※この記事では数値に紐づいた文字列のことを名前定義と記載しています。もしかしたら、ちゃんとした名前があるのかも…
##パターン① 名前定義のみ指定
配列で名前定義をします。この場合、定義順に0から順に名前定義と紐付けされます。
下記の例ではgood→0、normal→1、bad→2と言う風に自動採番される感じです。
また、この例の一番左に項目(仮にunrated
)を差し込むとunrated→0、good→1…と、数値の採番が変わります。
class Rating < ApplicationRecord
enum rate: [ :good, :normal, :bad ]
~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
#カラム名 #定義対象の内容
end
##パターン② 名前定義と対応する数値を指定
ハッシュで名前と数値の対応を指定して、定義することもできます。
色々探していると、この方法が最も一般的なのかな?と思います。
class Rating < ApplicationRecord
enum rate: { good: 0, normal: 1, bad: 2 }
~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
#カラム名 #定義対象の内容のマッピング(名前: 数値)
end
なお、公式リファレンス見てみると**「一番最初に宣言した値をデフォルト値としてモデルに定義することを推奨」**されています。多分こんな感じ。↓
class CreateRatings < ActiveRecord::Migration[5.2]
def change
create_table :ratings do |t|
#...略
t.integer :rate, defalut: 0 # ← enum定義前なので、ここでは名前定義使えません
#...略
end
end
null:false
を指定していたら問題ないようにも思いますが、例えば「ユーザが指定する項目でない」「自動生成されるレコード」「新たにカラムを追加する」など、指定しておくべきパターンも考えられるので、設定しておいた方が良さそうです。。
【補足説明】
他のページではBoolean型で宣言しているところがありましたが、公式に記載がないのと、試して見た結果うまくいかなかったので定義方法としてはオススメしません。。
ちなみに、どうなるかと言うと…
class Notification < ApplicationRecord
#boolean型の項目にenumで名前定義してみる
enum checked: { yet: false, already: true }
end
この状態でコンソールから値の更新をしてみます。
[1] pry(main)> notice = Notification.find(1)
=>#<Notification:0x00007ffad9cf5a90
#...略...
checked: "already", #とりあえず"already(true)"の項目取ってくる
#...略...
[2] pry(main)> notice.yet! #"yet(false)"に更新する…と、下記のエラー発生
Notification Update (1.5ms) UPDATE `notifications` SET `checked` = NULL, `updated_at` = '2019-10-07 17:40:12' WHERE `notifications`.`id` = 1
(2.8ms) ROLLBACK
ActiveRecord::NotNullViolation: Mysql2::Error: Column 'checked' cannot be null: UPDATE `notifications` SET `checked` = NULL, `updated_at` = '2019-10-07 17:40:12' WHERE `notifications`.`id` = 1
from /Users/ozakiyukiko/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.5.2/lib/mysql2/client.rb:131:in `_query'
Caused by Mysql2::Error: Column 'checked' cannot be null
from /Users/ozakiyukiko/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/mysql2-0.5.2/lib/mysql2/client.rb:131:in `_query'
[3]pry(main)> notice.checked
=> nil #どうやら0ではなく"nil"で更新される模様。そして、null: false項目だったからエラーでrollback
ちなみにalready
は1
でしっかり更新されます。
また、データベースの値が0
だった場合、しっかりyet
で認識されます。
が、更新時にnil
更新されるのでは…実用に耐えられない感じがしました(0ω0;)
#定義済みのEnumを参照
実際に定義した値をコンソールで参照してみます。
参照方法はカラム名にs
を付ければOK。何だか使い方がscopeみたいな感じで面白いですね。
因みに、定義方法に関わらず参照結果はハッシュ形式で返されてました。
# 定義済のEnumを参照
[1] pry(main)> Rating.rates
=> {"good"=>0, "normal"=>1, "bad"=>2} # ハッシュ形式でEnumの定義が取得できました
# Enumで定義した名前を参照
[2] pry(main)> Rating.rates[:good]
=> 0 # ハッシュ形式なので定義項目を1つだけ抽出することも可能です
# Enumに指定していない項目を参照
[3] pry(main)> Rating.rates[:unrated] # エラーにならずnilが返ってきます。
=> nil
【注意点】
Ruby/Railsでは、未定義の名前定義を選択しても例外は発生しません。単なるタイポが思わぬエラーとならないよう注意が必要そうです。
#Enumとeach文を組み合わせる
列挙した種類ごとに処理をループしたいことって有りそうですよね。
(私はあった!)ので、少し試した結果が↓です。
#each文を実行してみる
[1] pry(main)> Rating.rates.each do | rate |
[1] pry(main)* puts rate[0] # 名前定義
[1] pry(main)* puts rate[1] # 対応する数値
[1] pry(main)* end
good
0
normal
1
bad
2
=> {"good"=>0, "normal"=>1, "bad"=>2}
eachの中身であるrate
は、2次元配列になっている様です。
ループごとに["good",0]
と言う形式で取得できました。これを利用して色々処理が組めそうです。
また、0から順番でよければeach_with_index
も使えそうですね。(可読性と言う意味では微妙な気もしますが…)
#Enumを使ってインスタンスを操作する
定義されたEnumに対して処理を行ってみます。
##①インスタンスを作成する
Enumが定義されたインスタンスを作成してみます。
これは更新時も同様ですね。
#(従来)数値で指定する
[1] pry(main)> rate = Rating.new(
[1] pry(main)* rated_user_id: 1,
[1] pry(main)* rater_user_id: 2,
[1] pry(main)* trade_id: 1,
[1] pry(main)* rate: 0 # ← 数値で指定してみます
[1] pry(main)* )
=> #<Rating:0x00007fefbf41a4e0 # ← 以下、結果
#…略
rate: "good", # ← マップされた名前定義で認識されました!
#…略
#名前定義で指定する
[2] pry(main)> rate = Rating.new(
[2] pry(main)* rated_user_id: 1,
[2] pry(main)* rater_user_id: 2,
[2] pry(main)* trade_id: 1,
[2] pry(main)* rate: "good" # ← 名前定義で指定してみます
[2] pry(main)* )
=> #<Rating:0x00007fefbb4a32d0 # ← 以下、結果
#…略
rate: "good", # ← その通り認識されましたね
#…略
名前定義で指定ができるので、単語の意味合いから何を示すのか分かりやすいですね。
これで、かなり可読性をあげることが出来そうかな。
因みに、データベース上はちゃんと数値で保存されてます!
##②インスタンスの中身を確認する
①で作成したインスタンスを使ってEnumの中身を確認してみます。
[3] pry(main)> rate.rate #対象の項目を参照
=> "good" #数値ではなく、名前定義"good"が返ってきました!
[4] pry(main)> rate.good? #指定の名前定義かどうかを確認する便利メソッド!
=> true #エラーにならず、"true"が返ってきます!
[5] pry(main)> rate.normal? #違うパターンもやってみましょう
=> false #"false"が返ってきましたね
ifやcase文などの条件分岐に使用すると可読性が向上しそうですね。
##③インスタンスを更新する
Enumが定義されたインスタンス変数を更新してみます。
※updateを使用した場合、作成時と変わらないので説明を省略します。
[6] pry(main)> rate.rate #変更前の状態を確認します
=> "good" #"good"ですね
[7] pry(main)> rate.normal! #指定した名前定義で更新ができちゃう便利メソッド!
=> "normal" #"normal"に更新されました
[8] pry(main)> rate.rate #念のため、更新後の状態も見てみます
=> "normal" #[7]で指定した"normal"に変更されました!
ステータス管理の項目だった場合、この方法で変更すると効率も可読性も向上しそうですね。なお、このメソッドは、実行時点でデータベースに反映さるようです。
##④データベースを検索する
名前定義と同名のscopeが自動的に割り当てられるようです。
そのため、以下のようにデータベースの検索が可能になります。
[1] pry(main)> rate = Rating.good # 成功したっぽい
=> [#<Rating:0x00007ffad9f61540...
[2] pry(main)> rate = Rating.good.count
=> 6 # 件数も一致してるし、大丈夫かな
※5.2.4以降ではnot_xxxx
で打ち消し表現も出来るみたいですね。
自動生成されたscope自体をキャンセルすることも出来るみたい…??
【参考】Ruby on Rails master@a4ea17f ActiveRecord::Enum
#名前定義重複時の対応
同一モデル内に、同一の名前定義を持つ項目があった場合、それらを識別するために、オプションとしてprefix(接頭辞)またはsuffix(接尾辞)を名前定義に付与できます。
定義イメージは以下のとおり↓
class Rating < ApplicationRecord
enum user_rate: { good: 0, bad: 2, unrated: 3 }, _prefix: true
enum product_rate: { good: 0, normal: 1, bad: 2 }, _suffix: :sample
end
prefix、suffix、どちらに対してもカラム名・シンボル名の指定は可能なので、組み合わせて利用する感じかな。
設定値 | 概要 |
---|---|
true | 名前定義のprefix又はsuffixとしてカラム名を付与 |
任意のシンボル名 | 名前定義のprefix又はsuffixとして指定したシンボル名(又は文字列)を付与 |
rating.rb
のEnumを実際に参照してみるとこんな感じ↓
# _prefix:trueの実行例
[1] pry(main)> rate.user_rate
=> "unrated" #初期状態を確認
[2] pry(main)> rate.user_rate_good!
=> true #prefixにカラム名を指定して実行し、成功!
[3] pry(main)> rate.user_rate
=> "good" #想定どおり"good"に変わってる!
[4] pry(main)> rate.user_rate_good?
=> true #prefixにカラム名を指定した状態で、名前定義の合致確認もできた!
# _suffix: :sampleの実行例
[5] pry(main)> rate.product_rate
=> "normal" #初期状態の確認
[6] pry(main)> rate.good_sample!
=> true #suffixに任意名を指定して実行し、成功!
[7] pry(main)> rate.product_rate
=> "good" #想定どおり"good"に変わってる!
[8] pry(main)> rate.good_sample?
=> true #suffixに任意名を指定した状態で、名前定義の合致確認もできた!
なお、Enum自体を参照する際は、このオプションの影響を受けませんので検証割愛。
#日本語化を駆使してview表示
viewにEnumを表示させる際は、日本語で出て欲しいよね。
ってことで、少し探してみました。以下のような方法があるようです。
パターン① gemを使わずに頑張る
パターン② 諦めてgemを使う( enumerize / enum_helper辺りが有名どころ)
gemを使う方法は結構色々落ちていたので、今回はパターン①を模索してみました。
※以降はi18n機能によって日本語化が完了している前提で進めます※
##ja.ymlへの定義追加
ja:
から始まっていれば何でもOKなのですが、enumであることを明示しつつ以下の通り定義します。
ja:
enums: #enum型に対する日本語定義であることを明示するために記述
rating: #モデル名を指定
rate: #カラム名を指定
good: 良い #Enumの名前定義①
normal: 普通 #Enumの名前定義②
bad: 悪い #Enumの名前定義③
##Helperメソッドの作成
viewで扱いやすいように日本語化の処理をhelperメソッドとして切り出して用意しておきます。
module EnumHelper
# クラスオブジェクトとカラム名を引数として呼ばれるコールバック関数です
def options_for_select_from_enum(klass,column)
#該当クラスのEnum型リストをハッシュで取得
enum_list = klass.send(column.to_s.pluralize)
#Enum型のハッシュリストに対して、nameと日本語化文字列の配列を取得(valueは使わないため_)
enum_list.map do | name , _value |
# selectで使うための組み合わせ順は[ 表示値, value値 ]のため以下の通り設定
[ t("enums.#{klass.to_s.downcase}.#{column}.#{name}") , name ]
end
end
end
【補足説明】
t("ja.ymlのja:配下の階層を指定")
と指定することで日本語文字列を取得できます。これが可能なのは、configで探索開始位置を:ja
に設定しているからです(config.i18n.default_locale = :ja
)。設定していない場合、初期値は:en
なので、うまく参照できません。
【参考】ActiveRecord::Enumで定義された値のi18nをどう扱うか?
【参考】【Ruby】よく見かけるklassってなによ?
View定義
viewでの実装を2パターンほど考えてみました。
###selectタグ
先ほどのHelperメソッドを使用して、プルダウンメニューを定義します。
= form_with url:xxx, method: :post, local: true do | f |
= f.select :rate, options_for_select_from_enum(Rating, :rate)
= f.submit '評価する'
###radioボタン
こちらはHelperメソッドは使わずにLabelの日本語化だけ実施してみます。
= form_with url:xxx, method: :post, local: true do | f |
- Rating.rates.each do | rate |
= f.label t("enums.rating.rate.#{rate[0]}")
= f.radio_button :rate, rate[0]
= f.submit '評価する'
【補足説明】 パラメータの中身
これらのviewを使って得られるパラメータは以下の通りです。
ちゃんと名前定義として送られてるので、問題なく登録もできました。
[1] pry(#<TradesController>)> params
=> <ActionController::Parameters {"utf8"=>"✓",'…略…', "rate"=>"good", , "commit"=>"評価する", "controller"=>"xxxx", "action"=>"xxxx", "product_id"=>"xx", "id"=>"x"} permitted: false>
#まとめ
Enumは小回りの効く便利モジュールと言う感じでしょうか。
比較的少量の名前定義をサクッと追加したい場合、非常に役に立つように感じました。
ただ、i18n機能との相性はイマイチな気もします。
今後human_atribute_name
のような便利メソッドがenum用に提供されると良いなって少し思います。と言うか、私がi18nやja.ymlを知らなさ過ぎるだけかもしれません^^;
色々と調べていくと、深みにハマり自分が何を調べているのか分からなくなったり笑
間違いや考えが抜けているところなど色々とあるかもしれません。ご指摘、指南いただけると非常にありがたいです。
何卒宜しくお願い致します!!
#参考
以下のサイトを参考にさせていただきました。
ありがとうございます!
Ruby on Rails 4.1 リリースノート
[Ruby on Rails 5.2.3 ActiveRecord::Enum]
(https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/Enum.html)
Ruby on Rails master@a4ea17f ActiveRecord::Enum
Github:active_hash
Railsのgem 'active_hash'で都道府県データを作成してみた
【Ruby】よく見かけるklassってなによ?
ActiveRecord::Enumで定義された値のi18nをどう扱うか?