業務中に気付いたtipsです。
enumにできない文字列の条件は色々あるようですが、この記事ではActiveRecord::Relation
との競合によって定義できないものだけ調査しています。
ここに登場する文字列を避けても必ずenumを定義できるわけではないです。
環境は以下の通りです。
- Ruby: 2.7.1
- Rails: 6.0.3.4
⚓ 競合によってenumに定義できない文字列の一覧
前提として、enum hoge: %i(fuga)
が定義されているとき、hoge
を「enumの名前」、fuga
を「enumのラベル」と呼ぶことにします。
- 以下の文字列は、enumの名前として定義することができません(書かれないと思いますが、
pluralize
したものも定義できません)
_substitute_value
exec_query
joined_includes_value
load_record
preload_association
record
update_counter
value
- 以下の文字列は、enumのラベルとして定義することができません
ActiveRecord::Relation
で定義されているインスタンスメソッドと同名の文字列- 正確には、
ActiveRecord::Relation.instance_methods(false)
またはActiveRecord::Relation.private_instance_methods(false)
のどちらかに含まれている文字列 - 多すぎるのでgistに列挙しました
- 正確には、
これらの文字列一覧は、Railsの更新に伴って変更になる可能性があります。
enumの名前で気をつけたほうがいいのはrecord
、value
あたりでしょうか。
enumのラベルだとsize
、table
あたりは使ってしまうかもしれません。
ラベルの方は_prefix
や_suffix
などのオプションを使えばそのまま定義できますが、名前の方は別名にするのが良さそうです。
⚓ 定義不可となる例①
class QualitativeReport < ApplicationRecord
enum value: %i(good normal bad)
end
上述した通り、enumの名前にvalue
は定義できません。
インスタンスの生成などを試すとエラーが起きます。
[1] pry(main)> QualitativeReport.new
ArgumentError: You tried to define an enum named "value" on the model "QualitativeReport", but this will generate a class method "values", which is already defined by ActiveRecord::Relation.
⚓ 定義不可となる例②
class ProductAppearanceRecord < ApplicationRecord
enum survey_type: %i(size weight)
end
ActiveRecord::Relation
のインスタンスメソッドと同名であるため、enumのラベルにsize
は定義できません。
[1] pry(main)> ProductAppearanceRecord.new
ArgumentError: You tried to define an enum named "survey_type" on the model "ProductAppearanceRecord", but this will generate a class method "size", which is already defined by ActiveRecord::Relation.
⚓ 原因
enumの定義に伴い生成されるメソッドと、ActiveRecord::Relation
のインスタンスメソッドが競合しないよう、チェックが走っているためです。
以下のissueを解決するために追加された実装のようです。
⚓ enumの名前をチェックする過程
ActiveRecord::Relation
にはActiveRecord::Relation#values
というメソッドが存在します。
- APIドキュメント:
values
-- ActiveRecord::Relation
一方、ActiveRecord::Enum#enum
では引数をpluralize
したクラスメソッドが定義されます。「定義不可となる例①」だとQualitativeReport::values
です。
# activerecord/lib/active_record/enum.rb#L163
singleton_class.define_method(name.pluralize) { enum_values }
このクラスメソッド定義の直前に、QualitativeReport::values
を定義することでメソッド名が競合しないかどうかが検出されます。
# activerecord/lib/active_record/enum.rb#L162
detect_enum_conflict!(name, name.pluralize, true)
この検出過程でActiveRecord::Relation
に同名のメソッド#values
がないかチェックされるため、ActiveRecord::Enum
がエラーを起こします。
# activerecord/lib/active_record/enum.rb#L248-L249
elsif klass_method && method_defined_within?(method_name, Relation)
raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
⚓ enumのラベルをチェックする過程
ラベルについてもenumの名前と同じような過程でチェックされています。
今回はActiveRecord::Enum
がラベル名と同名のスコープを定義しようとして、その直前に競合の検出が走ります。「定義不可となる例②」だとProductAppearanceRecord::size
についてチェックが行われます。
# activerecord/lib/active_record/enum.rb#L205-L206
klass.send(:detect_enum_conflict!, name, value_method_name, true)
klass.scope value_method_name, -> { where(attr => value) }
これがActiveRecord::Relation#size
と同名のメソッドなので、エラーが発生します。
- APIドキュメント:
size
-- ActiveRecord::Relation
⚓ ActiveRecord::Relation
と競合する文字列を調べる
メソッド競合の検出過程では、以下のメソッドによって特定のクラスでメソッドが定義済みであるかチェックされています。
# activerecord/lib/active_record/attribute_methods.rb#L103-L113
# klass: ActiveRecord::Relation
def method_defined_within?(name, klass, superklass = klass.superclass)
if klass.method_defined?(name) || klass.private_method_defined?(name)
if superklass.method_defined?(name) || superklass.private_method_defined?(name)
klass.instance_method(name).owner != superklass.instance_method(name).owner
else
true
end
else
false
end
end
ActiveRecord::Relation
のインスタンスメソッドとして定義されていればtrue(可視性は問わない)ActiveRecord::Relation
のインスタンスメソッドとして呼び出し可能でも、ActiveRecord::Relation
で定義されておらず、superklass
以上の階層で定義されていればfalse
この条件を考慮して、ActiveRecord::Relation
と競合するメソッド名について調べた結果が、冒頭に記載した文字列一覧です。
調査に使ったコードも一応掲載しておきます。
# enumの名前で定義不可となる文字列
# -> pluralizeするとActiveRecord::Relationが定義するインスタンスメソッドと同名になるもの
# https://github.com/rails/rails/blob/v6.0.3.4/activerecord/lib/active_record/enum.rb#L162
methods = ActiveRecord::Relation.instance_methods(false) + ActiveRecord::Relation.private_instance_methods(false)
plural_method_names = methods.map(&:to_s).select do |method_name|
method_name == method_name.singularize.pluralize
end
plural_method_names.map(&:singularize).sort!
# enumのラベルで定義不可となる文字列
# -> ActiveRecord::Relationが定義するインスタンスメソッドと同名になるもの
# https://github.com/rails/rails/blob/v6.0.3.4/activerecord/lib/active_record/enum.rb#L205
methods = ActiveRecord::Relation.instance_methods(false) + ActiveRecord::Relation.private_instance_methods(false)
methods.map(&:to_s).sort!
# not_ がprefixとなるインスタンスメソッドもチェックされてはいますが、ActiveRecord::Relationには存在しないため冒頭では言及していません
# https://github.com/rails/rails/blob/v6.0.3.4/activerecord/lib/active_record/enum.rb#L208
methods.map(&:to_s).select { |method_name| method_name.start_with?('not_') } #=> []