Decoupled ActiveRecord scopes by merging

Named scopes are a great tool for abstracting database structure:

  • SQL fragments remain in the model and are not spread all over the code base
  • Prevent repetitions (equal SQL fragments) at several areas
  • Improves readability (illegible SQL is hidden behind comprehensible names)
  • Simplifies testing

Especially the last point is underestimated sometimes. A named scope can be tested explicitly in separation. Other tests, whose implementation relies on that named scope, then can stub out the scope with no remorse. This approach results in faster tests, because at least 2 database operations (write and read the object) can be avoided.
And easy to test implementations are the evidence for solid code.
However bad implemented named scopes also can lead to tight coupling between models.

The initial situation

The example is about bank accounts:

rails g model BankAccount serial:string && rake db:migrate

which can have many transactions:

class BankAccount < ActiveRecord::Base
  has_many :transactions
end

That means each transaction belongs to a bank account and can be finalized at certain point of time:

rails g model Transaction bank_account:belongs_to finalized_at:datetime && rake db:migrate

The model:

class Transaction < ActiveRecord::Base
  belongs_to :bank_account

  validates :bank_account, presence: true
end

Tight coupling through named scopes

All bank accounts should be found, currently having at least one finalized transaction. The requirement is implemented with the named scope with_transaction_today:

class BankAccount < ActiveRecord::Base
  has_many :transactions

  scope :with_transaction_today, -> {
    joins(:transactions)
      .where('transactions.finalized_at >= :day_begin',
        { day_begin: Time.current.at_beginning_of_day })
  }
end

Though the named scope works:

BankAccount.with_transaction_today.count # => 5

it reveals a serious problem: the condition refers to a Transaction model attribute. That is tight coupling between both classes.
Instead the BankAccount should know nothing about the internal structure of Transaction.

Decoupled named scope

An alternative to the coupling named scope is moving the necessary condition into the Transaction model own named scope:

class Transaction < ActiveRecord::Base
  belongs_to :bank_account

  validates :bank_account, presence: true

  scope :today, -> {
    where('finalized_at >= :day_begin', { day_begin: Time.current.at_beginning_of_day })
  }
end

Changes made to the internal structure need not to be maintained anywhere else than in the Transaction.
This scope can be reused in the BankAccount with ActiveRecord::Base#merge:

class BankAccount < ActiveRecord::Base
  has_many :transactions

  scope :with_transaction_today, -> {
    joins(:transactions).merge(Transaction.today)
  }
end

The refactored named scope works the same, but without any tight coupling:

BankAccount.with_transaction_today.count # => 5