Optimise ignoring of undefined variables when building find conditions in Rails - sql

I have a an method that retrieves Groups that are present in certain areas. Groups are given a country_id, region_id and city_id
The UI gives three select boxes to choose a country, a region from that country and then a city from that region. To find all groups in a particular city, I have this code:
#groups = Group.find(:all, :conditions => {:city_id => params[:city_id]})
This all works fine, but I also want it to find all groups in an area when the lower criteria isn't specified. For example, If a country and region are given, but not city, I'd like to find it by the region.
What I'm doing is this:
if !params[:city_id].nil?
#groups = Group.find(:all, :conditions => {:city_id => params[:city_id]})
else
if !params[:region_id].nil?
#groups = Group.find(:all, :conditions => {:region_id => params[:region_id]})
else
#groups = Group.find(:all, :conditions => {:country_id => params[:country_id]})
end
end
This works perfectly well, but it seems like it's a little inefficient. Am I doing it the best way or can I streamline a little?
One idea I had was to have a single find checking against all parameters, but I could not work out how to effectively 'ignore' parameters that were nil - my main thought was to check which ones were not set and set them to something like '*' or 'true', but that's not how SQL plays the game.

Sounds like a job for named scopes:
class Group < ActiveRecord::Base
named_scope :in_city, lambda { |city_id| {
:conditions => { :city_id => city_id }
}}
named_scope :in_region, lambda { |region_id | {
:conditions => { :region_id => region_id }
}}
named_scope :in_country, lambda { |country_id | {
:conditions => { :country_id => country_id }
}}
end
This establishes some simple scopes for restricting the Group records. Presumably you have indexed your database properly so these are quick to resolve.
The controller is much easier to implement then:
def index
#group_scope = Group
if (!params[:city_id].blank?)
#group_scope = #group_scope.in_city(params[:city_id])
elsif (!params[:region_id].blank?)
#group_scope = #group_scope.in_region(params[:region_id])
elsif (!params[:country_id].blank?)
#group_scope = #group_scope.in_country(params[:country_id])
end
#groups = #group_scope.all
end
Generally you should be testing for .blank? instead of .nil? as some form elements can send in empty results, such as a select with something akin to "All" as the default.

You could use some Ruby idioms to get something a little more succinct.
Try something like this: (untested code!)
def index
#groups = Group.find :all, :conditions => [:city_id, :region_id, :country_id].inject {} do |conditions, name|
conditions[name] = params[name] unless params[name].blank?
conditions
end
end

If every value in params is a candidate for :conditions you can just do this:
#groups = Group.all(:conditions => params.reject { |idx, val| val.nil? })
This just throws out nil values from params and uses the remaining values for conditions.
If you don't want to use all of the values in params, you have two options. You can just get rid of a bunch of redundancy in your original code:
conditions = if !params[:city_id].nil?
{ :city_id => params[:city_id] }
elsif !params[:region_id].nil?
{ :region_id => params[:region_id] }
else
{ :country_id => params[:country_id] }
end
#groups = Group.all(:conditions => conditions)
You can knock of a few more lines like this, but it sacrifices a bit of readability IMO:
conditions = if !params[:city_id].nil? then { :city_id => params[:city_id] }
elsif !params[:region_id].nil? then { :region_id => params[:region_id] }
else { :country_id => params[:country_id] }
end
Or you can do something like this:
conditions = [:city_id, :region_id, :country_id].inject({}) do |hsh, sym|
hsh[sym] = params[sym] unless params[sym].nil?
hsh
end
#groups = Group.all(:conditions => conditions)
This has the advantage that you don't need to add another condition for each symbol.

Related

How to do multiple where statements for the same column in ActiveRecord?

Using rails 3, How could one do multiple where statements without complicated stuff or extra gems?
Im having this column "accepted" and would like to get all the values where accepted == false and accepted == null
Both of examples below fail:
#scholars = Scholars.where(:scholar_id => current_user.id).where(["accepted = ? or accepted = ?", true, null])
and
#scholars = Scholars.where(:scholar_id => current_user.id).where(:accpeted => true).where(:accepted=> null)
Try:
#scholars = Scholars.where(:scholar => current_user, :accepted => true).all +
Scholar.where(:scholar => current_user, :accepted => nil).all
Did you name your model "Scholars"? Models are traditionally singular... if you named it properly, this should be Scholar.where(...).
The correct answer should be
#profiles = Profile.where(:user_id => current_user.id, :accepted => [true, nil]).order(:accepted)

Ruby on Rails - search in database based on a query

I have a simple form, where I set up a query that I want to browse, for example panasonic viera.
This is on how I search the term in database:
Product.where("name ilike ?", "%#{params[:q]}%").order('price')
The query looks like %panasonic viera%, but I would need to search the query this way: %panasonic%viera% - I need to find all products, where is in the title the word panasonic or viera... but how to make this query?
One solution would be to break up your query into individual terms and build a set of database queries connected by OR.
terms = params[:q].split
query = terms.map { |term| "name like '%#{term}%'" }.join(" OR ")
Product.where(query).order('price')
If you're using PostgreSQL, you can use pg_search gem. It's support full text search, with option any_word:
Setting this attribute to true will perform a search which will return all models containing any word in the search terms.
Example from pg_search:
class Number < ActiveRecord::Base
include PgSearch
pg_search_scope :search_any_word,
:against => :text,
:using => {
:tsearch => {:any_word => true}
}
pg_search_scope :search_all_words,
:against => :text
end
one = Number.create! :text => 'one'
two = Number.create! :text => 'two'
three = Number.create! :text => 'three'
Number.search_any_word('one two three') # => [one, two, three]
Number.search_all_words('one two three') # => []
How about via ARel
def self.search(query)
words = query.split(/\s+/)
table = self.arel_table
predicates = []
words.each do |word|
predicates << table[:name].matches("%#{word}%")
end
if predicates.size > 1
first = predicates.shift
conditions = Arel::Nodes::Grouping.new(predicates.inject(first) {|memo, expr| Arel::Nodes::Or.new(memo, expr)})
else
conditions = predicates.first
end
where(conditions).to_a
end
This isn't working?
WHERE name LIKE "panasonic" OR name LIKE "viera"

One efficient SQL call instead of 500

In my view, I've got a fiddly loop which creates 500 SQL queries (to get the info for 500 books). How can I avoid lots of SQL queries by loading a variable up in the controller?
My current (pseudo) code:
controller index action:
#books = Book.scoped.where(:client_id => #client.id).text_search(params[:query])
#feature_root = Book.multiple_summary_details_by_category( #books )
#...returns a hash of books
#features = #feature_root.to_a.paginate(:page => params[:page], :per_page => 4)
index.html.haml
= render :partial => "feature", :locals => { :features => #features }
_features.html.haml
- features.each_with_index do |(cat_name, array_of_books), i|
%h2
= cat_name
- array_of_books[0..10].each do |feature|
= link_to image_tag(feature[:cover], :class => "product_image_tiny"), book_path(feature[:book])
# more code
- array_of_books.sort_by{ |k, v| k["Author"] }.each do |feature|
- feature.each do |heading,value|
%span.summary_title
= heading + ':'
%span.summary_value
= value
What have you tried so far? It should be quite easy with standard ActiveRecord queries as documented in http://guides.rubyonrails.org/active_record_querying.html.
Also, instead of
array_of_books.sort_by{ |k, v| k["Author"] }
try something like
Book.order("author DESC")
(not sure about your exact model here) to let the db do the sorting rather than putting them in an array and let ruby handle it.

Rails assigning names to variables

I'm building a user ranking system, and am trying to assign user.rank values with a name.
I wanted to define something like this in my User model and then be able to reference it when displaying each user's rank, but this probably isn't the best way:
class User < ActiveRecord::Base
RANK_NAMES = {
'Peasant' => (0..75),
'Craftsman' => (76..250),
'Vassal' => (251..750),
'Noble' => (750..1500),
'Monarch' => (1501..999999)
}
Perhaps it would be better to define a method in a controller or helper like:
if user.rank == 0..75
rank_name = "Peasant"
elsif...
But not sure how to do that. Anyone have any thoughts? I'm not even sure what to call what it is I'm trying to do, thus making it difficult to research on my own.
It could be something even as simple as this, assuming user.rank exists.
class User < ActiveRecord::Base
...
def rank_name
case self.rank
when 0..75
'Peasant'
when 76..250
'Craftsman'
when 251..750
'Vassal'
when 750..1500
'Noble'
when 1501..999999
'Monarch'
end
end
...
end
If rank_name is specific to the User, I'd make it a method of User.
You could try something like below. It might give you some ideas.
class User
RANKS = [
{:name => 'Peasant', :min => 0, :max => 75},
{:name => 'Craftsman', :min => 76, :max => 250}
# ...
]
attr_accessor :rank
def rank_name
# TODO what happens if rank is out of range of all ranks or rank is nil
# or not an integer
User::RANKS[rank_index][:name]
end
private
def rank_index
User::RANKS.index { |r| (r[:min]..r[:max]).include? #rank }
end
end
user = User.new
user.rank = 76
puts user.rank_name # -> Craftsman

conditional update_all with join tables in ActiveRecord?

The following query returns the collection of AR objects that I want to update:
Variant.all(:joins => { :candy_product => :candy }, :conditions => "candies.name = 'Skittles'")
I'm trying to do something like the following:
Variant.update_all(:price => 5, :joins => { :candy_product => :candy }, :conditions => "candies.name = 'Skittles'")
This should only update the price for the variants returned from original query. Is this possible with AR or will I have to write the SQL? This is a pretty large collection, so anything that iterates is out.
Using Rails 2.3.4.
As #François Beausoleil pointed correctly we should use scoped
Variant.scoped(:joins => { :candy_product => :candy }, :conditions => "candies.name = 'Skittles'").update_all(:price => 5)