Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AssociationTypeMismatch for the same model

Summary/the error

I'm getting this error at different places in my application:

ActiveRecord::AssociationTypeMismatch in Settings::CompaniesController#show

Company(#70257861502120) expected, got Company(#70257861787700)

activerecord (3.2.11) lib/active_record/associations/association.rb:204:in `raise_on_type_mismatch'
activerecord (3.2.11) lib/active_record/associations/belongs_to_association.rb:6:in `replace'
activerecord (3.2.11) lib/active_record/associations/singular_association.rb:17:in `writer'
activerecord (3.2.11) lib/active_record/associations/builder/association.rb:51:in `block in define_writers'
activerecord (3.2.11) lib/active_record/attribute_assignment.rb:85:in `block in assign_attributes'
activerecord (3.2.11) lib/active_record/attribute_assignment.rb:78:in `each'
activerecord (3.2.11) lib/active_record/attribute_assignment.rb:78:in `assign_attributes'
activerecord (3.2.11) lib/active_record/base.rb:497:in `initialize'
app/controllers/settings/companies_controller.rb:4:in `new'
app/controllers/settings/companies_controller.rb:4:in `show'

Controller

The controller looks like this, but the problem can occur at any point where a Company model is used to save or update another model:

class Settings::CompaniesController < SettingsController
  def show
    @company = current_user.company
    @classification = Classification.new(company: @company)
  end

  def update
  end
end

Facts/observations

Some facts and observations:

  • The problem occurs randomly, but usually after the development server has been running for a while.
  • The problem does not occur in production.
  • The problem occurs even when I have made no changes at all to the Company model.
  • The problem is solved by restarting the server.

Theories

As far as I understand this is due to dynamic loading of classes.

Somehow the Company class is getting a new class identifier upon reloading. I've heard rumors about it being due to sloppy requires. I'm doing no requires of my own in the Company model, but I do use the active-record-postgres-hstore.

The models

This is the Company model:

class Company < ActiveRecord::Base
  serialize :preferences, ActiveRecord::Coders::Hstore
  DEFAULT_PREFERENCES = {
    require_review: false
  }
  has_many :users
  has_many :challenges
  has_many :ideas
  has_many :criteria
  has_many :classifications
  attr_accessible :contact_email, :contact_name, :contact_phone, :email, :logotype_id, :name, :phone, :classifications_attributes, :criteria_attributes, :preferences

  accepts_nested_attributes_for :criteria
  accepts_nested_attributes_for :classifications

  after_create :setup
  before_save :set_slug

  # Enables us to fetch the data from the preferences hash directly on the instance
  # Example:
  # company = Company.first
  # company.preferences[:foo] = "bar"
  # company.foo
  # > "bar"
  def method_missing(id, *args, &block)
    indifferent_prefs = HashWithIndifferentAccess.new(preferences)
    indifferent_defaults = HashWithIndifferentAccess.new(DEFAULT_PREFERENCES)
    if indifferent_prefs.has_key? id.to_s
      indifferent_prefs.fetch(id.to_s)
    elsif indifferent_defaults.has_key? id.to_s
      indifferent_defaults.fetch(id.to_s)
    else
      super
    end
  end

  private
  def setup
    DefaultClassification.find_each do |c|
      Classification.create_from_default(c, self)
    end

    DefaultCriterion.find_each do |c|
      Criterion.create_from_default(c, self)
    end
  end

  def set_slug
    self.slug = self.name.parameterize
  end
end

The Classification model:

class Classification < ActiveRecord::Base
  attr_accessible :description, :name, :company, :company_id
  has_many :ideas
  belongs_to :company

  def to_s
    name
  end
end

The actual question

I'd be really interested in knowing why this problem occurs and if it can be avoided somehow.

I know what the exception means in principle. I want to know how to avoid it.

In particular, I'd like to know if I caused the problem somehow or if it is the gem, and in that case if I could help fix the gem in any way.

Thank you in advance for any answers.

like image 339
Jesper Avatar asked Apr 09 '13 18:04

Jesper


2 Answers

The problem is almost assuredly because you are serializing copies of these classes into either a cache or the session, then later reconstituting them. This causes problems because classes get undefined and redefined on each request in development mode, so if you have a marshalled copy of an old definition of a class, and then manage to unmarshal it before the Rails class unloading, you're going to have two different classes with the same name.

The exception is being raised from here: https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/associations/association.rb#L204-212

You can see here that it's doing something very simple - it's testing that the object passed in is_a? instance of the class passed to the association. Undefining and redefining a class means that if you have an old copy of a class, and compare it to the new version of the class, it's not going to pass muster. Consider this example:

class Foo; end
f = Foo.new

Object.send :remove_const, :Foo
class Foo; end

puts f.is_a? Foo
# => false

What's happening here is that when we undefine and redefine Foo, it actually creates a new object (remember, classes are instances of Class!). Even though we know that f is a Foo, f.is_a? Foo fails because f.class is different from Foo. is_a? checks that the given object's class either matches the passed class, or that it is a subclass of the passed class - neither is the case here. They share the same name, but they are different classes. This is the core of what's happening in your associations.

At some point, your Classification association expects a certain version of Company, and you are assigning a different version. If I had to guess, I'd say that you are storing the entire user record in the session. This is going to marshal the record, including the associated Company record. This Company record will be unmarshaled by Rack before Rails does its class reloading, so it may end up being a different class (with the same name) than what the association expects. The flow is something like:

  • Define Company. We'll call this Company-1
  • Load a user and its associated Company (Company-1) record.
  • Save this whole dealio to the session.
  • Refresh the page
  • During Rack's setup, it will find a Company record in the session (attached to the User record) and unmarshal it. This will unmarshal it as Company-1 (as this is the instance of Company currently Object#constants)
  • Rails will then unload all your model constants and redefine them. In this process, it will redefine Company (Company-2), and set up Classification to expect a Company-2 record in the association.
  • You attempt to assign your Company-1 object to an association expecting a Company-2 object. The error is thrown, since as we saw earlier, an instance of Company-1 fails is_a? Company-2.

The solution is to avoid storing whole marshalled objects in the session or cache. Instead, store primary keys and perform lookups on each request. This solves this particular problem, as well as the problem of potentially incompatible object definitions later in production (consider a user who has a session existing with a marshalled object before you deploy a change that makes a significant change to that object's structure).

In general, this can be caused by anything that can persist old class references between requests. Marshal is the usual suspect, but certain class variables and globals can also do it.

The gem may possibly do it if it is somewhere storing a list of class references in a class or global variable, but my hunch is that it's something in your session.

like image 178
Chris Heald Avatar answered Sep 27 '22 18:09

Chris Heald


I had an ActiveJob in development environment running in async mode that would queue up a bunch of other ActiveJob's for a given model.

So basically FirstJob would start running and for each record it worked with it would start a SecondJob resulting in upwards of 25 jobs running asynchronously in same process. This quickly resultet in ActiveRecord::AssociationTypeMismatch and even A copy of Klass has been removed from the module tree but is still active errors.

By switching ActiveJob queue adapter to :inline in development I eliminated the issue.

I created an initializer with:

if Rails.env.test?
  ActiveJob::Base.queue_adapter = :test
elsif Rails.env.development?
  ActiveJob::Base.queue_adapter = :inline
else
  ActiveJob::Base.queue_adapter = :sidekiq # or your preferred choice
end
like image 43
mtrolle Avatar answered Sep 27 '22 17:09

mtrolle