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
Related
This might be more of a Ruby syntax thing than anything else. I'm having difficulty getting two limiting conditions on a SomeObject.find going.
Separated, the conditions seem to work:
if search != ''
find(:all, :conditions => ['name LIKE ?', "%#{search}%"])
else
find(:all, :conditions => ['active', 1]).shuffle
end
What I'm going for for the first case is this:
find(:all, :conditions => ['name LIKE ?', "%#{search}%"], ['active', 1])
But the line throws syntax error, unexpected ')', expecting tASSOC.
Rails 2
find(:all, :conditions => ['name LIKE ?', "%#{search}%"], ['active', 1]) isn't the proper syntax for passing hashes to a method. You can leave the curly braces off of a hash if it is the last argument to a method, but in this case you are passing an array as the last argument.
Use the following instead:
find(:all, :conditions => ["name LIKE ? AND active = ?", "%#{search}%", 1])
or
params = {:search => "%#{search}%", :active => 1}
find(:all, :conditions => ["name LIKE :search AND active = :active", params])
Rails 3 and 4
You would probably want to do something more like the following for recent Rails versions:
scope :active, -> { where(active: true) }
scope :name_like, ->(search) { where("name LIKE ?", "%#{search}%") }
And then you'd call it like this:
YourModel.active.name_like("Bunnies")
That allows you to reuse those specific queries in different combinations throughout the application. It also makes the code retrieving the data super easy to read.
If you don't like the scope syntax, you can also define these as class methods:
def self.active
where(active: true)
end
def self.name_like(search)
where("name LIKE ?", "%#{search}%")
end
You can also chain scopes on the fly. That will allow you to start building a chain of relation objects and then choose to include others based on conditions. Here's what it could look like when applied to the original question to achieve the same results:
results = active
results = results.name_like(search) if search.present?
Instead of using if-else which would involve a redundancy for checking on active = 1, a simpler syntax would be something like this:
result = where(:active => 1)
result = result.where('name like ?', "%#{search}%") unless search.blank?
As far as the error your seeing, it wouldn't appear to be caused by the code that you are posting. A stack trace may help narrow it down further...
i think you are using rails 2
try this.
find(:all, :conditions => ['name LIKE ? and active = 1', "%#{search}%"])
rails 3 syntax
where('name LIKE ? and active = 1', "%#{search}%")
For Rails 4, In rails new find_by and find_by! methods are introduced
Read answer
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
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}}
What is a difference between named_scope and named_scope + lambda Ruby on Rails code statements?
named_scope :with_avatar, :conditions => ['avatar IS NOT NULL']
and
named_scope :date_from, lambda { |date| { :conditions => ['created_at >= ?', DateTime.strptime(date, '%d.%m.%Y')] } }
With the lambda, you can specify arguments to the scope.
In the above case, you could say
Model.with_avatar and Model.date_from("10.08.2010"), however you cannot say for example Model.with_avatar(false)
In this case, you need to be somewhat careful about the arguments to the lambda: unless you pass an argument to date_from, it will probably not work. One "workaround" is to use |*date| , check if it was passed in and set it to some default value if it wasn't.
I have 2 simple named scopes defined as such:
class Numbers < ActiveRecord::Base
named_scope :even, :conditions => {:title => ['2','4','6']}
named_scope :odd, :conditions => {:title => ['1','3','5']}
end
if I call Numbers.even I get back 2,4,6 which is correct
if I call Numbers.odd I get back 1,3,5 which is correct
When I chain them together like this: Numbers.even.odd I get back 1,3,5 because that is the last scope I reference. So if I say Numbers.odd.even I would actually get 2,4,6.
I would expect to get 1,2,3,4,5,6 when I chain them together. One other approach I tried was this:
named_scope :even, :conditions => ["title IN (?)", ['2', '4','6']]
named_scope :odd, :conditions => ["title IN (?)", ['1', '3','5']]
But I get no results when I chain them together because the query it creates looks like this:
SELECT * FROM `numbers`
WHERE ((title IN ('1','3','5')) AND (title IN ('2','4',6')))
The 'AND' clause should be changed to OR but I have no idea how to force that. Could this be an issue with ActiveRecord??
It's an issue with how ActiveRecord handles scopes. When you apply multiple scopes, the results are joined together with AND. There is no option for using OR.
What you need to do instead is combine two result sets:
Numbers.even.all + Numbers.odd.all