8

The problem essence as I see it

One day, if I'm not mistaken, I have seen an example of reusing a named_scope to define another named_scope. Something like this (can't remember the exact syntax, but that's exactly my question):

named_scope :billable, :conditions => ...
named_scope :billable_by_tom, :conditions => {
    :billable => true, 
    :user => User.find_by_name('Tom')
}

The question is: what is the exact syntax, if it's possible at all? I can't find it back, and Google was of no help either.

Some explanations

Why I actually want it, is that I'm using Searchlogic to define a complex search, which can result in an expression like this:

Card.user_group_managers_salary_greater_than(100)

But it's too long to be put everywhere. Because, as far as I know, Searchlogic simply defines named_scopes on the fly, I would like to set a named_scope on the Card class like this:

named_scope from_big_guys, { user_group_managers_salary_greater_than(100) }

- this is where I would use that long Searchlogic method inside my named_scope. But, again, what would be the syntax? Can't figure it out.

Resume

So, is named_scope nesting (and I do not mean chaining) actually possible?

mxgrn
  • 1,733
  • 16
  • 20

5 Answers5

8

You can use proxy_options to recycle one named_scope into another:

class Thing
  #...
  named_scope :billable_by, lambda{|user| {:conditions => {:billable_id => user.id } } }
  named_scope :billable_by_tom, lambda{ self.billable_by(User.find_by_name('Tom').id).proxy_options }
  #...
end

This way it can be chained with other named_scopes.

I use this in my code and it works perfectly.

I hope it helps.

Oinak
  • 1,805
  • 1
  • 12
  • 13
  • Caveat is that proxy_options only returns the scope of the latest named scope, so this cannot be done against another derived named scope. – aceofspades Sep 08 '10 at 22:31
2

Refer to this question raised time ago here at SO. There is a patch at lighthouse to achieve your requirement.

Community
  • 1
  • 1
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
2

Rails 3+

I had this same question and the good news is that over the last five years the Rails core team has made some good strides in the scopes department.

In Rails 3+ you can now do this, as you'd expect:

scope :billable,        where( due: true )
scope :billable_by_tom, -> { billable.where( user: User.find_by_name('Tom') ) }

Invoice.billable.to_sql         #=> "... WHERE due = 1 ..."
Invoice.billiable_by_tom.to_sql #=> "... WHERE due = 1 AND user_id = 5 ..."

FYI, Rails 3+ they've renamed named_scope to just scope. I'm also using Ruby 1.9 syntax.

Bonus Round: Generic Scope.

If there are multiple people that are "billable" besides just "Tom" then it might be useful to make a generic scope that accepts a name param that gets passed into the block:

scope :billable_by, lambda { |name| billable.where( user: User.find_by_name( name ) ) }

Then you can just call it with:

Invoice.billable_by( "Tom" ).to_sql #=> "... WHERE due = 1 AND user_id = 5 ..."

Btw, you can see I used the older lambda syntax in the bonus round. That's because I think the new syntax looks atrocious when you're passing a param to it: ->( name ) { ... }.

Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
  • At least since 3.2 there is a clever solution : `scope :optional, ->() {where(option: true)}` `scope :accepted, ->() {where(accepted: true)}` `scope :optional_and_accepted, ->() { self.optional.merge(self.accepted) }` – Meta Lambda May 29 '15 at 15:36
  • @user2481743 Not sure how that's clever (or better). That requires two separate queries and merging the results as opposed to a single query with combined conditionals. – Joshua Pinter May 29 '15 at 23:26
  • Well, Absolutely not, `merge` will merge the scope, not the results. This will result in a single query and a reusable chainable scope. Have a try, I am sur you'll enjoy this solution ! If the argument of `merge` is an ActiveRecord::Relation it merges the conditions : http://api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html#method-i-merge This result in a single query. – Meta Lambda May 31 '15 at 15:48
  • 1
    @user2481743 Oh, that's clever of `ActiveRecord` to do that. So, using your example, couldn't you just go: `scope :optional_and_accepted, ->() { self.optional.accepted }` and not have to use the `merge`? – Joshua Pinter May 31 '15 at 17:22
  • Because it is just an example, it could be any other scope defined on any other AR model with a `joins()`. So I wished to make it explicit. You are right it could be shortened but it would not serve the teaching purpose. – Meta Lambda Jun 11 '15 at 09:11
  • @user2481743 Fair enough. Just making sure I wasn't missing something. Thanks. – Joshua Pinter Jun 12 '15 at 15:06
1

Chain Scopes.

Why not have a scope for stuff just by Tom in general, like:

scope :by_tom, where( user: User.find_by_name('Tom') )

And then you can get those records that are "billable by Tom" with:

Record.billable.by_tom
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
  • 1
    Thinking more on this, you'd really want to put this in a lambda so you could pass in any name there, not just 'Tom', like: `scope :by_tom, ->(name) { where( user: User.find_by_name(name) ) }` – Joshua Pinter Jul 08 '15 at 04:57
  • Typo in my comment above, it should be something like `scope :by, ->(name) { where( user: User.find_by_name(name) ) }` and then you can call it with `Record.by('Tom')`. – Joshua Pinter Aug 24 '15 at 14:53
0

You can use a method to combine some named_scope like :


def self.from_big_guys
  self.class.user_group_managers_salary_greater_than(100)
end

This feature is add on Rails 3 with new syntax (http://m.onkey.org/2010/1/22/active-record-query-interface)

shingara
  • 46,608
  • 11
  • 99
  • 105
  • Defining a class method is inconvenient because it's not a named_scope, so, for example, it can't be chained as such. – mxgrn Mar 14 '10 at 21:24
  • Actually it can be -- just return a scope using where() or whatever: http://edgerails.info/articles/what-s-new-in-edge-rails/2010/02/23/the-skinny-on-scopes-formerly-named-scope/index.html – adzdavies Mar 29 '11 at 01:20