11

摘要/错误

我在应用程序的不同位置收到此错误:

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'

控制器

控制器看起来像这样,但问题可能发生在使用 Company 模型保存或更新另一个模型的任何时候:

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

  def update
  end
end

事实/观察

一些事实和观察:

  • 该问题是随机发生的,但通常是在开发服务器运行一段时间之后。
  • 生产中不会出现该问题。
  • 即使我没有对Company模型进行任何更改,也会出现问题。
  • 问题通过重新启动服务器解决。

理论

据我了解,这是由于类的动态加载。

不知何故,公司类在重新加载时获得了一个新的类标识符。我听说它是​​由于草率的要求。我在 Company 模型中没有自己的要求,但我确实使用了active-record-postgres-hstore

模型

这是Company模型:

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

分类模型:

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

  def to_s
    name
  end
end

实际问题

我真的很想知道为什么会出现这个问题,以及是否可以以某种方式避免它。

我知道这个例外原则上意味着什么。我想知道如何避免它。

特别是,我想知道我是否以某种方式引起了问题,或者它是否是 gem,在这种情况下,我是否可以以任何方式帮助修复 gem。

提前感谢您的任何回答。

4

2 回答 2

31

问题几乎可以肯定是因为您将这些类的副本序列化到缓存或会话中,然后再重新构建它们。这会导致问题,因为在开发模式下,类在每个请求上都未定义和重新定义,所以如果你有一个类的旧定义的编组副本,然后在 Rails 类卸载之前设法解组它,你将有两个具有相同名称的不同类。

异常从这里提出:https ://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/associations/association.rb#L204-212

您可以在这里看到它正在做一些非常简单的事情 - 它正在测试is_a?传递给关联的类的实例中传递的对象。取消定义和重新定义一个类意味着如果你有一个类的旧副本,并将它与新版本的类进行比较,它不会通过集合。考虑这个例子:

class Foo; end
f = Foo.new

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

puts f.is_a? Foo
# => false

这里发生的是,当我们取消定义和重新定义时Foo,它实际上创建了一个新对象(请记住,类是 Class 的实例!)。即使我们知道那f是 a Foo,但由于与 不同而f.is_a? Foo失败。检查给定对象的类是否与传递的类匹配,或者它是传递的类的子类 - 这里不是这种情况。它们具有相同的名称,但它们是不同的类。这是你的协会中发生的事情的核心。f.classFoois_a?

在某些时候,您的Classification关联需要某个版本Company,而您正在分配不同的版本。如果我不得不猜测,我会说您将整个用户记录存储在会话中。这将编组记录,包括关联的Company记录。该公司记录将在 Rails 重新加载其类之前由 Rack 解组,因此它可能最终成为与协会预期不同的类(具有相同名称)。流程类似于:

  • 定义Company. 我们称它为 Company-1
  • 加载用户及其关联的公司 (Company-1) 记录。
  • 将整个交易保存到会话中。
  • 刷新页面
  • 在 Rack 的设置过程中,它将在会话中找到一条公司记录(附加到用户记录)并将其解组。这会将其解组为 Company-1(因为这是 Company 当前 Object#constants 的实例)
  • 然后 Rails 将卸载所有模型常量并重新定义它们。在此过程中,它将重新定义公司(Company-2),并设置分类以期望在关联中出现 Company-2 记录。
  • 您尝试将 Company-1 对象分配给期望 Company-2 对象的关联。错误被抛出,因为正如我们之前看到的,Company-1 的一个实例失败了is_a? Company-2

解决方案是避免将整个编组的对象存储在会话或缓存中。相反,存储主键并对每个请求执行查找。这解决了这个特定问题,以及后期生产中可能不兼容的对象定义的问题(考虑在部署对对象结构进行重大更改的更改之前与编组对象存在会话的用户)。

一般来说,这可能是由任何可以在请求之间保留旧类引用的东西引起的。Marshal 是通常的嫌疑人,但某些类变量和全局变量也可以做到这一点。

如果 gem 可能会在某个地方存储类或全局变量中的类引用列表,那么它可能会这样做,但我的直觉是它在你的会话中。

于 2013-07-16T03:51:12.277 回答
2

我有一个ActiveJobasync模式下运行的开发环境,它将为给定模型排队一堆其他 ActiveJob。

所以基本上FirstJob会开始运行,并且对于它使用的每条记录,它都会启动一个SecondJob导致超过 25 个作业在同一进程中异步运行。这很快导致ActiveRecord::AssociationTypeMismatch甚至A copy of Klass has been removed from the module tree but is still active错误。

通过将 ActiveJob 队列适配器切换到:inline开发中,我消除了这个问题。

我创建了一个初始化程序:

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
于 2018-08-08T11:33:16.160 回答