Ruby on Rails Named Scope Implementation

ruby on rails named scope implementation

There are two tricks (or patterns if you will) employed in the named_scope magic.

Proxy pattern - calling a named scope method on a class or an association always returns an instance of the ActiveRecord::NamedScope::Scope class, not a colleciton of filtered AR objects. This pattern, altough very useful, makes things kind of blurry sometimes, since the proxy objects are ambivalent in their nature.

Lazy loading - thanks to lazy loading (which in this context means - hitting the database only if neccessary) named scopes can be chained up to the point when you need to work with the collection defined by the scopes. Whenever you request the underlying colleciton, all the chained scopes are evaluated and a database query is executed.

One final note: There's one thing to have in mind when playing with named scopes (or with any thing that uses delegation of some kind) in IRB. Everytime you hit Enter, the thing you wrote beforehand is evaluated and the inspect method is called on the returned value. In the case of chained named scopes, although the whole expression is evaluated to a Scope instance, when the IRB calls the inspect method on it, the scopes are evaluated and the database query is fired. This is caused by the fact that the inspect method is by means of delegation propagated through all the scope objects up to the underlying collection.

What is scope/named_scope in rails?

A scope is a subset of a collection. Sounds complicated? It isn't. Imagine this:

You have Users. Now, some of those Users are subscribed to your newsletter. You marked those who receive a newsletter by adding a field to the Users Database (user.subscribed_to_newsletter = true). Naturally, you sometimes want to get those Users who are subscribed to your newsletter.

You could, of course, always do this:

User.where(subscribed_to_newsletter: true).each do #something

Instead of always writing this you could, however, do something like this.

#File: users.rb
class User < ActiveRecord::Base
scope :newsletter, where(subscribed_to_newsletter: true)
#yada yada
end

If you're using Rails 4 or newer, do this instead:

#File: users.rb
class User < ActiveRecord::Base
scope :newsletter, -> { where(subscribed_to_newsletter: true) }
#yada yada
end

This allows you to access your subscribers by simply doing this:

User.newsletter.each do #something

This is a very simple example but in general scopes can be very powerful tools to easy your work.

Check out this link: API Description

Rails 4: how to use named scope with has_many associations

I believe it should read like this in Rails 4:

scope :live, -> { where(is_deleted: 0, sent_to_api: 1) }

The rails 4 docs and all examples in it show you passing in a callable object to the scope to ensure it gets called each time. If it doesn't work like this try implementing it as a class method and see how that works out for you.

http://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html

Ruby on Rails - Implementing Simple Search with scopes

Does your search function look like this, as in the railscast?

def self.search(search)
if search
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
else
find(:all)
end
end

If so, I'm guessing your getting back an array of results, which cannot be chained with additional query methods.

So when you use the query methods (where, order, limit, etc), it returns an ActiveRelation object, which is basically a proxy for your eventual result set. It won't actually hit your DB until you try to use the result set by calling .all, .first, .each, something like that.

You could write your search method like this instead:

def self.search(search)
if search
where('name LIKE ?', "%#{search}%")
end
end

So if search is present, you'll scope your AR object down to the matching results. Otherwise you won't, which will have the same result as returning everything. Then you can still order those results, limit them, paginate them, whatever.

Creating a named scope that includes an associated model

Reading @TheChamp's reasoning in his answer, it seems like just doing a method pasting in the query part works equally well. Chainable as well.

class Event
belongs_to :organizer
end

class Organizer
has_many :events

def upcoming_events
self.events.order('begin_day asc').where('begin_day >= ?', Date.today).where(published: true)
end

end
end

This works now:

@organizer.upcoming_events.limit(8)

Initialize instance variable through named scope

named scopes are not instantiating any single record, until they are iterated. And they may be, as you correctly say, be chained with more scopes.

I see two possible solutions. One relies on the database to populate a fake column with the matching value. It should be simple using a special :select option to the query (or a special argument to .select() in rails 3)

The other is to rely on the controller. This is much easier to implement, but you have to ensure that the "match" in ruby is the same than the "LIKE" in the database. I'm speaking about collation, unicode normalized forms etc. Most probably there is at least one case in which they behave differently in both engines.

Is there a way to combine named scopes into a new named scope?

Well I'm still new to rails and I'm not sure exactly what you're going for here, but if you're just going for code reuse why not use a regular class method?


def self.ab(a, b)
a(a).b(b)
end

You could make that more flexible by taking *args instead of a and b, and then possibly make one or the other optional. If you're stuck on named_scope, can't you extend it to do much the same thing?

Let me know if I'm totally off base with what you're wanting to do.

How do you scope ActiveRecord associations in Rails 3?

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.

How can I use a named scope in my model against an array of items?

If you pass an array as the value, ActiveRecord is smart enough to compare for inclusion in the array. For example,

Book.where(:author_id => [1, 7, 42])

produces a SQL query with a WHERE clause similar to:

WHERE "author_id" IN (1, 7, 42)

You can take advantage of this in a scope the same way you would set normal conditions:

class Book < ....
# Rails 3
scope :by_author, lambda { |author_id| where(:author_id => author_id) }

# Rails 2
named_scope :by_author, lambda { |author_id
{ :conditions => {:author_id => author_id} }
}
end

Then you can pass a single ID or an array of IDs to by_author and it will just work:

Book.by_author([1,7,42])

How to write named scope for sub queries in Active Record Rails

I had a same implementation, hope you can get help.

category.rb

class Category < ApplicationRecord
has_many :category_details
end

category_detail.rb

class CategoryDetail < ApplicationRecord
belongs_to :category
scope :get_first_category_vise_details, -> {order(:category_id).group_by(&:category_id).map{|cat_detail_group| cat_detail_group.last.first} }
end

select * from categories;

+----+------+
| id | name |
+----+------+
| 1 | abc |
| 2 | xyz |
+----+------+

select * from category_details;

+----+-------------+-------------+
| id | category_id | description |
+----+-------------+-------------+
| 1 | 1 | test |
| 2 | 1 | test1 |
| 3 | 1 | test2 |
| 4 | 2 | test |
| 5 | 2 | testabc |
+----+-------------+-------------+

CategoryDetail.get_first_category_vise_details

[#<CategoryDetail id: 1, category_id: 1, description: "test">, #<CategoryDetail id: 4, category_id: 2, description: "test">]


Related Topics



Leave a reply



Submit