Thursday, April 25, 2024
HomeRuby On RailsRails 7 updates through_reflection to distribute transactions throughout database connections

Rails 7 updates through_reflection to distribute transactions throughout database connections


Whereas ActiveRecord works fairly nice throughout most eventualities, it struggles with multi-database architectures.
Fortuitously, the previous few releases have had a number of work completed on this side.

One main disadvantage is the way in which transactions work in a multi-database structure.
A transaction is principally a set of database operations which can be carried out as a single unit.
Right here’s an instance,

irb(primary):001:1* ActiveRecord::Base.transaction do
irb(primary):002:1*   Firm.first.contact
irb(primary):003:1*   Firm.final.contact
irb(primary):004:0> finish
  TRANSACTION (0.1ms)  start transaction
  Firm Load (0.2ms)  SELECT "firms".* FROM "firms" ORDER BY "firms"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Firm Replace (1.2ms)  UPDATE "firms" SET "updated_at" = ? WHERE "firms"."id" = ?  [["updated_at", "2022-08-21 10:48:22.853590"], ["id", 1]]
  Firm Load (0.1ms)  SELECT "firms".* FROM "firms" ORDER BY "firms"."id" DESC LIMIT ?  [["LIMIT", 1]]
  Firm Replace (0.1ms)  UPDATE "firms" SET "updated_at" = ? WHERE "firms"."id" = ?  [["updated_at", "2022-08-21 10:48:22.860248"], ["id", 2]]
  TRANSACTION (1.1ms)  commit transaction
...

Although there are two database operations (Firm.first.contact and Firm.final.contact),
they’re carried out inside a single transaction.

Nonetheless, while you use a transaction throughout a number of databases,
it’s primarily left up-to the developer to make sure that the transactions are distributed throughout the databases in a approach that’s in keeping with the meant use case.
It’s because transactions work on a single database connection.

Earlier than

Let’s take the instance of a multi-database structure the place there’s a Firm mannequin on the first database
and a Freelancer mannequin on the secondary database.
Each these fashions are linked through the has_many :via affiliation
through the Contract mannequin on the first database.

irb(primary):001:0> firm = Firm.create! identify: 'Saeloun'
...
irb(primary):002:0> freelancer1 = Freelancer.create! identify: 'Joe'
...
irb(primary):003:0> freelancer2 = Freelancer.create! identify: 'Schmoe'
...
irb(primary):004:0> firm.freelancers = [freelancer1, freelancer2]
  TRANSACTION (0.1ms)  start 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)  start 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
...

As you’ll be able to see, two transactions happen to insert two data into the contract desk.
It’s because the firm.freelancers = [freelancer1, freelancer2] operation is carried out on the first database connection
and the Contract.create! operation is carried out on the secondary database connection.

It’s as much as the developer to wrap this round a double transaction block to make sure that the 2 operations are carried out
inside a single transaction.

irb(primary):001:1* Firm.transaction do
irb(primary):002:2*   Freelancer.transaction do
irb(primary):003:2*     firm.freelancers = [freelancer1, freelancer2]
irb(primary):004:1*   finish
irb(primary):005:0> finish
  TRANSACTION (0.1ms)  start 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
...

After

Due to this PR that updates
how through_reflection distributes transactions throughout database connections,
we are able to now carry out the identical operation with out the necessity for a double transaction block.

irb(primary):001:0> firm.freelancers = [freelancer1, freelancer2]
  TRANSACTION (0.1ms)  start 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
...

This was achieved through a quite simple replace to the ThroughAssociation module.
The transaction methodology opens up the second block with the connection of the through_reflection
as a substitute of the source_reflection.

module ActiveRecord
  module Associations
    module ThroughAssociation
      ...
      non-public
        def transaction(&block)
          through_reflection.klass.transaction(&block)
        finish
      ...
    finish
  finish
finish
RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments