【Rails入門説明書】enumについて解説
enumの基本
「enum」(日本語では列挙型)はRubyにはありませんが、最近になってRailsに導入された、とても便利な型です。
Rails 4.1 以上に対応しております。
文字列と値を関係づけてプログラムの可読性を上げるとともに、データベースに格納される値が規定の範囲を超えることがないようにすることもできます。
今回は、そんな、Railsで利用できる「enum」について、詳しく説明していきます。
他の言語のenumの基本的な特徴
Railsのenumの説明に入る前に、enumの一般的な特徴について、説明します。そのほうが、Railsのenumもイメージしやすいかもしれません。
※「すでに知っている」と言う人は読み飛ばしてください。
プログラムで状態や種別を表すため、それらに数値を割り当てることはよくあります。
例えば、0が「応答待ち」、1が「リクエスト中」、2が「処理中」、3が「入力待ち」と設定して、 状態を保持する変数statusに格納するような処理です。
しかし、プログラム上で数値だけを扱っていると、どうしても「どの数値が何を表しているのか」が分からなくなってきます。そのため、定数定義するなどして、数値に名前を付けて可読性を上げるのは常套手段です。
:
Waiting = 0
Requesting = 1
Running = 2
Queuing = 3
:
しかし、数値そのものに意味がない定数を定義するだけでは、新たな状態や種別の定義の追加時に、以下のように、たまたま同じ数値を使ってしまうようなリスクがゼロではありません。
:
Analyzing = 3
:
また、定数定義といってもただの数値の読み替えですので、変数statusに想定外の数値が代入されることを防ぎようがありません。
そんなときに、「enum型」が役に立つのです。
「enum型」ではその定義の中で名前を羅列すると、それらの名前に自動で値を割り当ててくれるのです。そのため、プログラマーはその値自体を意識する必要がありません。
そのうえ、定義の中のリストの追加や削除をするだけで、適切に(重複しないように)値を割り当て直してくれますし、割り当てられている値の範囲外の数値を代入しようとした場合、例外が発生するプログラム言語もあります。
つまり、「enum型」というのは、名前に数値を割り当てることで、プログラムコードの可読性を上げて、かつ不要な不具合を防ぐのに役立つものだと言えるでしょう。
改めて特徴をまとめておくと、以下のようになります。
・名前(文字列)のリストに一意の値が割り当てられる
・定義の順番通りに値が割り当てられ、追加や削除すると自動的に割り当て直される(値を指定することも可能)
・enum型の変数は、定義されている以外の値を受け付けない(例外が発生する)
Railsのenumとは
Railsのenumは、モデルに定義することができる型で、定義することで、データベースの特定のカラムの値(整数値)を、指定した文字列(シンボル)で扱うことができるようになります。
基本的な構文は、以下の2通りです。
enum カラム名: [ シンボル1, シンボル2, ,,, ] # (1) 値を自動に割り当て
enum カラム名: { 項目名1: 値1, 項目名2: 値2, ,,, } # (2) 固定値を割り当て
イメージとしては、データベースのカラムをenum型で宣言し、同時に格納できる項目(値)を定義していると考えれば良いでしょう。
※大前提として、モデルの親クラスとなるActiveRecordは、「データベースの1行分のデータを扱うクラス」です
例えば、カラム「status」について、以下のように定義します。
class User < ApplicationRecord
enum status:[:guest, :active, :vip, :freeze]
end
この場合、statusには、0から3の値しか格納されません。また、先頭のシンボルから順番に値が割り当てられ(:guestが0、:activeが1、:vipが2、:freezeが3)、次のように、プログラムコードでこれらのシンボルを利用することができるのです。
:
def check_user(status)
if status == :freeze
# ...凍結会員の場合
else
# ...生きている会員の場合
end
end
:
動かしてみよう
実際に環境を作って確認してみましょう。(以降、「enum_test」という名前の環境で説明を続けます)
環境の準備
ブラウザから値を確認しやすいので、今回はscaffoldを導入します。 詳細は【Rails入門説明書】scaffoldについて解説を参照していただきたいですが、コマンドとしては以下のコマンドを実行していきます。 [bash] > rails new enum_test > cd enum_test > rails generate scaffold Menu name:string main:integer drink:integer ><span>rails db:migrate</span> [/bash] 以上の操作で、メインディッシュと飲み物を登録するメニューモデルが出来上がり、データベース上にテーブルも生成されました。なお、メインディッシュと飲み物は、いろいろと変更がかかる可能性がありますので、整数で保持しています。 以下のコマンドでサーバーを起動して、ブラウザから「localhost:3000/menus」にアクセスしてください。 [bash] > rails s [/bash] データがないので寂しいですが、登録データ一覧の画面が表示されています。 「New Menu」のリンクから、3件ほどデータを入力しておきましょう。 これで準備ができました。
では、作った環境にenumを導入していきます。変更するファイルは以下のモデルファイルです。 /app/models/menu.rb このファイルを開いて、以下のように追記しましょう。 class Menu < ApplicationRecord enum main:[:beef, :fish, :pork, :lamb] # main dish end そして、「localhost:3000/menus」を表示しているブラウザでCTRL+F5キーを押下して再表示してください。 先ほどまで数値だったメインディッシュの表示が、enumで定義したシンボルと同じ文言になっています。同様に飲み物も定義してみましょう。 class Menu < ApplicationRecord enum main:[:beef, :fish, :pork, :lamb] # main dish enum drink:[:coffee, :tea, :beer, :wine] # drink end こちらも、同じように数値が定義したシンボル名に変更されています。
selectを使ってenumからリストを作る
では、もう1か所、ビューについても、enumを使った選択リストの形に修正しましょう。 修正するのは、以下のフォーム用のファイルです。 app/views/menus/_form.html.erb <%= form_with(model: menu, local: true) do |form| %> <% if menu.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(menu.errors.count, "error") %> prohibited this menu from being saved:</h2> <ul> <% menu.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= form.label :name %> <%= form.text_field :name %> </div> <div class="field"> <%= form.label :main %> <%= form.number_field :main %> </div> <div class="field"> <%= form.label :drink %> <%= form.number_field :drink %> </div> <div class="actions"> <%= form.submit %> </div> <% end %> このファイルは、新規追加と編集の画面、両方で使われているフォームですので、これだけ修正すれば、両方の画面に対応できます。 その中の、以下の2つのフィールドが、メインディッシュと飲み物の入力欄です。 : <div class="field"> <%= form.label :main %> <%= form.number_field :main %> </div> <div class="field"> <%= form.label :drink %> <%= form.number_field :drink %> </div> : それぞれ、selectフォームに変更しましょう。 : <div class="field"> <%= form.label :main %> <%= form.select :main, Menu.mains.keys, :selected=>Menu.mains[@menu.main] %> </div> <div class="field"> <%= form.label :drink %> <%= form.select :drink, Menu.drinks.keys, :selected=>Menu.drinks[@menu.drink] %> </div> : この変更で、数値入力ではなく、リストからの選択での入力に変更されています。もちろん、リストはenumに定義したシンボル名の文字列で構成されています。 以上のように、モデルに定義することで、定義したシンボルをビューでも利用することができるのです。
追加削除には注意
enumの特徴の1つとして、「値を自動的に割り当てる」機能があります。これは、項目の追加や削除をしても値が重複することがないため、一般的に、間違った値で処理してしまう不具合を防ぐ効果があります。 しかし、Railsの場合、データベースに格納されているのは、あくまでも整数値です。そのため、定義だけ追加削除してしまうと、本来のデータからずれてしまう問題が発生してしまいます。 enum_testで確認してみましょう。 飲み物の先頭に「greentea」を追加します。 class Menu < ApplicationRecord enum main:[:beef, :fish, :pork, :lamb] # main dish enum drink:[:greentea, :coffee, :tea, :beer, :wine] # drink end 一覧画面を再読み込みして見てみると、B-setに登録していないはずのgreenteaが表示されており、それ以外もA-setのteaがcoffeeになって、C-setのbeerがteaになってしまっています。つまり、1つずれてしまっているのです。 これは、項目を追加することで変化するのは定義の値割り当てだけで、データベース上の数値は変更されないことが原因です。そのため、数値とその意味を表す文言がずれるのです。 しかし、項目を変更するたびにデータベースの値を修正するのは本末転倒です。そのため、構文の(2)で紹介している、enumの定義を固定値の割り当てにする方法でこの問題を回避しましょう。 class Menu < ApplicationRecord enum main:[:beef, :fish, :pork, :lamb] # main dish enum drink:{ greentea: 4, coffee: 0, tea: 1, beer: 2, wine: 3 } # drink end 値を固定することで、自動割り当ての利点はなくなりますが、自由に追加削除できますので、利便性は上がります。このあたりは、トレードオフになるとも言えますが、1か所に定義しているので重複するリスクは小さく、利便性の向上の目利との方が大きい場合がほとんどでしょう。
enumの便利なメソッド
ここまで紹介したように、enumには次のような特徴があります。 ・シンボルのリストに0から順番の値が割り当てられる(値を指定することも可能) ・モデルに定義すれば、ビューからも参照でき、値ではなくシンボルの文字列を表示することができる ・項目を追加削除する場合は、シンボルへ割り当てられる値がずれてしまうため、固定値を指定してやる必要がある しかし、enumの魅力はそれだけではありません。
enum定義はハッシュ
まず、enumはそれだけでハッシュの定義になっています。 先ほど「app/views/menus/_form.html.erb」でnumber_fieldからselectへ変更した部分を見てください。 : <%= form.select :main, Menu.mains.keys, :selected=>Menu.mains[@menu.main] %> : 編集時に設定された値を表示するにしている、「selectedオプション」に登録しているのが、enum定義が配列になっていることを表しています。 : .. Menu.mains[@menu.main] .. : 「Menu.mains」が、enum定義を表しており、@menu.mainがデータベースに保存されている値のラベル(文字列)です。つまり、この部分は、enum定義のハッシュの、ラベルをキーとする値を取得する処理になっているのです。 なお、自動で値を割り付けしたものでも、値を固定にしたものでも、同じように値にアクセスすることができます。
enumのメソッド
モデル内で定義されたenumは、モデルを通じてデータベースのカラムとつながっています。そのため、それ自体が値を保持しているオブジェクトと同じイメージで、同じように扱うことができるようになっているのです。 つまり、モデルオブジェクトにenum定義することによって、該当するカラムの値の参照や設定などを行えるメソッドが追加されているわけです。
メソッド | 説明 |
---|---|
モデルクラス.カラム名の複数形 | enum定義をハッシュ形式で参照する |
モデルクラス.カラム名の複数形[項目名] | 項目名に割り当てられている値を取得する |
モデルクラス.項目名 モデルクラス.where(カラム名 項目名) | enum定義に項目名が含まれているかどうかを検索する |
モデルオブジェクト.カラム名 モデルオブジェクト[カラム名] | 該当カラムの値を項目名で取得する |
モデルオブジェクト.カラム名 = 値 モデルオブジェクト.項目名! | 該当カラムに、(項目名に割り当てられた)値を格納する |
モデルオブジェクト.項目名? | その行のenum定義されたカラムに、項目名に割り当てられた値があればtrue、なければfalse |
モデルオブジェクト.カラム名_before_type_cast | 該当カラムの値を参照する |
※モデルオブジェクトのインスタンスメソッドは、保持している行に対する操作、クラスメソッドは、enum定義に対する操作です
動作確認
enum_testの環境で、実際に動作を確認してみましょう。以下のコマンドで、コンソールを立ち上げてください。 [bash] > rails console [/bash] 前述のメソッドの動きを確認していきましょう。 (「> 」が入力行で、その下の「=>」の行がその出力です) [bash] > Menu.mains => {"beef"=>0, "fish"=>1, "pork"=>2, "lamb"=>3} [/bash] モデルクラスのクラスメソッドとして、カラム名(enum定義名)の複数形のメソッドが定義されており、該当のenum定義を返してくれます。 [bash] > Menu.mains["lamb"] => 3 [/bash] Menu.mainsはハッシュ形式を返してくれますので、キーである項目名を使って、対応した値を取得できます。 [bash] > Menu.fish => #<ActiveRecord::Relation [# <Menu id: 2, name: "B-set", main: "fish", drink: "coffee", created_at: "2018-09-19 11:15:23", updated_at: "2018-09-19 11:15:23">, # <Menu id: 4, name: "D-set", main: "fish", drink: "coffee", created_at: "2018-09-21 11:31:58", updated_at: "2018-09-21 11:31:58">]> [/bash] テーブルを検索して、指定した項目を含む行のデータを取得することができます。複数の行が該当する場合は、すべて取得できます。 ここからはオブジェクトが必要ですので、newメソッドを使って取得します。 [bash] > menu = Menu.new(main: :pork, drink: :tea) => # <Menu id: nil, name: nil, main: "pork", drink: "tea", created_at: nil, updated_at: nil> [/bash] このとき、データベースに格納する値を「カラム名 項目名のシンボル」で指定することができます。メインディッシュは「pork」、飲み物は「tea」にしてみましょう。 [bash] > menu.drink => "tea" [/bash] モデルオブジェクトを介して、そのオブジェクトが保持しているデータベースの該当カラムの値を項目名で取得します。ここでは、drinkカラムに格納した"tea"を取得しました。 [bash] > menu.wine! => true > menu.drink => "wine" [/bash] 項目名!メソッドは、その項目を含むenum定義のカラムの値を、項目名に割り当てられた値に書き換えます。カラム名を指定する必要がない点が気になるかもしれませんが、他のカラムであっても同じ項目名は使えませんので、間違ったカラムの値を書き換えてしまうことはありません。もちろん、存在しない項目名の場合は、エラーが発生します。 また、カラム名メソッドを利用して、何が格納されているのかを取得することもできます。 [bash] > menu.wine? => true > menu.tea? => false [/bash] 項目名?のメソッドで、その項目名がオブジェクトの示す行にあるかどうかを取得できます。 [bash] > menu.drink_before_type_cast => :tea [/bash] カラムに格納されている値を取得する方法は、もう1つあります。こちらは、シンボルを取得しています。
日本語対応は?
これまで、項目名はアルファベット(正確にはアスキー文字)だけを指定していました。しかし、Railsのenumは少し工夫するだけで日本語(全角文字)を使うことができるようになりますので、その方法を紹介しましょう。 なお、日本語を含めて、言語を多言語化(国際化)することをi18nと呼びます。 これは英語の「Internationalization(国際化)」という単語が、最初のiと最後のnの間に18文字挟まっている長い単語であるために使われている略称です。 つまり、i18nと書いて「Internationalization」を表しているわけです。 じつは、Railsには、enumをi18n対応させるgem「enum_help」が用意されています。 そのため、enumの国際化は、簡単に行うことができるのです。 その手順は、以下の4ステップです。
少し詳しく説明していきます。
enum_helpのインストール
まずは、enumをi18n対応させるgem「enum_help」をインストールします。 Gemfileに、以下の1行を追記してください。 [bash] gem 'enum_help' [/bash] そして、以下のコマンドでgemをインストールします。 ※なお、サーバーが起動している場合は、一度終了させてください [bash] bundle install [/bash] 「Bundle complete!」を含む節が表示されれば、enum_helpのインストールは完了です。
localeファイルの作成
localeファイルは、翻訳用のファイルです。 実際には、enumに定義する項目名に対応した日本語を設定するファイルです。そのため、項目数と同じだけの翻訳情報を登録しておく必要があります。 テキストエディタなどで、以下のファイルを作成し、「ja.yml」というファイル名で、「config/locales/」に格納してください。 [xml] ja: enums: menu: main: beef: 牛肉 fish: 魚 pork: 豚肉 lamb: 羊肉 drink: greentea: 緑茶 coffee: コーヒー tea: 紅茶 beer: ビール wine: ワイン [/xml] ※必ずUTF-8で保存してください。
defaultのlocale設定の変更
準備の最終段階は、言語(locale)設定です。
なにも設定しなければ、言語は英語になっているため、それを日本語に変更してやります。
「config/application.rb」のApplicationクラス内に、以下を追記します。
config.i18n.default_locale = :ja
「:ja」が、先ほど作ったja.ymlファイルを表しています。
i18n対応の記載を行う
前述までで、i18n化の準備が整いましたので、プログラムコードを書き換えましょう。
書き換えるのは、enum定義を使う箇所で、カラム名の末尾に「_i18n」を付与することで、表示される項目名が日本語に変わります。
では、先ほどrails consoleで行ったメソッドの中でカラム名のメソッドの動作確認をしてみましょう。
[bash]> Menu.mains_i18n
=> {“beef”=>”牛肉”, “fish”=>”魚”, “pork”=>”豚肉”, “lamb”=>”羊肉”}
> Menu.mains_i18n[“lamb”]
=> “羊肉”
> menu = Menu.new(main: :pork, drink: :tea)
=> #
<Menu id: nil, name: nil, main: “pork”, drink: “tea”, created_at: nil, updated_at: nil>
> menu.drink_i18n
=> “紅茶”
[/bash]
以上のように、enum_helpを用いることで、簡単に日本語化することが可能なのです。
まとめ
今回は、Railsのenumについて説明しました。
Railsのenumは他のプログラム言語のenumと同様の「可読性を上げる」機能を持ちつつ、Webアプリケーションらしく、データベースに保持されたデータの表示の可読性も上げてくれます。
また、i18n対応も簡単にできるgemも準備されていますので、Webアプリケーションの見た目も、簡単に国際化することができる大きな利点もあります。
いろいろなツールやテクニックもありますが、enumもその1つとして活用し、だれでも読みやすく、ミスの少ないプログラムを目指していきましょう。
・enumは、意味のない数値を隠して意味のある文字列やシンボルをプログラムコードでつかるようにしてくれる
・enumを使うことで、可読性と安全性が上がる
・enumの定義はモデル内で行う
・enumはデータベースのカラムに保存される値に対して定義する
・enum定義することで、便利なメソッドが使えるようになる
・i18nは、「Internationalization」の略=国際化
・enumで定義した項目名は、「enum_help」(gem)を使うことで、簡単にi18n対応ができる