Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: enumに定義できない、ActiveRecord::Relationと競合する文字列の一覧

業務中に気付いた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の名前で気をつけたほうがいいのはrecordvalueあたりでしょうか。
enumのラベルだとsizetableあたりは使ってしまうかもしれません。
ラベルの方は_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というメソッドが存在します。

一方、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と同名のメソッドなので、エラーが発生します。

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_') } #=> []

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。