5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby on Rails その2Advent Calendar 2018

Day 23

RailsのEnumで複数項目を選択可能にするGemを作ってる話

Last updated at Posted at 2018-12-23

つまり・・・どういうこと?

一言で言うと、「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で使える機能を可能な限り再現
5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?