-1

I have a complicated case where I'm developing a Rails 3 Engine and I only intermittently get the error in the title. Here's the stacktrace:

ActiveRecord::ConfigurationError - Association named 'whatever' was not found; perhaps you misspelled it?:
  activerecord (3.2.18) lib/active_record/associations/preloader.rb:150:in `block in records_by_reflection'
  activerecord (3.2.18) lib/active_record/associations/preloader.rb:146:in `records_by_reflection'
  activerecord (3.2.18) lib/active_record/associations/preloader.rb:139:in `grouped_records'
  activerecord (3.2.18) lib/active_record/associations/preloader.rb:130:in `preload_one'
  activerecord (3.2.18) lib/active_record/associations/preloader.rb:109:in `preload'
  activerecord (3.2.18) lib/active_record/associations/preloader.rb:98:in `block in run'
  activerecord (3.2.18) lib/active_record/associations/preloader.rb:98:in `run'
  activerecord (3.2.18) lib/active_record/relation.rb:181:in `block in exec_queries'
  activerecord (3.2.18) lib/active_record/relation.rb:180:in `exec_queries'
  activerecord (3.2.18) lib/active_record/relation.rb:160:in `block in to_a'
  activerecord (3.2.18) lib/active_record/explain.rb:41:in `logging_query_plan'
  activerecord (3.2.18) lib/active_record/relation.rb:159:in `to_a'
  activerecord (3.2.18) lib/active_record/relation/delegation.rb:39:in `+'
  /Users/me/src/appointment_engine/app/controllers/appointment_engine/appointments_controller.rb:42:in `block (3 levels) in index'
  /Users/me/src/appointment_engine/app/controllers/appointment_engine/appointments_controller.rb:41:in `block (2 levels) in index'
  actionpack (3.2.18) lib/action_controller/metal/mime_responds.rb:196:in `respond_to'
  /Users/me/src/appointment_engine/app/controllers/appointment_engine/appointments_controller.rb:12:in `index'

To summarize: There's a model named Appointment in the engine which is polymorphically associated with has_many :through to the host app's User model (this is a requirement because we also associate to another model). Here's the has_many declaration in Appointment

class Appointment < ActiveRecord::Base
  has_many :scheduleables,
    through: :appointments_scheduleables,
    source_type: KAE.scheduleable_class.name
  has_many :schedulers,
    through: :appointments_schedulers,
    source_type: KAE.scheduler_class.name
end

Here I ran into my first problem; I need to set the :source_type on has_many :through polymorphic associations (It doesn't work without it) and for that I need to know the class of the associated model but, when my engine's Appointment model loads it does so before the host app's User model loads and therefore my engine's module KAE hasn't received the value for KAE.scheduleable_class yet.

Here's how KAE receives that value:

# in host app
class User < ActiveRecord::Base
  acts_as_scheduler
end

I wrote acts_as_scheduler as an AR mixin, it will declare the has_many :through association to Appointment.

My first attempt to fix this: I put the Appointment's has_many declaration in a hook inside a railtie:

ActiveSupport.on_load :after_initialize do
  KAE::Appointment.class_eval do
    has_many :scheduleables,
      through: :appointments_scheduleables,
      source_type: KAE.scheduleable_class.name
    has_many :schedulers,
      through: :appointments_schedulers,
      source_type: KAE.scheduler_class.name
  end
end

Ok now that works, I wait till the host app loads completely and now I have the values for KAE.scheduleable_class and KAE.scheduler_class, great.

Except I get the error in the title intermittently!

I can boot up fine, use the app for a while (10-30 mins) and then out of nowhere boom! I've tried it under rails server, thin, and unicorn; all the same so it must be an app/framework level bug. I look in the active record preloader class to where the top of the stacktrace points to:

# lib/active_record/associations/preloader.rb:150
def records_by_reflection(association)
  records.group_by do |record|
    reflection = record.class.reflections[association]

    unless reflection
      raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
                                              "perhaps you misspelled it?"
    end

    reflection
  end
end

How can a model forget some of it's associations?

So now I'm doing this directly in the Appointment model and it seems to be working so far but it's really ugly:

class Appointment < ActiveRecord::Base
  if KAE.scheduler_class && KAE.scheduleable_class
    has_many :scheduleables,
      through: :appointments_scheduleables,
      source_type: KAE.scheduleable_class.name
    has_many :schedulers,
      through: :appointments_schedulers,
      source_type: KAE.scheduler_class.name
  else
    binding.pry # TODO
    ActiveSupport.on_load :after_initialize do
      KAE::Appointment.class_eval do
        has_many :scheduleables,
          through: :appointments_scheduleables,
          source_type: KAE.scheduleable_class.name
        has_many :schedulers,
          through: :appointments_schedulers,
          source_type: KAE.scheduler_class.name
      end
    end
  end
end

Anybody know of a better way of declaring has_many :through polymorphic associations in a Rails 3 Engine?

I've been looking at open source projects like acts_as_taggable_on_steroids and paper_trail to see how they do their associations but they don't have polymorphic has_many :through.

Anybody know any projects that have this type of association?

DiegoSalazar
  • 13,361
  • 2
  • 38
  • 55

1 Answers1

0

Load ordering can be a pain with Rails autoloading+auto-reloading magic. In these situations I just make my dependencies more explicit. I suggest you try using Rails's require_dependency in your host to ensure this doesn't occur:

# in host app
require_dependency 'appointment'
class User < ActiveRecord::Base
  acts_as_scheduler
end

It's ActiveSupport's version of require that also plays well with the development environment's auto-reloading, which is probably why you're experiencing this issue intermittently when working on your application.

Chris Keele
  • 3,364
  • 3
  • 30
  • 52