I would like to construct a query in ActiveRecord based on GET params and using named_scope. I thought I'd chain some scopes and add conditions based on GET param availability, but I fear this will overwork the db, sending a new query on each query portion added:
# in model
named_scope :sorted, :order => 'title, author'
named_scope :archived, :conditions => { :is_archived => true }
named_scope :by_date, lambda { |date| { :conditions => [ 'updated_at = ?', date ] } }
# in controller / helper
#articles = Article.sorted.all
#articles = #articles.by_date(params[:date]) if params[:date]
#articles = #articles.archived if params[:archived] == '1'
Another option I've thought of is building a method chaining string that'll then be sent to the object using Object#send, but that seems a bit dirty and somewhat problematic when the named_scope receives arguments (like by_date). I realize I can construct a query string to use with :conditions => ... in ActiveRecord::Base#find, but I thought I'd try first with named_scope to see if it's possible to do lazy querying with the latter. Any suggestions on how to do this using the named_scope and not having the database bombarded by queries? thanks.
You can make lambda more smarter
# in model
named_scope :sorted, :order => 'title, author'
named_scope :archived, lambda { |is_archived| (is_archived == 1) ? {:conditions => {:is_archived => true}} : {}}
named_scope :by_date, lambda { |date| date.nil? ? {} : { :conditions => [ 'updated_at = ?', date ]}}
# in controller / helper
#articles = Article.by_date(params[:date]).archived(params[:archived]).sorted
Related
I'm upgrading a Rails 3 code base to Rails 4 and have been updating to use the new scope style and querying interface.
I'm unsure how to switch over this scope that is using include as well as conditions in the scope's lambda.
scope :search_stuff, lambda { |search|
search_conditions = Array.new(4, "%#{search}%")
{
:include => {:sponsor => [], :routing_form_pi_users => [:user], :item => []},
:conditions => ["id like ? or title like ? or users.name like ? or sponsors.name like ?", *search_conditions]
}
}
When you compare the documentation about the query interface in different versions of the Rails Guide then you can see that the interface for eager loading of multiple associations didn't change that much.
The example in Rails 2.3.8 syntax:
Category.find 1, :include => {:posts => [{:comments => :guest}, :tags]}
The example for Rails 4.2 syntax:
Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
Therefore it should be possible to just copy the old nested hash into the new syntax:
scope :search_stuff, lambda { |search|
includes(item: [], routing_form_pi_users: [:user], sponsor: []).
where('id like ? or title like ? or users.name like ? or sponsors.name like ?', *Array.new(4, "%#{search}%"))
}
There's a named_scope in a project I'm working on that looks like the following:
# default product scope only lists available and non-deleted products
::Product.named_scope :active, lambda { |*args|
Product.not_deleted.available(args.first).scope(:find)
}
The initial named_scope makes sense. The confusing part here is how .scope(:find) works. This is clearly calling another named scope (not_deleted), and applying .scope(:find) afterwards. What/how does .scope(:find) work here?
A quick answer
Product.not_deleted.available(args.first)
is a named-scope itself, formed by combining both named scopes.
scope(:find) gets the conditions for a named-scope (or combination of scopes), which you can in turn use to create a new named-scope.
So by example:
named_scope :active, :conditions => 'active = true'
named_scope :not_deleted, :conditions => 'deleted = false'
then you write
named_scope :active_and_not_deleted, :conditions => 'active = true and deleted = false'
or, you could write
named_scope :active_and_not_deleted, lambda { self.active.not_deleted.scope(:find) }
which is identical. I hope that makes it clear.
Note that this has become simpler (cleaner) in rails 3, you would just write
scope :active_and_not_deleted, active.not_deleted
Scope is a method on ActiveRecord::Base that returns the current scope for the method passed in (what would actually be used to build the query if you were to run it at this moment).
# Retrieve the scope for the given method and optional key.
def scope(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
key ? scope[key] : scope
end
end
So in your example, the lambda returns the scope for a Product.find call after merging all the other named scopes.
I have a named_scope:
named_scope :active, {:conditions => {:active => true}}
In my console output, Object.active.scope(:find) returns:
{:conditions => {:active => true}}
Having a bit of difficulty figuring out how to create a named_scope from this SQL query:
select * from foo where id NOT IN (select foo_id from bar) AND foo.category = ? ORDER BY RAND() LIMIT 1;
Category should be variable to change.
What's the most efficient way the named_scope can be written for the problem above?
named_scope :scope_name, lambda { |category|
{
:conditions => ["id NOT IN (select foo_id from bar) AND foo.category = ?", category],
:order => 'RAND()',
:limit => 1
}
}
More of a comment than an answer but it won't really fit...
zed_oxff is on the ball.
To simplify things and keep them DRY, you might consider defining discrete named scopes instead of one big one, and chaining them together.
For example:
named_scope :random_order, :order => 'RAND()'
named_scope :limit, :lambda => { |limit| :limit => limit }
named_scope :whatever, ...
So you would use them as follows:
Person.random_order.limit(3).whatever
Let's say I have a form where users can search for people whose name starts with a particular name string, for example, "Mi" would find "Mike" and "Miguel". I would probably create a find statement like so:
find(:all, :conditions => ['name LIKE ?', "#{name}%"])
Let's say the form also has two optional fields, hair_color and eye_color that can be used to further filter the results. Ignoring the name portion of the query, a find statement for people that can take in an arbitrary number of optional parameters might look like this:
find(:all, :conditions => { params[:person] })
Which for my two optional parameters would behave as the equivalent of this:
find(:all, :conditions => { :hair_color => hair_color, :eye_color => eye_color })
What I can't figure out is how to merge these two kinds of queries where the required field, "name" is applied to the "like" condition above, and the optional hair_color and eye_color parameters (and perhaps others) can be added to further filter the results.
I certainly can build up a query string to do this, but I feel there must be a "rails way" that is more elegant. How can I merge mandatory bind parameters with optional parameters?
This is the perfect use of a named scope.
create a named scope in the model:
named_scope :with_name_like, lambda {|name|
{:conditions => ['name LIKE ?', "#{name}%"]}
}
At this point you can call
Model.with_name_like("Mi").find(:all, :conditions => params[:person])
And Rails will merge the queries for you.
Edit: Code for Waseem:
If the name is optional you could either omit the named scope from your method chain with an if condition:
unless name.blank?
Model.with_name_like("Mi").find(:all, :conditions => params[:person])
else
Model.find(:all, :conditions => params[:person])
end
Or you could redefine the named scope to do the same thing.
named_scope :with_name_like, lambda {|name|
if name.blank?
{}
else
{:conditions => ['name LIKE ?', "#{name}%"]}
end
}
Update
Here is the Rails 3 version of the last code snippet:
scope :with_name_like, lambda {|name|
if not name.blank?
where('name LIKE ?', "#{name}%")
end
}
To comply also with Waseem request, but allowing nil instead blank? (which is udeful in case you want to use "#things = Thing.named_like(params[:name])" directly)
named_scope :named_like, lambda do |*args|
if (name=args.first)
{:conditions => ["name like ?",name]}
else
{}
end
end
# or oneliner version:
named_scope :named_like, lambda{|*args| (name=args.first ? {:conditions => ["name like ?",name]} : {}) } }
I hope it helps
I want to find records on a combination of created_on >= some date AND name IN some list of names.
For ">=" I'd have to use sql condition. For "IN" I'd have to use a hash of conditions where the key is :name and the value is the array of names.
Is there a way to combine the two?
You can use named scopes in rails 2.1 and above
Class Test < ActiveRecord::Base
named_scope :created_after_2005, :conditions => "created_on > 2005-01-01"
named_scope :named_fred, :conditions => { :name => "fred"}
end
then you can do
Test.created_after_2005.named_fred
Or you can give named_scope a lambda allowing you to pass in arguments
Class Test < ActiveRecord::Base
named_scope :created_after, lambda { |date| {:conditions => ["created_on > ?", date]} }
named_scope :named, lambda { |name| {:conditions => {:name => name}} }
end
then you can do
Test.created_after(Time.now-1.year).named("fred")
If you're using an older version Rails, Honza's query is close, but you need to add parentheses for the strings that get placed in the IN condition:
Person.find(:all, :conditions => ["created_at > ? AND name IN (?)", date, names])
Using IN can be a mixed bag: it's fastest for integers and slowest for a list of strings. If you find yourself using just one name, definitely use an equals operator:
Person.find(:all, :conditions => ["created_at > ? AND name = ?", date, name])
The cool thing about named_scopes is that they work on collections too:
class Post < ActiveRecord::Base
named_scope :published, :conditions => {:status => 'published'}
end
#post = Post.published
#posts = current_user.posts.published
For more on named_scopes see Ryan's announcement and the Railscast on named_scopes
class Person < ActiveRecord::Base
named_scope :registered, lambda { |time_ago| { :conditions => ['created_at > ?', time_ago] } }
named_scope :with_names, lambda { |names| { :conditions => { :names => names } } }
end
If you are going to pass in variables to your scopes you have to use a lambda.
You can chain the where clause:
Person.where(name: ['foo', 'bar', 'baz']).where('id >= ?', 42).first
The named scopes already proposed are pretty fine. The clasic way to do it would be:
names = ["dave", "jerry", "mike"]
date = DateTime.now
Person.find(:all, :conidtions => ["created_at > ? AND name IN ?", date, names])
I think I'm either going to use simple AR finders or Searchgasm.