如何避免在创建关联模型时不必要地加载它?

How can I avoid unnecessary loading of Associated model while creating it?

提问人:jwilm 提问时间:9/12/2023 更新时间:9/12/2023 访问量:42

问:

在我的 Rails 应用程序中,我们有几个类似的模型

class Pen < ActiveRecord::Base
  has_one :ink
  after_create :add_ink

  def add_ink
    create_ink(color: "blue")
  end
end

class Ink < ActiveRecord::Base
  belongs_to :pen
end

在控制器中,我们做这样的事情

pen = Pen.new(..)
pen.save

从概念上讲,笔总是有一个墨迹,我们在创建笔时创建墨迹。(也对比after_create钩更好的方法来实现这一目标持开放态度)。

问题在于,它总是为关联发出 SELECT,即使保证那里什么都没有。

[DEBUG]   TRANSACTION (0.2ms)  BEGIN
[DEBUG]   Pen Create (1.5ms)  INSERT INTO pens ("owner_id") VALUES ('00f74077-c079-4482-aa03-15b23951a7bd') RETURNING "id"
[DEBUG]   Ink Load (0.4ms)  SELECT "inks".* FROM "inks" WHERE "inks"."pen_id" = '93a45bae-2cf3-48c1-ac00-6b3059dca5ae' LIMIT 1
[DEBUG]   Ink Create (0.3ms)  INSERT INTO "inks" ("pen_id", "color") VALUES ('93a45bae-2cf3-48c1-ac00-6b3059dca5ae', 'blue') RETURNING "pen_id"
[DEBUG]   TRANSACTION (0.2ms)  COMMIT

具体来说,这不应该发生

[DEBUG]   Ink Load (0.4ms)  SELECT "inks".* FROM "inks" WHERE "inks"."pen_id" = '93a45bae-2cf3-48c1-ac00-6b3059dca5ae' LIMIT 1

我尝试使用后跟 、 和 以及将 an 与 an 一起使用,它们都在某些时候会导致不必要的 Load。build_associationsavecreate_associationassociation=Association.create

Ruby-on-Rails Ruby ActiveRecord 关联

评论


答:

1赞 engineersmnky 9/12/2023 #1

您所看到的是由以下事实引起的:当 的创建发生在 .PenInkafter_create

文档

  • 将对象分配给关联会自动保存该对象和要替换的对象(如果有),以便更新其外键 - 除非父对象未保存 ()。has_onenew_record? == true
  • 如果其中任一保存失败(由于其中一个对象无效),则会引发异常并取消分配。ActiveRecord::RecordNotSaved

因此,您看到的是 rails 试图确定关联的记录是否已经存在,以便可以相应地更新它。

根据您想要的结果,您可以尝试;但是,我认为将其移动到操作中可能是一个更好的实现,例如Ink.create(pen_id: self.id, color: 'blue')before_create

class Pen < ActiveRecord::Base
  has_one :ink
  before_create :add_ink

  def add_ink
    build_ink(color: "blue")
  end
end

根据文档

:自动保存

如果为 true,则在保存父对象时,请始终保存关联的对象或销毁它(如果标记为销毁)。如果为 false,则切勿保存或销毁关联的对象。默认情况下,仅当关联的对象是新记录时,才保存该对象。

因此,由于这是一条新记录,因此它也应该保存,而无需查询数据库。before_createPenInk

评论

0赞 jwilm 9/12/2023
啊哈,很有见地!我最终不得不在钩子和钩子中做“build_ink”。没有它,我在尝试读取数据库提供的 ID 时会收到一个循环调用。谢谢!before_createink.pen_id = id; ink.saveafter_createPen.save
1赞 jwilm 9/12/2023
没关系;保存是隐式进行的,并且 ID 会使用您的示例自动更新。
0赞 engineersmnky 9/12/2023
@jwilm正确。这就是为什么我们需要确保在执行这些操作时没有保留(新记录),以便当实际保存时,它会保存关联的 和 但不执行 SELECT 查询。PenPenPenInk
0赞 mechnicov 9/12/2023 #2

如果你需要一些空笔(没有墨水)怎么办?

如果您需要带有绿色墨水的笔怎么办?

我建议这样

class CreatePen
  def self.call(params, color: :blue)
    new(params:, color:).call
  end

  def initialize(params:, color:)
    @pen = Pen.new(params)
    @color = color
  end

  def call
    persist_pen
  rescue ActiveRecord::RecordInvalid
    pen
  end

  private

  attr_reader :pen, :color

  def persist_pen
    pen.transaction do
      pen.save!

      Ink.create!(color:, pen_id: pen.id) if color.present?

      pen
    end
  end
end

在这种情况下,您可以这样调用它:

CreatePen.(params) # return pen with blue ink if everything ok
CreatePen.(params, color: :green) # return pen with green ink if everything ok
CreatePen.(params, color: nil) # return empty pen without ink if everything ok