Reusing named_scope to define another named_scope
Asked Answered
M

5

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?

Maddocks answered 14/3, 2010 at 20:31 Comment(0)
S
2

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

Somatotype answered 15/3, 2010 at 3:49 Comment(0)
B
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.

Bradberry answered 20/7, 2010 at 12:52 Comment(1)
Caveat is that proxy_options only returns the scope of the latest named scope, so this cannot be done against another derived named scope.Lithographer
S
2

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

Somatotype answered 15/3, 2010 at 3:49 Comment(0)
I
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 ) { ... }.

Interflow answered 13/4, 2015 at 14:1 Comment(6)
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) }Disbelieve
@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.Interflow
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 : api.rubyonrails.org/classes/ActiveRecord/… This result in a single query.Disbelieve
@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?Interflow
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.Disbelieve
@user2481743 Fair enough. Just making sure I wasn't missing something. Thanks.Interflow
I
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
Interflow answered 29/5, 2015 at 23:30 Comment(2)
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) ) }Interflow
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').Interflow
C
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)

Contraption answered 14/3, 2010 at 20:39 Comment(2)
Defining a class method is inconvenient because it's not a named_scope, so, for example, it can't be chained as such.Maddocks
Actually it can be -- just return a scope using where() or whatever: edgerails.info/articles/what-s-new-in-edge-rails/2010/02/23/…Consternate

© 2022 - 2024 — McMap. All rights reserved.