やりたいこと
Webサービスでもアプリでもユーザに何かを選択させたい場合、選択ボックスを使うと思うが、その選択肢をどこでどうやって管理するのがベストなのかを考察してみる。
例えば、以下のようなHTMLで表現されるように、ユーザへの何らかの情報の配信頻度を選択させる場合に、毎月/毎週/毎日等の選択肢をどこで、どう管理するのか。という問題を対象に検討してみます。
<form action="/users/id" method="POST">
<select name="frequency_id">
<option value="1">毎月</option>
<option value="2">毎週</option>
<option value="3">毎日</option>
</select>
</form>
Railsの場合、セレクトボックスを作るフォームヘルパーがあります。
= select オブジェクト名, プロパティ名, 選択肢の配列
または、オブジェクトに対するフォームビルダーを使用すると、
= form_with model: オブジェクト変数 do |f|
= f.select プロパティ名, 選択肢の配列
と簡単にセレクトボックスを作ることができるのです。
ちなみに、Rails7です。
Rails6以前のバージョンの場合、以下のように読み替えてください。
- form_with model: @user
+ form_for @user
よって、「どうやって選択肢を管理するのか」という問題を検討するためには、以下の2つの問題に分けてそれぞれ検討する必要があります。
- 選択肢の配列を生成する時、どこからデータを持ってくるのか
- 要件が変更して選択肢が増減した時、どうやって選択肢を追加、削除するのか
補足
ちなみに、セレクトボックスのヘルパーはたくさんあるので、以下に使い分けをまとめておきます。
- パターン1:selectタグ+optionタグ
- パターン2:collection_selectヘルパー
なお、結論を言うとパターン2をお勧めします。
パターン1:selectタグ+optionタグ
パターン1の場合はselectタグを生成するヘルパーとoptionタグを生成するヘルパーを組み合わせます。
複数ヘルパーがあるので非常に紛らわらしいです。
No | ヘルパーメソッド | 使い分けのポイント |
---|---|---|
1 | select_tag 要素名, options_for_select(配列orハッシュ) |
あまり使わない。単純にHTMLを生成。引数の配列orハッシュに指定の形式の配列を与えないといけないので面倒。 |
2 | select_tag 要素名, options_from_collection_for_select(オブジェクトの配列, value属性にセットしたいオブジェクトの属性, 表示に使用するオブジェクトの属性) |
モデルがあるのであれば、No1より遥かにスマート。 |
3 | select オブジェクト名, プロパティ, options_from_collection_for_select(オブジェクトの配列, value属性にセットしたいオブジェクトの属性, 表示に使用するオブジェクトの属性) |
フォームのモデルと対応しているならNo2よりもスマート |
パターン2:collection_selectヘルパー
パターン2は一つのメソッドでselectタグもoptionタグも生成できるので、よりスマート。基本的にはこちらを使って行きたいです。
= collection_select オブジェクト名, プロパティ名, オブジェクトの配列, value属性にセットしたいオブジェクトの属性, 表示に使用するオブジェクトの属性
selectヘルパーについては、以下に使い方があります。
比較案
どこで・どうやっての管理方法をいくつか考えてみました。
- 案1-1:モデルで管理する方法(専用テーブル)
- 案1-2:モデルで管理する方法(列挙体) ←2018/6/5追加
- 案2-1:ヘルパークラスで定数を管理する方法(列挙体もどき)
- 案2-2:ActiveHashで管理する方法←2018/12/14追加
- 案3:ファイルで管理する方法
案の概要
案1-1:モデルで管理する方法(専用テーブル)
配信頻度を表すモデルとテーブルがあれば、簡単に選択肢の配列を作成できます。
例えば、Frequencyというモデルを作成し、以下のようにテーブルを作ってデータを投入します。
class マイグレーションクラス名 < ActiveRecord::Migration[7.0]
def change
create_table :frequencies do |t|
t.string :name, null: false
t.timestamps
end
end
end
Frequency.create(name: "毎月")
Frequency.create(name: "毎週")
Frequency.create(name: "毎日")
そしてusersテーブルには、ユーザごとの配信頻度を表す属性を追加し、frequenciesテーブルのidを外部キーとして設定します。
class マイグレーションクラス名 < ActiveRecord::Migration[7.0]
def change
add_reference :users, :frequency
end
end
こうすれば、以下の通りになります。
= collection_select :users, :frequency_id, Frequency.all, :id, :name
または、Userオブジェクトに対するフォームビルダーを使用すると、
= form_with model: @user, method: :patch do |f|
= f.collection_select :frequency_id, Frequency.all, :id, :name
案1-2:モデルで管理する方法(列挙体)
2018/6/4追記
モデルに列挙型を定義できることを知ったので追記しておきます。
案1-1は頻度を格納するテーブルをわざわざ作りましたが、この案の方法では不要です。ActiveRecordには今回のようなただのコード表、つまり列挙型の定数を定義するための仕組みがあります。
頻度の属性を持たせたいのはユーザなので、usersテーブルに属性を追加し、その属性に対してuserモデル側にどんな値を取り得るのかを定義するのです。
class User < ApplicationRecord
enum frequency: { "毎月" => 0, "毎週" => 1, "毎日" => 2 }
end
対応するデータベースは整数型でデフォルト0(毎月がデフォルト設定とする)のカラムを定義しておきます。
class マイグレーションクラス名 < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.integer :frequency, default: 0
end
end
end
こうすれば、以下の通りになります。
= select :frequency, options_for_select(User.frequencies)
または、Userオブジェクトに対するフォームビルダーを使用すると、
= form_with model: @user, method: :patch do |f|
= f.select :frequency, options_for_select(User.frequencies)
列挙型を使えば、値の一覧を複数形で指定することでハッシュとして返してくれるメソッドが使える点が便利です。
しかもそのままoptions_for_select
メソッドに引き渡せられます。
[1] pry(main)> User.frequencies
=> {"毎日"=>0, "毎週"=>1, "毎日"=>2}
案2-1:ヘルパークラスで定数を管理する方法(列挙体もどき)
案1はデータベース≒モデルで選択肢を管理させるのに対し、案2は専用のクラスで選択肢を管理する方法です。
つまり、選択肢を管理するためのクラスを作成し、そこに定数として選択肢を定義する方法です。
JavaでいうところのEnumクラスな感じです。ということで案1-2に対して列挙体もどきです。
# パターン1
class Frequency
MONTHLY = "毎月".freeze
WEEKLY = "毎週".freeze
DAILY = "毎日".freeze
# わざわざこんなメソッドを定義するのは、以下の理由から。
# 1:定数を全部返すメソッドconstantsはセットされた値(毎月等)だけしか返せずoptions_for_selectにはそのまま使えないから
# 2:options_for_selectメソッドはkey, valueという並びの配列またはハッシュが必要だから
def self.constant_values
constants.map do |c|
[Frequency.const_get(c), c]
end
end
end
# パターン2
class Frequency
# パターン1で必要だったメソッドを省略するために、このパターンでは最初からハッシュで定義しておきます。
# このハッシュは変更されては困るので中も外もfreezeを使ってイミュータブルなオブジェクトにしておきます。
statuses = { "毎月" => "MONTHLY".freeze,
"毎週" => "WEEKLY".freeze,
"毎日" => "DAILY".freeze }.freeze
end
こうすれば、以下の通りになります。
// パターン1
= select :users, :frequency, options_for_select(Frequency.constant_values)
// パターン2
= select :users, :frequency, options_for_select(Frequency.statuses)
または、Userオブジェクトに対するフォームビルダーを使用すると、
= form_with model: @user, method: :patch do |f|
// パターン1
= f.select :frequency, options_for_select(Frequency.constant_values)
// パターン2
= f.select :frequency, options_for_select(Frequency.statuses)
実際にusersテーブルに値をセットするには、コントローラにてUser.create(frequency: params[:frequency])
のようにしておけばいいですね。
案2-2:ActiveHashで管理する方法←2018/12/14追記
ActiveHashという読み取り専用データベースにハッシュのようにアクセスするような感覚で使えるgemがあります。
active_hash gem
早速、使ってみましょう。
class Frequency < ActiveHash::Base
include ActiveHash::Enum
self.data = [
{ id: 1, name: '毎月' },
{ id: 2, name: '毎週' },
{ id: 3, name: '毎日' },
]
end
すると、このデータには案1-1と同じようにアクセスできます。
= collection_select :users, :frequency_id, Frequency.all, :id, :name
案3:ファイルで管理する方法
案2とあまり変わりませんが、クラスで定義するのではなく、外部定義ファイルで管理する方法もあります。
Ruby標準ライブラリでは様々なファイル形式を扱うことができます。
案3-1 XMLの場合
例えばXMLを使用した場合は、以下のようにrexmlライブラリを読み込んで使用します。
<?xml version="1.0" encording="UTF-8" ?>
<root>
<frequency>
<item value="month">毎月</item>
<item value="week">毎週</item>
<item value="day">毎日</item>
</frequency>
</root>
require "rexml/document"
class Frequency
def self.get_values
doc = REXML::Document.new(open("frequency.xml"))
doc.get_elements("root/frequency/item").map do |e|
[e.text, e.attribute("value").value]
end
end
end
こうすれば、以下の通りになります。
= select :users, :frequency, options_for_select(Frequency.get_values)
または、Userオブジェクトに対するフォームビルダーを使用すると、
= form_with model: @user, method: :patch do |f|
= f.select :frequency, options_for_select(Frequency.get_values)
こちらも実際にusersテーブルに値をセットするには、案2と同じです。
案3-2 YAMLの場合
RubyといえばYAML、という感覚をもっているので、以下のようにもっと簡単に管理することも可能です。
---
毎日: daily
毎週: weekly
毎月: monthly
- require "yaml" # viewにrequireはダサいので、実際はヘルパーとかデコレーターを使ったりすると良い
- frequencies = YAML.load_file("./frequency.yml") # ファイルパスは実際の環境に合わせる
= select :users, :frequency, options_for_select(frequencies)
メリデメ
観点 | 案1-1 | 案1-2 | 案2 | 案3-1 | 案3-2 | 評価軸 |
---|---|---|---|---|---|---|
エレガントさ | ◯ | ◎ | × | △ | ○ | 単純にコードのエレガントさを評価するもの |
保守性 | ◎ | ◯ | × | △ | ○ | 習得やデバッグの容易性を評価するもの |
拡張性 | ◎ | △ | × | △ | △ | 要件変更時の対応に対する柔軟性を評価するもの |
総評 | ◎ | ◯ | × | △ | ○ | 上記観点での総合評価 |
案1-1:MVCアプリケーションであればやはりモデル、DBで管理し、データのありかを一箇所に集めた方が何かと管理しやすいですね。しかも、独立した専用テーブルにすることで再利用性が高まります。また、「毎月を表す値は0じゃなくてMにしたい」といった値を変える操作も造作もないです。他の案だと、usersテーブルの全レコードをUPDATEかけないといけなくなります。
案1-2:案1-1と比較してDBテーブルを作成しなくて済み、設計と実装がシンプルになる点が効果が大きいです。ただし、特定のモデルの特定の属性にだけenum
という形で値を定義するため、再利用できず拡張性に乏しいのがマイナスです。また、コード値(毎月は0、毎週は1等)の変更に弱いです。
案2:新たなクラスに直接書く(ハードコーディングする)とコードの無影響確認(ノンデグレードテスト、リグレッションテスト)の範囲が広がるのがマイナス評価。案1-2もハードコーディングしていますが、ActiveRecordの標準機能という点では品質に懸念はありません。これもまた、コード値の変更に弱いです。
案3:ハードコーディングしていない分、保守性や拡張性は案2より高評価。案3を採用するには、その値を誰がどんなユースケースでメンテするかがポイント。例えば、画面から管理者が動的に変更できるようにする要件があるなら、DB管理のほうが良い。ファイルはトランザクションが無いし、再読み込みが必要だし、メリットがない。一方で、単独のソフトウェアパッケージだとしたら、設定ファイルという形で提供できるので、案1より案3の方がフィットしそう。
保守性や再利用性を考慮するならば案1-1。特に動的に変更する必要はなく、変更する場合は十分な検証がなされるなら案1-2。パッケージソフト等で配布する場合は案3。
参考
ちなみに、タイムゾーンや国を選択させる場合はRails標準でtime_zone_selectヘルパーメソッドが用意されているので、そちらを使うことも検討ください。
Railsガイド
以上