つまり・・・どういうこと?
一言で言うと、「enumで複数項目を選択できる風にするRails Plugin」。
リポジトリ(未完成。gemには登録してないので、installするにはgithubから)
https://github.com/EastResident/any_enums
何を言ってるのか分からないと思うので、以下のサンプルコードで雰囲気を感じて下さい
class Engineer < ApplicationRecord
include AnyEnums
any_enums field: { server: 1, front: 2, infra: 3 }
end
engineer = Engineer.server_or_front.new
p engineer.field # => "server_or_front"
p engineer.fields # => ["server", "front"]
開発思想
例えば、 ApplicationRecordを継承するEngineerモデルがあるとしましょう。Engineerモデルはfieldと言う属性を持っており、これはserver、front、infraのいずれかの値が入ります(説明するまでもないですが、大雑把な専門分野のイメージです)。
Engineerのインスタンスはfieldを一つだけではなく、複数持つ場合もあるとします(フルスタックエンジニアとかあるしね)。
このようなデータ構造を持たせる場合、一般的にRailsなら中間テーブルを挟んだ多対多のリレーションを作るかもしれません。
class Engineer < ApplicationRecord
has_many :engineer_fields
has_many :fields, through: :engineer_fields
end
class Field < ApplicationRecord
has_many :engineer_fields
has_many :engineers, through: :engineer_fields
end
class EngineerField < ApplicationRecord
belongs_to :engineer
belongs_to :field
end
field属性が今後も増えていくなら、この構造は合理的だと思います。
しかし、field属性の種類が今後も増える予定がなく、3項目のままだとしたらどうでしょうか?
Engineerモデルにfield属性を持たせるために、モデル(とテーブル)を二つも増やすのは少し大げさな気もします。
このようなケースを解決する選択肢の一つを提供するのが、このany_enumsです。
使い方
定義
普通、enumは以下のように定義すると思います。
普通のenum定義
class Engineer < ApplicationRecord
enum field: { server: 1, front: 2, infra: 3 }
end
any_enumsでは、これを以下のように変更します。
any_enumsの定義
class Engineer < ApplicationRecord
include AnyEnums
any_enums field: { server: 1, front: 2, infra: 3 }
end
AnyEnumsをincludeして、enumをany_enumsに置き換えるだけですね。
インスタンスの作成
例えば、通常のenumでfieldがserverのEngineerモデルのインスタンスを作成するには以下のようにします。
engineer = Engineer.server.new
engineer.save
そして、fieldの値を取得する場合はこんな感じですね。
p engineer.field # => "server"
p engineer.field_before_type_cast # => 1
では次にany_enumsの機能を使って、serverとfront両方をfieldに持つEngineerのインスタンスを作成します。
engineer = Engineer.server_or_front.new
engineer.save
ここでengineerインスタンスは、fieldメソッド・field_before_type_castメソッドの他に、fields・fields_before_type_castメソッドが使えるようになっています
p engineer.field # => "server_or_front"
p engineer.fields # => ["server", "front"]
p engineer.field_before_type_cast # => 110
p engineer.fields_before_type_cast # => [1, 2]
先ほどインスタンスを作成する際に、server_or_front.newとしましたが、これはfront_or_server.newとしても動作します(ただし、enumの定義順にorで繋ぐことが推奨されます)。
どちらの方法でインスタンスを作成しても、fieldに入る値は同じになります。
宣言はどちらでもOKだが、入る値は同じ
engineer = Engineer.server_or_front.new
engineer = Engineer.front_or_server.new
仕組み
前項で使い方を説明した際に、以下のような記述をしました。
engineer = Engineer.server_or_front.new
p engineer.field # => "server_or_front"
p engineer.field_before_type_cast # => 110
お気付きだと思いますが、any_enumsは裏側で{server_or_front: 110}と言うような割り当てを行っています。では、どのようなルールでserver_or_frontが110に割り当てられたのでしょうか?
any_enumsは独自のハッシュテーブルを保持しており、0〜9の整数からなる全ての組み合わせに対して、101以上の整数を割り当てています。
[0, 1]=>101,
[0, 2]=>102,
[0, 3]=>103,
〜〜
[4, 6, 7, 8]=>467,
[4, 6, 7, 9]=>468,
〜〜
〜〜
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] => 1113
御察しの通り、上記の機構を持つ理由からany_enumsを使用するには制限があります。
まず、any_enumsには0から9までの整数しか割り当てられません。また、101から1113までの整数が自動的に割り当てられる可能性があるため、割り当てが非推奨になります。このような仕様にした理由としては、自分の経験から、一つのカラムに対して10以上の値を列挙することはないと判断したからです(経験上、多くて5〜6個)。
今後の展望
- formで利用するview helperの作成
- 通常のenumで使える機能を可能な限り再現