RailsのEnumで、「その他」をいい感じに扱ってみた & gemも作ってみた
環境
やりたかったこと
本来は自由記述であったフォームを、データ分析の観点からenumに書き換えたかった。一方で、enumでは拾いきれない外れ値も保存しておきたかった。
フォームを分けると、「その他」を選択した際と選択しなかった際、それぞれのバリデーションが問題になるので、データ分析しづらいレコードが混入しないように気をつける必要があった。
JSでフォームを動的に生成するのは、使いたい環境ではやや手間だったので、Railsのみで実装した。
以下は作りたかったフォームのイメージ。
設計
大きく分けて「カラムを分けない」「カラムを分ける」という、2つの設計が考えられました。それぞれ以下のような特徴があると考えられます。
カラムを分けない
メリット
- テーブル定義をスッキリしたままに保てる。
デメリット
- いい感じのhelperメソッドがない。
- アプリケーション側の可読性が下がりそう。
カラムを分ける
メリット
- Railsのレールに乗ることができそう。
デメリット
- カラムが増え、テーブル定義が複雑になってしまう。
今回はカラムを分けることにしました。
実装
マイグレーションファイル
create_table :hoge do |t| t.integer :reason t.string :other_reason end
モデル
class Hoge < ApplicationRecord validates :other_reason, fuga: true enum reason: { foo: 10, bar: 20, other_reason: 1000 } end
ビュー
<div> <%= form.select :reason, Hoge.reasons.keys %> </div> <div> <%= form.text_field :other_reason %> </div>
バリデータ
class FugaValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return unless record.send("#{attribute.to_s[6..-1]}?") if record.send(attribute.to_s + '?') record.errors.add(attribute, :blank) if value.blank? elsif value.present? record.errors.add(attribute, :present) end end end
バリデータにロジックを押し込めて、疎結合を実現しようとしています。しかし、バリデータがモデルについて知りすぎている(特にattribute.to_s[6..-1]
のあたりが残念)感があります。validate_eachの引数を変えられない以上は、エラーハンドリングでいい感じにするしかなさそうでした。
なお、複数カラムにまたがるvalidationなので、validatesではなくvalidateを使うべきなのかと、かなり悩みました。が、抽象化することを見越すならば、validatesメソッドを使った方が見通しが良くなりそうでした。(validateメソッドを使った場合は、Validatorの共通化がかなり難しそうでした)
gem作成
Moduleにするなら、ついでにgemを作ってしまおうと考えました。特に大きく変えた部分はありませんが、名前空間については注意が必要でした。 validatesメソッドの定義を見る限りでは、名前空間を切っていても動きそうなのですが、
"hoge_hoge::fuga_fuga".camelize # => "HogeHoge::fugaFuga"
となってしまい、validatorを探しに行ってくれません。少し気持ち悪いですが、
validates other_hoge, "HogeHoge::FugaFuga" => true
とするしかなさそうでした。(この例だとHogeHoge::FugaFugaValidatorを呼んでくれます)