How do you scope ActiveRecord associations in Rails 3?
Asked Answered
A

6

59

I have a Rails 3 project. With Rails 3 came Arel and the ability to reuse one scope to build another. I am wondering if there is a way to use scopes when defining a relationship (e.g. a "has_many").

I have records which have permission columns. I would like to build a default_scope that takes my permission columns into consideration so that records (even those accessed through a relationship) are filtered.

Presently, in Rails 3, default_scope (including patches I've found) don't provide a workable means of passing a proc (which I need for late variable binding). Is it possible to define a has_many into which a named scope can be passed?

The idea of reusing a named scope would look like:

Orders.scope :my_orders, lambda{where(:user_id => User.current_user.id)}
has_many :orders, :scope => Orders.my_orders

Or implicitly coding that named scope in the relationship would look like:

has_many :orders, :scope => lambda{where(:user_id => User.current_user.id)}

I'm simply trying to apply default_scope with late binding. I would prefer to use an Arel approach (if there is one), but would use any workable option.

Since I am referring to the current user, I cannot rely on conditions that aren't evaluated at the last possible moment, such as:

has_many :orders, :conditions => ["user_id = ?", User.current_user.id]
Apospory answered 9/3, 2010 at 20:33 Comment(1)
None of the answers submitted actually address your question about associations. I have a similar/same question here: #5784353Earleneearley
F
21

I suggest you take a look at "Named scopes are dead"

The author explains there how powerful Arel is :)

I hope it'll help.

EDIT #1 March 2014

As some comments state, the difference is now a matter of personal taste.

However, I still personally recommend to avoid exposing Arel's scope to an upper layer (being a controller or anything else that access the models directly), and doing so would require:

  1. Create a scope, and expose it thru a method in your model. That method would be the one you expose to the controller;
  2. If you never expose your models to your controllers (so you have some kind of service layer on top of them), then you're fine. The anti-corruption layer is your service and it can access your model's scope without worrying too much about how scopes are implemented.
Fawn answered 30/3, 2010 at 16:53 Comment(3)
I had seen that post. I had hoped the model relationships could use a "dynamic" default scope to make the use of them more legible, but I am resigned to write methods that return the ActiveRecord relationship criteria I need.Apospory
Make sure you read till the end, as update #2 to that article says that the bug was fixed and Mainly, whether you use scopes or class methods has, again, become a matter of personal tasteHypogynous
I think this link is not relevant anymore since it shows an outdated information and suggests not using scopes because of a bug in rails (which has been fixed already)Michaelson
L
20

How about association extensions?

class Item < ActiveRecord::Base
  has_many :orders do
    def for_user(user_id)
      where(user_id: user_id)
    end
  end
end

Item.first.orders.for_user(current_user)

UPDATE: I'd like to point out the advantage to association extensions as opposed to class methods or scopes is that you have access to the internals of the association proxy:

proxy_association.owner returns the object that the association is a part of. proxy_association.reflection returns the reflection object that describes the association. proxy_association.target returns the associated object for belongs_to or has_one, or the collection of associated objects for has_many or has_and_belongs_to_many.

More details here: http://guides.rubyonrails.org/association_basics.html#association-extensions

Laylalayman answered 3/3, 2013 at 5:1 Comment(2)
tried it and realized that this will circumvent eager loading items = Item.where(id: [1,2,3,4]).includes(:orders) followed with items.first.for_user(1) will fire another query to the db, just have to be aware of then do something like def finished_orders which will load several orders not eagerlyToggle
@Toggle its true; this is a common mistake in rails - anytime you tack on another where query to any relation that's already loaded, you will issue another query. In methods that I need to be super performant, I write methods that actually check whether the association is loaded (self.orders.loaded?) and if so, will filter via ruby, not via query. Of course, you need to be conscious of set sizes as well.Laylalayman
I
15

Instead of scopes I've just been defining class-methods, which has been working great

def self.age0 do
  where("blah")
end
Infold answered 18/8, 2010 at 5:36 Comment(5)
Can you stack these? Ie, Record.age0.newly_created.born_yesterday?Eliott
@Eliott Yes, class methods that return an Arel association are identical in function to scopes.Stuccowork
and if you don't run an arel method for some reason, you could always return self.scoped to make sure they're stackable anyways.Ornithosis
Maybe just to clarify for some newbs like myself, we are talking about Chaining Methods here right? Not Stacking? Aren't these two concepts different? You Stack Classes and Chain Methods? The word stacking throws me off course here...Tubate
Can you preload these? i.e. User.includes(:age0) so that iterating through a whole lot of these is scalableZora
P
10

I use something like:

class Invoice < ActiveRecord::Base
  scope :aged_0,  lambda{ where("created_at IS NULL OR created_at < ?", Date.today + 30.days).joins(:owner) }
end 
Piperpiperaceous answered 15/7, 2010 at 2:42 Comment(0)
Q
9

You can use merge method in order to merge scopes from different models. For more details search for merge in this railscast

Quinnquinol answered 26/7, 2012 at 13:15 Comment(0)
R
3

If you're just trying to get the user's orders, why don't you just use the relationship?

Presuming that the current user is accessible from the current_user method in your controller:

@my_orders = current_user.orders

This ensures only a user's specific orders will be shown. You can also do arbitrarily nested joins to get deeper resources by using joins

current_user.orders.joins(:level1 => { :level2 => :level3 }).where('level3s.id' => X)
Rightwards answered 20/4, 2011 at 3:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.