【问题标题】:ActiveRecord seems to be validating unchanged child records unnecessarilyActiveRecord 似乎在不必要地验证未更改的子记录
【发布时间】:2015-10-27 13:07:47
【问题描述】:

我发现 ActiveRecord 似乎不必要地验证子记录的情况。提前道歉,因为这很复杂。

这涉及通过以前使用过但未以任何方式更改的关联。它发生在 3.2 到最近的 master 上。我不确定这是导致意外行为的设计决策,还是某种错误。

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

型号:

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

迁移:

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

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

b = B.create!
c = C.create!
b.cs << c
a = A.new
a.b = b
a.cs.first
puts "X"
a.valid?

给出输出:

B
C
C
X
A
C

这表明验证 A 会验证其 C。

现在研究了这个我知道has_many :validate =&gt; false 选项,并使用它,问题就消失了。但在我看来,这里发生的事情比这更多——请耐心等待。

AR docs say:

:validate 如果为 false,则在保存父对象时不验证关联对象。默认为真。

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

  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

这一切都取决于从关联缓存中获取的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

我能想到的唯一真正的解释是只验证加载的记录是因为 ActiveRecord 的目的是只验证更改的记录。我真的希望它验证当且仅当它要保存时,因此仅保存更改记录的默认自动保存选项应该会阻止验证。

我找到了 related ticket 和提交 27aa4dda7d89ce733(我认为还没有在任何版本中)进行了更改,但没有从我的测试中解决这个特定问题。但是它确实包含以下表达式:

!record.persisted? || record.changed? || record.marked_for_destruction?

如果我将此条件添加到validate_collection_association 的最内层循环,那么问题就会消失,ActiveRecord 测试仍在我的机器上通过。

这在我的项目中是一个重大的性能问题,因为有问题的模型只应该在管理员中进行验证,其中自定义验证中使用的未索引字段是可以接受的,因为它很少被保存,因此我判断索引它会过度索引(它不仅仅是一个字段)。显然,在大多数情况下,这种过度验证不会那么严重,而且它似乎只发生在非常特定的情况下,所以这可能是一个错误。

所以,虽然我对正在发生的事情有一个好主意,但我并不完全确定应该发生什么,这就是为什么我没有将此作为 ActiveRecord 票提交。你认为这是一个错误吗?为什么它会这样工作?验证选项的真正用途是什么?如果这是一个错误,您能解释一下为什么代码会这样工作,以及为什么会超出范围吗?什么情况下我的代码更改为 ActiveRecord 以上会中断?

【问题讨论】:

  • 我很想看看在关联上添加 inverse_of: 是否会改变这种行为。 TBH 不过,我不抱太大希望。
  • 在我的测试中没有。

标签: ruby-on-rails validation activerecord


【解决方案1】:

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

在分配a.b = b 之前,a 没有bs 或cs。

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

一旦您调用a.cs,a 将通过b 查找关联的cs 对象,因为b 可用。它会找到这些对象并将它们作为子对象添加到a。

我明白你的意思,从技术上讲,在这种特殊情况下,在 cs 的这个特定架构中没有什么可做的,但我明白为什么 ActiveRecord 正在检查。就其而言,这些对象是a 的子对象,并且子记录会得到验证,除非通过validate: false 明确告知不要这样做。

在这种情况下,a 是 b 的子代,因此不需要 a 对其进行验证。

一般来说,父母会促使他们关联的孩子被验证。孩子不必验证他们的父母。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-27
    • 2013-06-11
    • 1970-01-01
    • 2022-06-12
    • 1970-01-01
    • 2017-12-06
    • 1970-01-01
    相关资源
    最近更新 更多