ActiveRecord似乎在不必要地验证未更改的子记录

mat*_*uck 9 validation activerecord ruby-on-rails

我发现ActiveRecord似乎不必要地验证子记录的情况.事先道歉,因为这非常复杂.

这涉及通过以前使用但未以任何方式改变的关联.它发生在3.2到最近的主人.我不确定这是一个导致意外行为或某种错误的设计决定.

我从实际代码中减少了一个测试用例,如下所示:

楷模:

class A < ActiveRecord::Base
  belongs_to :b
  has_many :cs, :through => :b
  before_validation { puts "A" }
end

class B < ActiveRecord::Base
  has_many :as
  has_many :cs
  before_validation { puts "B" }
end

class C < ActiveRecord::Base
  belongs_to :b
  before_validation { puts "C" }
end
Run Code Online (Sandbox Code Playgroud)

移民:

class AddABC < ActiveRecord::Migration
  def change
    create_table :as do |t|
      t.references :b
    end

    create_table :bs do |t|
    end

    create_table :cs do |t|
      t.references :b
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

在空数据库上运行时,触发它的简化测试用例是:

b = B.create!
c = C.create!
b.cs << c
a = A.new
a.b = b
a.cs.first
puts "X"
a.valid?
Run Code Online (Sandbox Code Playgroud)

给出输出:

B
C
C
X
A
C
Run Code Online (Sandbox Code Playgroud)

这表明验证A验证其Cs.

现在我已经了解了这个has_many :validate => false选项,并且使用它,问题就消失了.但在我看来,这里发生的事情比这更多 - 忍受我.

AR文档说:

:validate如果为false,则在保存父对象时不要验证关联的对象.默认为true.

但我发现这令人困惑,因为这显然不能代表所有记录.如果我从未获得关联(a.cs.first从上面的代码中删除),或者我得到它但从不使用它(替换为a.cs),它将不会验证对象.这是因为它通过validate_collection_associationlib/active_record/autosave_association.rb其中包含的代码:

  def validate_collection_association(reflection)
    if association = association_instance_get(reflection.name)
      if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
        records.each_with_index { |record, index| association_valid?(reflection, record, index) }
      end
    end
  end
Run Code Online (Sandbox Code Playgroud)

这完全取决于association_instance_get从关联缓存中获取哪些内容.没有缓存意味着没有要验证的记录.

我试图做一个更简单的has_many,通过设置一个引用A的B模型,但是我需要在A之前创建B,然后如果我试图保存它,A将不再是新记录,并且此代码可以防止问题,因为被调用的分支将不再是第一个:

  def associated_records_to_validate_or_save(association, new_record, autosave)
    if new_record
      association && association.target
    elsif autosave
      association.target.find_all(&:changed_for_autosave?)
    else
      association.target.find_all(&:new_record?)
    end
  end
Run Code Online (Sandbox Code Playgroud)

我只能验证加载记录的唯一真正解释是因为ActiveRecord的意图是仅验证更改的记录.我真的希望它能够验证是否以及是否会保存,因此仅保存更改记录的默认自动保存选项应该阻止验证.

我找到了一个相关的票据和提交27aa4dda7d89ce733(我认为还没有在任何版本中),它进行了更改,但没有修复我的测试中的这个特定问题.但它确实包含以下表达式:

!record.persisted? || record.changed? || record.marked_for_destruction?
Run Code Online (Sandbox Code Playgroud)

如果我将这个条件添加到最里面的循环validate_collection_association然后问题消失了,ActiveRecord测试仍然在我的机器上传递.

这在我的项目中是一个重要的性能问题,因为所讨论的模型只能在admin中验证,其中自定义验证中使用的无索引字段是可接受的,因为它的保存很少,因此我判断它是索引它会过度索引(它不仅仅是一个字段).显然在大多数情况下,这种过度验证会严重得多,而且似乎只是在一个特定的情况下发生,所以这可能是一个错误.

所以,虽然我对发生的事情有了一个好主意,但我并不完全确定应该发生什么,这就是为什么我没有将其作为ActiveRecord票据提交.你觉得这是个bug吗?它为什么这样工作?什么是验证选项呢?如果这是一个错误,你能解释为什么代码以这种方式工作,以及它为什么过度扩展?我的代码会在什么情况下更改为ActiveRecord以上?

Bas*_*man 3

发生这种情况的原因是因为 A 和 C 之间的关系是通过 B 建立的。

在分配之前a.b = ba没有bscs

如果您分配a.b = b但不调用a.cs,则a没有理由尝试加载关联的cs. has_many仅创建cs便捷方法,它不会为您调用它。这里只a.b_id设置为b.id

一旦您调用a.cs,将通过“since is available”a查找关联cs对象。它将找到这些对象并将它们作为子对象添加到.bba

我明白你的观点,从技术上讲,在这个特定的情况下,在这个特定的模式中没有什么可做的cs,但我可以明白为什么ActiveRecord要检查。就其而言,这些对象是 的子对象,a并且子记录是经过验证的,除非明确告知不要通过validate: false

在本例中,a是 的子级b,因此a不需要验证它。

一般来说,父母会让他们的相关孩子得到验证。孩子不必认可父母。