Find by many conditions in Ruby on Rails - ruby-on-rails

There is a search action in RoR that can handle some params e.g.:
params[:name] # can be nil or first_name
params[:age] # can be nil or age
params[:city] # can be nil or country
params[:tag] # can be nil or country
The model name is Person. It also has_many :tags.
When finding persons I need like to AND all the conditions that are present. Of course, it not rational and not DRY.
What I tried to do:
conditions = []
conditions << [ "name like ?", params[:name]+"%" ] if params[:name].present?
conditions << [ "age = ?", params[:age] ] if params[:age].present?
conditions << [ "city = like ?", params[:city]+"%" ] if params[:city].present?
#persons = Person.all(:conditions => conditions )
#What about tags? How do include them if params[:tag].present?
Of course, I want my code to be DRY. Now it's not. Even more, it will cause an exception if params[:age] and params[:name] and params[:city] are not present.
How can I solve? And how do I include tags for persons filtered by tag.name=params[:tag] (if params[:tag].present?) ?

You should do something like this:
lets say filters parameters include this:
filters = {
:name_like => "Grienders",
:age_equal => 15
}
Now you define methods for each
class Person
def search_with_filters(filters)
query = self.scoped
filters.each do |key, values|
query = query.send(key, values)
end
return query
end
def name_like(name)
where("name like ?", name)
end
def age_equal(age)
where(:age => age
end
end
Do you see the method search_with_filters will be the "controlling" method that will take a set of query conditions (name_like, age_equal, etc...) and pass them out to matching method name using the send method, and along with that we also pass the condition which will be the parameter of the method.
The reason why this way is good is because you can scale to any number of conditions (your filter lets say get huge) and also the code is very clean because all you have to do is populate your filters parameter. The method is very readable and very modular and also easy to test

To include the tags condition in your query:
if params[:tag].present?
#persons = Person.all(:conditions => conditions).includes(:tags).where("tags.name = ?", params[:tag])
else
#persons = Person.all(:conditions => conditions)
end
As for there error you are getting when params[:age] .. is not present - This is very strange because it is supposed to return false incase the key is not set in params. Could you please paste the error you are getting?

I would work extensively with scopes for this. Starting with default scope, merge other scopes based on conditions.
Write scopes (or class methods):
class Person << AR::Base
...
NAME_PARTS = ['first_name', 'last_name']
scope :by_name, lambda { |q| where([NAME_PARTS.join(' LIKE :q OR ') + ' LIKE :q', { :q => "%#{q}%" }]) }
scope :by_email, lambda { |q| joins(:account).where(["accounts.email LIKE :q", { :q => "%#{q}%" }]) }
scope :by_age, where(["people.age = ?", q])
scope :tagged, lambda { |q| joins(:tags).where(["tags.name LIKE :q", { :q => "%#{q}%" }]) }
end
Refer:
Scopes in Rails 3
Scopes overhaul section in How Rails 3 Makes Your Life Better
Now, merge the scopes only when the condition is met. As I understand your condition, is the value for a particular search item is nil (like, age is not given), do not search for that scope.
...
def search(object)
interested_fields = ['name', 'age', 'email', 'tags']
criteria = object.attributes.extract!(*interesting_fields) # returns { :age => 20, ... }
scope = object.class.scoped
build(criteria).each do |k, v|
scope = scope.send(k.to_sym, v)
end
scope.all
end
# This method actually builds the search criteria.
# Only keep param which has value and reject the rest.
def build(params)
required = ['name', 'age', 'email', 'tags']
params.delete_if { |k, v| required.include?(k) && v.blank? }
params
end
...
Refer:
extract!

Related

Ruby on Rails search with multiple parameters

For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.

SQL-like syntax for array of objects

I have an array of objects. I need to use an SQL-like condition WHERE field like '%value%' for some object fields in this array.
How to do it?
Edited:
For example I have array with Users and I need to find all users with first_name like ike and email like 123.
Edited2:
I need method to get Users with first_name like smth and email like smth from ARRAY of my Users. Users have first_name and email.
Edited3:
All my users are in database. But I have some business logic, at the end of this logic I have array with Users. Next I need to filter this array with some text: ike for first_name and 123 for email. How to do it?
arr = %w[hello quick bool boo foo]
arr.select { |x| x.include?("foo") }
=> ["bool", "boo", "foo"]
or in your case, if you have an array of objects, you can do:
x.first_name.include?("foo") && x.email.include?("123")
For more customization, you can use Array#select with Regexeps
If you can just use ruby methods for this that do something like this:
User = Struct.new(:email, :first_name) # Just creating a cheap User class here
users = [
User.new('1#a.com' , 'ike'),
User.new('123#a.com', 'bob'),
User.new('123#a.com', 'ike'),
]
# results will be an array holding only the last element in users
results = users.find_all do |user|
user.email =~ /123/ and
user.first_name =~ /ike/
end
Writing your own sql parser seems like a pretty bad idea, but if you really need to parse simple SQL where clauses you could do something like this:
User = Struct.new(:email, :first_name) # Just creating a cheap User class here
users = [
User.new('1#a.com' , 'ike'),
User.new('123#a.com', 'bob'),
User.new('123#a.com', 'ike'),
]
def where(array, sql)
sql = sql.gsub(/\s+AND\s+/, ' ') # remove AND's
terms = Hash[ *sql.split(/\s+LIKE\s+| /) ] # turn "a LIKE 'b'" into {'a': "'b'"}
array.find_all do |item|
terms.all? do |attribute, matcher|
matcher = matcher.gsub('%', '.*') # convert %
matcher = matcher.gsub(/^['"]|["']$/, '') # strip quotes
item.send(attribute) =~ /^#{matcher}$/
end
end
end
# results will be an array holding only the last element in users
results = where(users, "first_name LIKE '%ike%' AND email LIKE '%123%'")
This will only work for where clauses what only contain LIKE statements connected by AND's. Adding support for all valid SQL is left as an exercise for the reader, (or better yet, just left alone).
I've built a ruby gem uber_array to enable sql-like syntax for arrays of Hashes or Objects you might want to try.
require 'uber_array'
# Array of Hash elements with strings as keys
items = [
{ 'name' => 'Jack', 'score' => 999, 'active' => false },
{ 'name' => 'Jake', 'score' => 888, 'active' => true },
{ 'name' => 'John', 'score' => 777, 'active' => true }
]
uber_items = UberArray.new(items)
uber_items.where('name' => 'John')
uber_items.where('name' => /Ja/i)
uber_items.like('ja')
uber_items.where('name' => %w(Dave John Tom))
uber_items.where('score' => 999)
uber_items.where('score' => ->(s){s > 900})
uber_items.where('active' => true, 'score' => 800..900)

Filtering results in Rails

I am showing a list of questions when the user use the index action. I want to filter this list, showing only rejected questions, questions that only have images attached to them etc.
How do you do that? Do you just add code in the index action that checks if the different named parameters is in the request parameter hash and use them build a query.
myurl.com/questions?status=approved&only_images=yes
Or are there better ways?
You can use has_scope to do this elegantly:
# Model
scope :status, proc {|status| where :status => status}
scope :only_images, ... # query to only include questions with images
# Controller
has_scope :status
has_scope :only_images, :type => boolean
def index
#questions = apply_scopes(Question).all
end
To keep your controller thin and avoid spaghetti code you can try to use following way:
Controller:
def index
#questions = Question.filter(params.slice(:status, :only_images, ...) # you still can chain with .order, .paginate, etc
end
Model:
def self.filter(options)
options.delete_if { |k, v| v.blank? }
return self.scoped if options.nil?
options.inject(self) do |scope, (key, value)|
return scope if value.blank?
case key
when "status" # direct map
scope.scoped(:conditions => {key => value})
when "only_images"
scope.scoped(:conditions => {key => value=="yes" ? true : false})
#just some examples
when "some_field_max"
scope.scoped(:conditions => ["some_field <= ?", value])
when "some_field_min"
scope.scoped(:conditions => ["some_field >= ?", value])
else # unknown key (do nothing. You might also raise an error)
scope
end
end
end
So, I think there are places where you need to code to be good in such a scenario; the model and the controller.
For the model you should use scopes.
#Model
scope :rejected, lambda { where("accepted = false") }
scope :accepted lambda { where("accepted = true") }
scope :with_image # your query here
In the controller,
def index
#questions = #questions.send(params[:search])
end
You can send the method name from the UI and directly pass that to the scope in the model. Also, you can avoid an "if" condition for the .all case by passing it from the UI again.
But as this directly exposes Model code to view, you should filter any unwanted filter params that come from the view in a private method in the controller using a before_filter.

Elegant ruby function, need help cleaning this up so it looks like ruby

I have to parameters: name and age
def self.search(name, age)
end
If name is not empty/nil, then add it to the search expression.
If age is not empty/nil, then add it to the search expression.
if both are nil, return all.
So far I have:
def self.search(name, age)
if name.nil? && age.nil?
return User.all
end
end
Finding it hard to write this in a elegant way.
def self.search(name, age)
conditions = {}
conditions[:name] = name if name
conditions[:age] = age if age
User.find(:all, :conditions => conditions)
end
I'm not sure what sort of search you're doing but I prefer to handle these sort of things in a scope.
class User < ActiveRecord::Base
scope :by_name, lamba{|name| name.nil?? scoped : where(:name=>name) }
scope :by_age, lamba{|age| age.nil?? scoped : where(:age=>age) }
def self.search(name, age)
User.by_name(name).by_age(age)
end
end
It's a bit more code overall I suppose but it's more reusable and everything is in it's place.
In Rails 3 you can do it like this:
def self.search(name, age)
scope = User
scope.where(:name => name) if name.present?
scope.where(:age => age) if age.present?
scope
end
Note the use of present? rather than nil? to skip empty strings as well as nil.
In the other comment you mentioned wanting to OR these conditions. ActiveRecord does not provide convenient facilities for that; all conditions are AND by default. You will need to construct your own conditions like so:
def self.search(name, age)
scope = User
if name.present? && age.present?
scope.where('name = ? OR age = ?', name, age)
else
scope.where(:name => name) if name.present?
scope.where(:age => age) if age.present?
end
scope
end

Dynamic find conditions in active record

I have an index action in rails that can handle quite a few params eg:
params[:first_name] # can be nil or first_name
params[:age] # can be nil or age
params[:country] # can be nil or country
When finding users I would like to AND all the conditions that are not nil. This gives me 8 permutations of the find conditions.
How can I can I keep my code DRY and flexible and not end up with a bunch of if statements just to build the conditions for the find. Keep in mind that if no conditions are specified I just want to return User.all
How about something like:
conditions = params.only(:first_name, :age, :country)
conditions = conditions.delete_if {|key, value| value.blank?}
if conditions.empty?
User.all
else
User.all(:conditions => conditions)
end
I would normally use named scopes for something like this:
class User < ActiveRecord::Base
named_scope :name_like, lambda {|name| {:conditions => ["first_name LIKE ?", "#{name}%"]}}
named_scope :age, lambda {|age| {:conditions => {:age => age}}}
named_scope :in_country, lambda {|country| {:conditions => {:country => country}}}
end
class UsersController < ActionController
def index
root = User
root = root.name_like(params[:first_name]) unless params[:first_name].blank?
root = root.age(params[:age]) unless params[:age].blank?
root = root.country(params[:country]) unless params[:age].blank?
#users = root.paginate(params[:page], :order => "first_name")
end
end
That's what I normally do.
This seems to work quite nicely:
conditions = params.slice(:first_name, :age, :country)
hash = conditions.empty? ? {} : {:conditions => conditions}
#users = User.all hash
Using James Healy answer, I modify the code to be used in Rails 3.2 (in case anyone out there need this).
conditions = params.slice(:first_name, :age, :country)
conditions = conditions.delete_if {|key, value| value.blank?}
#users = User.where(conditions)
You could try Ambition, or a number of other ActiveRecord extensions.
This works for me too
conditions = params[:search] ? params[:search].keep_if{|key, value| !value.blank?} : {}
User.all(:conditions => conditions)
If you happen to be on an ancient project (Rails 2.x) and very messy, you could do something like the following for adding new fields to the original query.
Original code:
User.find(:all,
:conditions => ['first_name LIKE ? AND age=? AND country=?',
"#{name}%", age, country]
Adding a new dynamic condition on zip_code field:
zip_code = params[:zip_code] # Can be blank
zip_query = "AND zip_code = ?" unless zip_code.blank?
User.find(:all,
:conditions => ['first_name LIKE ? AND age=? AND country=? #{zip_query}',
"#{name}%", age, country, zip_code].reject(&:blank?)
Adding a reject(&:blank?) to the conditions arrays will filter the nil value.
Note: The other answers are much better if you are coding from zero, or refactoring.

Resources