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が発生します。

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

baba

ゆとりプログラマー。

高校時代から趣味でプログラミングを初め、そのままコードを書き続けて現在に至る。慶應義塾大学環境情報学部(SFC)卒業。BPS設立初期に在学中から参加している最古参メンバーの一人。Ruby on Rails、PHP、Androidアプリ、Windows/Macアプリ、超縦書の開発などを気まぐれにやる。軽度の資格マニアで、情報処理技術者試験(15区分 + 情報処理安全確保支援士試験)、技術士(情報工学部門)、CITP、Ruby Programmer Goldなどを保有。

babaの書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ