Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
205
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【Rails】Enumってどんな子?使えるの?

はじめに

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…と、数値の採番が変わります。

app/models/ratings.rb
class Rating < ApplicationRecord
  enum rate:    [ :good, :normal, :bad ]
       ~~~~~     ~~~~~~~~~~~~~~~~~~~~~~
      #カラム名        #定義対象の内容
end

パターン② 名前定義と対応する数値を指定

ハッシュで名前と数値の対応を指定して、定義することもできます。
色々探していると、この方法が最も一般的なのかな?と思います。

app/models/ratings.rb
class Rating < ApplicationRecord
  enum rate: { good: 0, normal: 1, bad: 2 }
       ~~~~~   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
      #カラム名   #定義対象の内容のマッピング(名前: 数値)
end

なお、公式リファレンス見てみると「一番最初に宣言した値をデフォルト値としてモデルに定義することを推奨」されています。多分こんな感じ。↓

db/migrate/yyyyMMddhhmmss_create_ratings.rb
class CreateRatings < ActiveRecord::Migration[5.2]
  def change
    create_table :ratings do |t|
      #...略
      t.integer :rate, defalut: 0  # ← enum定義前なので、ここでは名前定義使えません
      #...略
  end
end

null:falseを指定していたら問題ないようにも思いますが、例えば「ユーザが指定する項目でない」「自動生成されるレコード」「新たにカラムを追加する」など、指定しておくべきパターンも考えられるので、設定しておいた方が良さそうです。。

【補足説明】
他のページではBoolean型で宣言しているところがありましたが、公式に記載がないのと、試して見た結果うまくいかなかったので定義方法としてはオススメしません。。
ちなみに、どうなるかと言うと…

app/models/notifications.rb
class Notification < ApplicationRecord
    #boolean型の項目にenumで名前定義してみる
    enum checked: { yet: false, already: true }
end

この状態でコンソールから値の更新をしてみます。

console.ターミナル
[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

ちなみにalready1でしっかり更新されます。
また、データベースの値が0だった場合、しっかりyetで認識されます。
が、更新時にnil更新されるのでは…実用に耐えられない感じがしました(0ω0;)

定義済みのEnumを参照

実際に定義した値をコンソールで参照してみます。
参照方法はカラム名にsを付ければOK。何だか使い方がscopeみたいな感じで面白いですね。
因みに、定義方法に関わらず参照結果はハッシュ形式で返されてました。

console.ターミナル
# 定義済の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文を組み合わせる

列挙した種類ごとに処理をループしたいことって有りそうですよね。
(私はあった!)ので、少し試した結果が↓です。

console.ターミナル
#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が定義されたインスタンスを作成してみます。
これは更新時も同様ですね。

console.ターミナル
#(従来)数値で指定する
[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",                      # ← その通り認識されましたね
#…略

名前定義で指定ができるので、単語の意味合いから何を示すのか分かりやすいですね。
これで、かなり可読性をあげることが出来そうかな。
因みに、データベース上はちゃんと数値で保存されてます!
Image from Gyazo

②インスタンスの中身を確認する

①で作成したインスタンスを使ってEnumの中身を確認してみます。

console.ターミナル
[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を使用した場合、作成時と変わらないので説明を省略します。

console.ターミナル
[6] pry(main)> rate.rate      #変更前の状態を確認します
=> "good"                     #"good"ですね
[7] pry(main)> rate.normal!   #指定した名前定義で更新ができちゃう便利メソッド!
=> "normal"                   #"normal"に更新されました
[8] pry(main)> rate.rate      #念のため、更新後の状態も見てみます
=> "normal"                   #[7]で指定した"normal"に変更されました!

ステータス管理の項目だった場合、この方法で変更すると効率も可読性も向上しそうですね。なお、このメソッドは、実行時点でデータベースに反映さるようです。

④データベースを検索する

名前定義と同名のscopeが自動的に割り当てられるようです。
そのため、以下のようにデータベースの検索が可能になります。

console.ターミナル
[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(接尾辞)を名前定義に付与できます。
定義イメージは以下のとおり↓

app/models/ratings.rb
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を実際に参照してみるとこんな感じ↓

console.ターミナル
# _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であることを明示しつつ以下の通り定義します。

config/locales/ja.yml
ja:
  enums:              #enum型に対する日本語定義であることを明示するために記述
    rating:           #モデル名を指定
      rate:           #カラム名を指定
        good: 良い     #Enumの名前定義①
        normal: 普通   #Enumの名前定義②
        bad: 悪い      #Enumの名前定義③

Helperメソッドの作成

viewで扱いやすいように日本語化の処理をhelperメソッドとして切り出して用意しておきます。

app/helpers/enum_helper.rb
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メソッドを使用して、プルダウンメニューを定義します。

rating.html.haml
= form_with url:xxx, method: :post, local: true do | f | 
  = f.select :rate, options_for_select_from_enum(Rating, :rate)
= f.submit '評価する'

<画面イメージ>
スクリーンショット 2019-10-07 3.13.46.png
Image from Gyazo

radioボタン

こちらはHelperメソッドは使わずにLabelの日本語化だけ実施してみます。

rating.html.haml
= 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 '評価する'

<画面イメージ>
Image from Gyazo
Image from Gyazo

【補足説明】 パラメータの中身
これらのviewを使って得られるパラメータは以下の通りです。
ちゃんと名前定義として送られてるので、問題なく登録もできました。

console.ターミナル
[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
Ruby on Rails master@a4ea17f ActiveRecord::Enum
Github:active_hash
Railsのgem 'active_hash'で都道府県データを作成してみた
【Ruby】よく見かけるklassってなによ?
ActiveRecord::Enumで定義された値のi18nをどう扱うか?

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
205
Help us understand the problem. What are the problem?