AssociationTypeMismatch for the same model

Summary / Error

I get this error in different places of 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 a problem can occur at any time when 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 arise in production.
  • The problem arises even when I did not make changes to the Company model at all.
  • The problem is solved by restarting the server.

Theory

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

Somehow the company class gets a new class identifier when it reloads. I heard rumors about it due to sloppiness. I do not require myself in the company model, but I use active-record-postgres-hstore .

Models

This is a 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 

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 

Factual question

I would be interested to know why this problem occurs, and if it can be somehow avoided.

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

In particular, I would like to know if I caused the problem somehow, or if it is a gem, and in this case, if I could somehow fix the stone.

Thank you in advance for any answers.

+6
source share
1 answer

The problem is almost certainly related to the fact that you serialize copies of these classes into a cache or session, and then restore them. This causes problems because the classes get undefined and are redefined each time they are requested in design mode, so if you have a marshalled copy of the old class definition and then delete it before unloading the Rails class, you are going to have two different classes with the same name.

An exception occurs here: https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/associations/association.rb#L204-212

Here you can see that it does something very simple - does it verify that the object is passed to the is_a? instance is_a? class passed to association. Failure to define and override a class means that if you have an old copy of the class and compare it with the new version of the class, it will not pass. Consider this example:

 class Foo; end f = Foo.new Object.send :remove_const, :Foo class Foo; end puts f.is_a? Foo # => false 

What happens is that when we refuse to define and redefine Foo , it actually creates a new object (remember that classes are instances of the class!). Although we know that f is Foo , f.is_a? Foo f.is_a? Foo fails because f.class is different from Foo . is_a? checks that the given class of the object either matches the passed class, or that it is a subclass of the passed class - here and not here. They have the same name, but they are different classes. This is the core of what happens in your associations.

At some point, your Classification association expects a specific version of Company , and you assign a different version. If I were to guess, I would say that you save the entire user record in the session. This will marshal the record, including the associated Company record. This company report will be untied by Rack before Rails reloads its class, so it may become a different class (with the same name) than the association expects. The stream looks something like this:

  • Define Company . We will call this company-1
  • Download the user and the associated Company (1) entry.
  • Save it all in a session.
  • Refresh the page
  • During the installation of the rack, he will find the company record in the session (attached to the user record) and cancel it. This will cancel it as Company-1 (since this is an instance of the constants of the object # currently)
  • Rails will then unload all your model constants and override them. In this process, he will redefine the company (Company-2) and adjust the classification to expect company-2 to be registered in the association.
  • You are trying to assign your Company-1 object to an association awaiting a Company-2 object. The error is triggered because, as we saw earlier, the Company-1 instance fails is_a? Company-2 is_a? Company-2 .

The solution is to avoid storing entire marshalled objects in a session or cache. Instead, save the primary keys and search for each query. This solves this specific problem, as well as the problem of identifying potentially incompatible objects later in the production process (consider a user who has a session existing with a marshalled object before deploying a change that makes significant changes to this object structure).

In general, this can be caused by the fact that it can keep old class references between requests. Marshall is the usual suspect, but some class variables and globals can also do this.

This stone can do this if it somewhere stores a list of class references in a class or global variable, but I suspect this is something in your session.

+18
source

All Articles