了解Rails 7更新through_reflection——以便在数据库连接中分配事务

102 阅读3分钟

虽然ActiveRecord在大多数情况下工作得很好,但它在多数据库架构中却很吃力。 幸运的是,过去的几个版本在这方面做了大量的工作。

一个主要的缺点是事务在多数据库架构中的工作方式。 一个事务基本上是一组数据库操作,作为一个单元来执行。 这里有一个例子。

irb(main):001:1* ActiveRecord::Base.transaction do
irb(main):002:1*   Company.first.touch
irb(main):003:1*   Company.last.touch
irb(main):004:0> end
  TRANSACTION (0.1ms)  begin transaction
  Company Load (0.2ms)  SELECT "companies".* FROM "companies" ORDER BY "companies"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Company Update (1.2ms)  UPDATE "companies" SET "updated_at" = ? WHERE "companies"."id" = ?  [["updated_at", "2022-08-21 10:48:22.853590"], ["id", 1]]
  Company Load (0.1ms)  SELECT "companies".* FROM "companies" ORDER BY "companies"."id" DESC LIMIT ?  [["LIMIT", 1]]
  Company Update (0.1ms)  UPDATE "companies" SET "updated_at" = ? WHERE "companies"."id" = ?  [["updated_at", "2022-08-21 10:48:22.860248"], ["id", 2]]
  TRANSACTION (1.1ms)  commit transaction
...

虽然有两个数据库操作(Company.first.touchCompany.last.touch ),但它们是在一个事务中执行的。

然而,当你在多个数据库中使用一个事务时,基本上是由开发人员来确保事务在数据库中的分布方式与预期的使用情况相一致。 这是因为事务是在单个数据库连接上工作的。

之前

让我们以一个多数据库架构为例,在主数据库中有一个公司模型,在辅助数据库中有一个自由职业者模型。 这两个模型都是通过has_many :through 关联,通过主数据库上的Contract 模型连接。

irb(main):001:0> company = Company.create! name: 'Saeloun'
...
irb(main):002:0> freelancer1 = Freelancer.create! name: 'Joe'
...
irb(main):003:0> freelancer2 = Freelancer.create! name: 'Schmoe'
...
irb(main):004:0> company.freelancers = [freelancer1, freelancer2]
  TRANSACTION (0.1ms)  begin transaction
  Contract Create (0.8ms)  INSERT INTO "contract" ("company_id", "freelancer_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 1], ["freelancer_id", 1], ["created_at", "2022-08-21 07:23:41.521082"], ["updated_at", "2022-08-21 07:23:41.521082"]]
  TRANSACTION (1.2ms)  commit transaction
  TRANSACTION (0.0ms)  begin transaction
  Contract Create (0.4ms)  INSERT INTO "contract" ("company_id", "freelancer_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 2], ["freelancer_id", 1], ["created_at", "2022-08-21 07:23:41.525693"], ["updated_at", "2022-08-21 07:23:41.525693"]]
  TRANSACTION (0.6ms)  commit transaction
...

正如你所看到的,发生了两个事务来向contract 表插入两条记录。 这是因为company.freelancers = [freelancer1, freelancer2] 操作是在主数据库连接上执行的,Contract.create! 操作是在辅助数据库连接上执行的。

这就需要开发人员用一个双事务块来保证这两个操作是在一个事务中执行的。

irb(main):001:1* Company.transaction do
irb(main):002:2*   Freelancer.transaction do
irb(main):003:2*     company.freelancers = [freelancer1, freelancer2]
irb(main):004:1*   end
irb(main):005:0> end
  TRANSACTION (0.1ms)  begin transaction
  Contract Create (0.7ms)  INSERT INTO "contract" ("company_id", "freelancer_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 1], ["freelancer_id", 1], ["created_at", "2022-08-21 07:32:41.351072"], ["updated_at", "2022-08-21 07:32:41.351072"]]
  Contract Create (0.1ms)  INSERT INTO "contract" ("company_id", "freelancer_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 2], ["freelancer_id", 1], ["created_at", "2022-08-21 07:32:41.351083"], ["updated_at", "2022-08-21 07:32:41.351083"]]
  TRANSACTION (1.3ms)  commit transaction
...

之后

感谢这个PR,它更新了through_reflection 在数据库连接间分配事务的方式,我们现在可以执行同样的操作,而不需要双事务块。

irb(main):001:0> company.freelancers = [freelancer1, freelancer2]
  TRANSACTION (0.1ms)  begin transaction
  Contract Create (0.7ms)  INSERT INTO "contract" ("company_id", "freelancer_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 1], ["freelancer_id", 1], ["created_at", "2022-08-21 07:32:41.351072"], ["updated_at", "2022-08-21 07:32:41.351072"]]
  Contract Create (0.1ms)  INSERT INTO "contract" ("company_id", "freelancer_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", 2], ["freelancer_id", 1], ["created_at", "2022-08-21 07:32:41.351083"], ["updated_at", "2022-08-21 07:32:41.351083"]]
  TRANSACTION (1.3ms)  commit transaction
...

这是通过对ThroughAssociation模块的一个非常简单的更新实现的。 transaction 方法用through_reflectionconnection 来打开第二个块,而不是source_reflection

module ActiveRecord
  module Associations
    module ThroughAssociation
      ...
      private
        def transaction(&block)
          through_reflection.klass.transaction(&block)
        end
      ...
    end
  end
end