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'
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
Some facts and observations:
Company
model.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.
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
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.
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:
Company
. We'll call this Company-1is_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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With