Tech Racho エンジニアの「?」を「!」に。
  • 開発

belongs_to polymorphicで、モデル名が存在しない場合

社内で質問されたので回答を書いておきます。

以下のようなモデルがあったとき、attachable_typeに存在しないモデル名(たとえば"Hoge")が入っていたらどのような挙動をするのか?

class AttachedImage < ActiveRecord::Base
  belongs_to :attachable, :polymorphic => true
end

やってみればわかりますが、せっかくなのでソースを読んでみましょう。
当然、手元にrailsのソースコードはcloneしてありますよね?

git clone git://github.com/rails/rails.git
cd rails
git checkout v3.2.8

結論から言うと、該当のソースコードは、activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rbです。

belongs_toの定義から追いかけてみましょう。
このクラスはBelongsToPolymorphicAssociation → BelongsToAssociation → SingularAssociationと継承関係があります。

まず、belongs_toの定義はactive_record/associations.rb:1427です。

# active_record/associations.rb
def belongs_to(name, options = {})
  Builder::BelongsTo.build(self, name, options)
end

ActiveRecord::Associations::Builder::BelongsToのbuild(未定義なので先祖であるActiveRecord::Associations::Builder::Associationのself.build)が呼ばれます。
その後buildで内部で、define_accessorsを呼んでいますね。

# active_record/associations/builder/association.rb
def self.build(model, name, options)
  new(model, name, options).build
end

def build
  validate_options
  reflection = model.create_reflection(self.class.macro, name, options, model)
  define_accessors
  reflection
end

def define_accessors
  define_readers
  define_writers
end

def define_readers
  name = self.name
  mixin.redefine_method(name) do |*params|
    association(name).reader(*params)
  end
end

def define_writers
  name = self.name
  mixin.redefine_method("#{name}=") do |value|
    association(name).writer(value)
  end
end

belongs_toで定義したassociation名の自動メソッド(この例だとattachable)の定義が見つかりました。
つまり、association(name).reader(*params)が呼ばれるわけですね。association(name)はActiveRecord::Associationsで定義されていて、この場合だとBelongsToPolymorphicAssociationが生成されます。

# active_record/associations/singular_association.rb
def reader(force_reload = false)
  if force_reload
    klass.uncached { reload }
  elsif !loaded? || stale_target?
    reload
  end

  target
end

ちなみにこのtargetはsuperのattr_readerで定義されたもので、実体は@targetのアクセサです。
reloadの中でload_targetが呼ばれ、その中でfind_target?がtrueならfind_targetの結果が@targetに代入されます。

find_target?の定義を見てみましょう。

# active_record/associations/belongs_to_association.rb
def find_target?
  !loaded? && foreign_key_present? && klass
end

klassが呼ばれていますね。klassはBelongsToPolymorphicAssociationで再定義されていて、以下のようになっています。

# active_record/associations/belongs_to_polymorphic_association.rb
def klass
  type = owner[reflection.foreign_type]
  type.presence && type.constantize
end

ようやくたどり着きました。
ここのtype.constantize部分で、NameError: uninitialized constant Hogeというエラーが出ます。

※find_target?の定義の通り、attachable_idがblank(nilや空)の場合には、klassが呼ばれないので、そのままnilになります。
※ちなみに、エラーにならない場合、このままSingularAssociationのfind_targetに進み、AssociationScopeのscope→add_constratinsでid制限がかかったscopeが生成された後、そのscopeのfirstが取得されます。

あー長かった。

結論:NameErrorが発生します。


CONTACT

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