Rails 7 allows constructors on has_one :through associations


When we declare a belongs_to association, the declaring class automatically gains build_association and create_association methods.

For example, given the declaration:

class Book < ApplicationRecord
  belongs_to :author
end

Each instance of the Book model will have build_author and create_author methods.

Now let’s consider the following models.

# app/models/supplier.rb
class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account
end
# app/models/account.rb
class Account < ApplicationRecord
  belongs_to :supplier
  has_one :account_history
end
# app/models/account_history.rb
class AccountHistory < ApplicationRecord
  belongs_to :account
end

To create a supplier account, we can call create_account on the Supplier object as shown below:

Supplier.first.create_account

# Supplier Load  (0.5ms) SELECT "suppliers".* FROM "suppliers" ORDER BY "suppliers"."id" ASC LIMIT $1  [["LIMIT", 1]]

# Account Load  (0.5ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."supplier_id" = $1 LIMIT $2
[["supplier_id", "6700d0ba-b8ed-4ff5-bb69-c57e7ea7f285"], ["LIMIT", 1]]

The direct association of has_one: :account works as expected. But when we try to create AccountHistory, we get the below error:

Supplier.first.create_account_history

# Supplier Load  (0.5ms) SELECT "suppliers".* FROM "suppliers" ORDER BY "suppliers"."id" ASC LIMIT $1  [["LIMIT", 1]]

NoMethodError: undefined method `create_account_history' for #<Supplier:0x00007f949d6016c8>

As we can see, the create_account_history method is missing when we created the has_one :through relationship.

Before:

Before Rails 7, to fix the above issue, we can add a method create_account_history to the Supplier model.

class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account

  def create_account_history
    account = self.account
    account.create_account_history
  end
end

This change works as expected, but we would end up writing similar methods for all our has_one :through associations.

After:

To resolve the above issue, Rails 7 allows build_association and create_association on has_one :through associations.

AccountHistory can be created via Supplier object as shown below.

Supplier.first.create_account_history

# AccountHistory Create (1.3ms)  INSERT INTO "account_histories" ("account_id", "user_id") VALUES ($1, $2) RETURNING "id" [["account_id", "5d671a08-1513-442a-8cfa-ac00e128d423"], ["user_id", "24571aff-3456-442a-8cfa-aerte128d423"], ["organization_user_id""6700d0ba-b8ed-4ff5-bb69-c57e7ea7f285"], ["created_at", "2021-04-09 06:26:59.002535"], ["updated_at", "2021-04-09 06:26:59.002535"]]

Similarly, build_account_history will work and, it will not throw a NoMethodError exception.