36

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.

Rob Sobers
  • 20,737
  • 24
  • 82
  • 111
  • something like this? `scope:account lambda {|account| {:conditions => {:account_id => account}}}` and also have a look into this SO http://stackoverflow.com/questions/16203576/rails-associations-how-do-i-limit-scope-a-has-many-through-with-multiple-self?rq=1 – Pavan Apr 17 '14 at 04:36
  • @Pavan the key difference in my case is that the value that needs to be passed isn't something from another model, it's something only the controller knows (i.e., the current account). – Rob Sobers Apr 17 '14 at 18:22
  • 1
    Probably that might help - https://blog.widefix.com/parameterized-rails-associations/ – ka8725 Apr 03 '23 at 16:31

3 Answers3

42

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)
Малъ Скрылевъ
  • 16,187
  • 5
  • 56
  • 69
  • Is it possible to access the customer record in the `by_account` method if you wanted it to be part of your condition? – littleforest Jan 19 '16 at 19:37
31

You pass in an instance of the class you have defined. In your case, you would pass in a customer and then get the account.

From the API http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Accessing the owner object

Sometimes it is useful to have access to the owner object when building the query. The owner is passed as a parameter to the block. For example, the following association would find all events that occur on the user's birthday:

class User < ActiveRecord::Base
  has_many :birthday_events, ->(user) { where starts_on: user.birthday }, 
    class_name: 'Event'
end

In your example it would be:

class Customer < ActiveRecord::Base
  has_many :orders, ->(customer) { where(account_id: customer.account.id) }
end
Georg Ledermann
  • 2,712
  • 4
  • 31
  • 35
Brad
  • 556
  • 4
  • 10
  • 3
    This answer presupposes a 1:1 relationship between Customer and Account. However the question specified a scenario where the opposite is true i.e. a multi-tenant system where a given customer can place orders on behalf of multiple accounts. – inopinatus Mar 26 '16 at 01:43
0

I know this is old, but since no answer was accepted yet, I thought adding my views on the point would harm no one.

The problem is that whenever you pass a scope to a has_many relationship, passing the instance of the owner class as an argument is not only a possibility but it is the only possibility to pass an argument. I mean, you are not allowed to pass more arguments, and this one will always be the instance of the owner class.

So @RobSobers, when you

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

it is not arbitrary, you get all orders with th id of the customer you called the relation on. I guess your code was something like

Customer.first.orders(@some_account_which_is_ignored_anyway)

Seems like has_many relation was not meant to accept arguments.

Personally, I prefer the solution of @МалъСкрылевъ.

Misu
  • 441
  • 5
  • 15