How do I pass an argument to a has_many association scope in Rails 4?

Rob Sobers picture Rob Sobers · Apr 17, 2014 · Viewed 18.6k times · Source

Rails 4 lets you scope a has_many relationship like so:

class Customer < ActiveRecord::Base
  has_many :orders, -> { where processed: true }
end

So anytime you do customer.orders you only get processed orders.

But what if I need to make the where condition dynamic? How can I pass an argument to the scope lambda?

For instance, I only want orders to show up for the account the customer is currently logged into in a multi-tenant environment.

Here's what I've got:

class Customer < ActiveRecord::Base
  has_many :orders, (account) { where(:account_id => account.id) }
end

But how, in my controller or view, do I pass the right account? With the code above in place when I do:

customers.orders

I get all orders for account with an id of 1, seemingly arbitrarily.

Answer

Малъ Скрылевъ picture Малъ Скрылевъ · Nov 26, 2015

The way is to define additional extending selector to has_many scope:

class Customer < ActiveRecord::Base
   has_many :orders do
      def by_account(account)
         # use `self` here to access to current `Customer` record
         where(:account_id => account.id)
      end
   end
end

customers.orders.by_account(account)

The approach is described in Association Extension head in Rails Association page.

To access the Customer record in the nested method you just can access self object, it should have the value of current Customer record.

Sinse of rails (about 5.1) you are able to merge models scope with the othe model has_many scope of the same type, for example, you are able to write the same code as follows in the two models:

class Customer < ApplicationRecord
   has_many :orders
end

class Order < ApplicationRecord
   scope :by_account, ->(account) { where(account_id: account.id) }
end

customers.orders.by_account(account)