Better way to build multiple conditions in Rails 2.3 - ruby-on-rails

I'm developing with Rails 2.3.8 and looking for a better way to build find conditions.
On search page, like user search, which user sets search conditions, find conditions are depends on the condition which user have chosen, e.g age, country, zip-code.
I've wrote code below to set multiple find conditions.
# Add condition if params post.
conditions_array = []
conditions_array << ['age > ?', params[:age_over]] if params[:age_over].present?
conditions_array << ['country = ?', params[:country]] if params[:country].present?
conditions_array << ['zip_code = ?', params[:zip_code]] if params[:zip_code].present?
# Build condition
i = 0
conditions = Array.new
columns = ''
conditions_array.each do |key, val|
key = " AND #{key}" if i > 0
columns += key
item_master_conditions[i] = val
i += 1
end
conditions.unshift(columns)
# condiitons => ['age > ? AND country = ? AND zip_code = ?', params[:age], params[country], prams[:zip_code]]
#users = User.find(:all,
:conditions => conditions
)
This code works fine but it is ugly and not smart.
Is there better way to build find conditions?

Named scopes could make it a bit more readable, albeit bulkier, while still preventing SQL injection.
named_scope :age_over, lambda { |age|
if !age.blank?
{ :conditions => ['age > ?', age] }
else
{}
end
}
named_scope :country, lambda { |country|
if !country.blank?
{ :conditions => ['country = ?', age] }
else
{}
end
}
named_scope :zip_code, lambda { |zip_code|
if !zip_code.blank?
{ :conditions => ['zip_code = ?', age] }
else
{}
end
}
And then when you do your search, you can simply chain them together:
#user = User.age_over(params[:age_over]).country(params[:country]).zip_code(params[:zip_code])

I have accidentally run on your questions, and even it is old one, here is the answer:
After defining your conditions, you could use it like this:
# Add condition if params post.
conditions_array = []
conditions_array << ["age > #{params[:age_over]}"] if params[:age_over].present?
conditions_array << ["country = #{params[:country]}"] if params[:country].present?
conditions_array << ["zip_code = #{params[:zip_code]}"] if params[:zip_code].present?
conditions = conditions_array.join(" AND ")
#users = User.find(:all, :conditions => conditions) #Rails 2.3.8
#users = User.where(conditions) #Rails 3+

Related

Need help refactoring to use .where clause - ruby on rails

I have the following code which is legacy code and I need to refactor it to .where clauses but I'm having issues in refactoring it and the best way to do it.
Here is the code
# legacy
#debit_transactions = FinancialTransaction.legacy_find(
:all,
:include => [:project, :department, :stock, :debit_account, :credit_account, :transaction_type],
:conditions => ["dr_account_id = ?", #customer.id],
:order => 'financial_transactions.id desc',
:limit => 20)
# refactor attempt
#debit_transactions = FinancialTransaction.where(dr_account_id: #dr_account_id, customer: #customer.id).order('financial_transactions.id desc').limit(20)
# legacy
contact_or = ''
contact_or = ' OR contact_id IN(?) ' if #customer.contacts.present?
#customer_complaints = Event.legacy_find(:all, { :order => 'id desc', :limit => 10, :conditions => ["complaint is true and (customer_account_id = ? #{contact_or} )", #customer.id].push_if(#customer.contacts.to_a.map(&:id), #customer.contacts.present?) })
# refactor attempt
#customer_complaints = Event.where(complaints: true, customer_account_id: [#customer_account_id], customer: #customer.id).order('id desc').limit(10)
Any help would be appreciated
edit some methods that might be help in understanding the legacy_find
def self.legacy_find(type, args=nil)
# need to add capability to handle array of id's
if type.kind_in?([Numeric, String, Array]) && !args
result = self.find(type)
elsif !args
result = self.send(type.to_s)
else
result = self
if type.kind_of?(Array)
if !args[:conditions]
args[:conditions] = ["1 = 1"]
elsif args[:conditions].kind_of?(String)
args[:conditions] = [args[:conditions]]
end
args[:conditions][0] = '(' + args[:conditions][0] + ')'
args[:conditions][0] += " AND `" + self.table_name + "`.`id` in(" + type.join(',') + ")"
elsif type && !type.kind_of?(Symbol)
if !args[:conditions]
args[:conditions] = ["1 = 1"]
elsif args[:conditions].kind_of?(String)
args[:conditions] = [args[:conditions]]
end
args[:conditions][0] = '(' + args[:conditions][0] + ')'
args[:conditions][0] += " AND `" + self.table_name + "`.`id` = ?"
args[:conditions].push(type)
end
result = self.legacy_conditions(args)
if type && ((type.kind_of?(String) && type.to_i.to_s == type) || type.kind_of?(Numeric))
result = result.first
elsif type && !type.kind_of?(Symbol)
result = result.to_a
else
result = type ? result.send((type == :all ? 'to_a' : type).to_s) : result
end
end
new_result = result
return new_result
end
def self.legacy_count(args=nil)
new_result = self.legacy_conditions(args).count
end
def self.legacy_sum(col, args=nil)
new_result = self.legacy_conditions(args).sum(col.to_s)
end
def self.legacy_conditions(args)
return self if !args
args[:conditions] = [] if args[:conditions] && args[:conditions][0].kind_of?(String) && args[:conditions][0].size == 0
result = self
result = result.where(args[:conditions]) if (args.has_key?(:conditions) && args[:conditions] && args[:conditions].size > 0)
result = result.select(args[:select]) if args.has_key?(:select) && args[:select]
result = result.includes(args[:include]) if args.has_key?(:include) && args[:include]
result = result.includes(args[:include_without_references]) if args.has_key?(:include_without_references) && args[:include_without_references]
result = result.references(args[:include]) if args.has_key?(:include) && args[:include]
result = result.joins(args[:joins]) if args.has_key?(:joins) && args[:joins]
result = result.order(args[:order]) if args.has_key?(:order) && args[:order]
result = result.group(args[:group]) if args.has_key?(:group) && args[:group]
result = result.limit(args[:limit]) if args.has_key?(:limit) && args[:limit]
result = result.offset(args[:offset]) if args.has_key?(:offset) && args[:offset]
result = result.from(args[:from]) if args.has_key?(:from) && args[:from]
result = result.lock(args[:lock]) if args.has_key?(:lock) && args[:lock]
result = result.readonly(args[:readonly]) if args.has_key?(:readonly) && args[:readonly]
result
end
Honestly why this code exists is beyond me so I'm trying to phase it out.
edit 2
Based on the answers below I've come up with the following
#debit_transactions = FinancialTransaction
.includes(:project, :department, :stock, :debit_account, :credit_account, :transaction_type)
.where(dr_account_id: #dr_account_id)
.order(id: :desc)
.limit(20)
#credit_transactions = FinancialTransaction
.includes(:project, :department, :stock, :debit_account, :credit_account, :transaction_type)
.where(cr_account_id: #cr_account_id)
.order(id: :desc)
.limit(20)
contact_or = ''
contact_or = ' OR contact_id IN(?) ' if #customer.contacts.size > 0
#customer_complaints = Event.where(customer_account_id: #customer_id, complaint: true).order(id: :desc).limit(10).or(Event.where(contact_id: #customer.contacts)) if #customer.contacts.present?
#customer_leads = Event.where(customer_account_id: #customer_id, lead: true).order(id: :desc).limit(10)
#customer_quotes = SalesQuote.where(customer_account_id: #customer_id).or(SalesQuote.where(contact_id: #contact_id)).order(id: :desc).limit(10)
#customer_orders = SalesOrder.where(customer_account_id: #customer_id).order(id: :desc).limit(10)
#customer_invoices = Invoice.where(customer_account_id: #customer_id).order(id: :desc).limit(10)
#customer_credits = CreditNote.where(customer_account_id: #customer_id).order(id: :desc).limit(10)
#customer_opportunities = Opportunity.where(customer_account_id: #customer_id).or(Opportunity.where(contact_id: #contact_id)).order(id: :desc).limit(10)
#customer_estimates = Estimate.where(customer_account_id: #customer_id).or(Estimate.where(contact_id: #contact_id)).order(id: :desc).limit(10)
#customer_support_tickets = SupportTicket.where(customer_account_id: #customer_id).order(id: :desc).limit(10)
#financial_matching_sets = FinancialMatchingSet.where(customer_account_id: #customer_id).order(id: :desc).limit(10)
However, I'm getting the following
Mysql2::Error: Unknown column 'sales_orders.customer_account_id' in 'where clause': SELECT `sales_orders`.* FROM `sales_orders` WHERE `sales_orders`.`customer_account_id` IS NULL ORDER BY `sales_orders`.`id` DESC LIMIT 10
the best way to do it.
I don't have a concrete answer to this, but there's a few things you could try, such as:
Write test cases for the behaviour of the old implementation, thus ensuring that they still behave the same with the new implementation. (Maybe you already have some such tests in place??!!)
Write test cases for the implementation of the old vs new code, by checking query.to_sql remains unchanged?!
Try running both versions on production, in parallel, assuming you have good error logging. For example, could you gradually switch over 10% of users to use the "new" implementations, thus catching any errors without causing mass failures for everyone?
But anyway... Aside from the pain of actually rewriting all of these in a safe/robust/well-tested way:
First query:
#debit_transactions = FinancialTransaction.legacy_find(
:all,
:include => [:project, :department, :stock, :debit_account, :credit_account, :transaction_type],
:conditions => ["dr_account_id = ?", #customer.id],
:order => 'financial_transactions.id desc',
:limit => 20)
# refactor attempt
#debit_transactions = FinancialTransaction
.where(dr_account_id: #dr_account_id, customer: #customer.id)
.order('financial_transactions.id desc')
.limit(20)
This refactor ignores the include parameters. The legacy method says:
# ...
result = result.includes(args[:include]) if args.has_key?(:include) && args[:include]
result = result.references(args[:include]) if args.has_key?(:include) && args[:include]
# ...
So, your version should have been:
#debit_transactions = FinancialTransaction
.includes([:project, :department, :stock, :debit_account, :credit_account, :transaction_type])
.references([:project, :department, :stock, :debit_account, :credit_account, :transaction_type])
.where(dr_account_id: #dr_account_id, customer: #customer.id)
.order('financial_transactions.id desc')
.limit(20)
Second query:
# legacy
contact_or = ''
contact_or = ' OR contact_id IN(?) ' if #customer.contacts.present?
#customer_complaints = Event.legacy_find(
:all,
:order => 'id desc',
:limit => 10,
:conditions => ["complaint is true and (customer_account_id = ? #{contact_or} )", #customer.id].push_if(#customer.contacts.to_a.map(&:id), #customer.contacts.present?)
)
# refactor attempt
#customer_complaints = Event
.where(complaints: true, customer_account_id: [#customer_account_id], customer: #customer.id)
.order('id desc')
.limit(10)
Your refactor ignores the OR clause in the condition; you've written this as 3 AND clauses instead.
I think this can be written as something like:
Event.where(customer_account_id: [#customer_account_id])
.or(Event.where(customer: #customer.id))
.merge(Event.where(complaints: true))
.order('id desc')
.limit(10)
...Or something like that. Check the generated SQL in both cases.
The first query is relatively straight forward:
#debit_transactions = FinancialTransaction
# you missed the includes
.includes(
:project, :department, :stock, :debit_account,
:credit_account, :transaction_type
)
.where(
dr_account_id: #dr_account_id.id,
)
.order(id: :desc)
.limit(20)
Then second query is a bit tougher.
You can create a WHERE x IN (...) clause simply by passing an array:
#debit_transactions = FinancialTransaction.where(
id: [1,2,3]
)
You can also create a WHERE x IN (subquery) by passing a ActiveRecord::Relation:
Event.where(contact_id: #customer.contacts)
This is far more effective them using .map(:id) or .ids as you remove a full round trip to the DB.
Support for OR was added in Rails 5:
scope = Event.where(customer_account_id: #customer_id)
scope = scope.or(Event.where(contact_id: #customer.contacts)) if #customer.contacts.present?
So altogether it would look something like:
scope = Event.where(
customer_account_id: #customer_id
complaint: true
).order('id desc')
.limit(10)
scope = scope.or(Event.where(contact_id: #customer.contacts)) if #customer.contacts.present?

rails 3 sqlite pass array in query via placeholder

How can i give an array as a placeholder without sqlite seeing as 1 value but several values that are in the array
value = Array.new
value.push(broadcast_date_from)
value.push(broadcast_date_to)
puts value #["a", "2006-01-02 00:00", "2006-01-02 23:59"]
find(:all, :order => 'broadcast_date', :conditions => ['name LIKE ? and broadcast_date >= ? and broadcast_date <= ?', name, #value ])
But i get this error:
wrong number of bind variables (1 for 3) in: name LIKE ? and broadcast_date >= ? and broadcast_date <= ?
Is there anyway to make it see 3 values in the array and not 1.
You need to add the splat operator * before you call your array:
values = ['condition for name']
values.push(broadcast_date_from)
values.push(broadcast_date_to)
find(:all, :order => 'broadcast_date', :conditions => ['name LIKE ? and broadcast_date >= ? and broadcast_date <= ?', *values ])
Small article about the splat operator: http://theplana.wordpress.com/2007/03/03/ruby-idioms-the-splat-operator/
Improvement for you: use .where() instead of .find()
First, the excellent guide about it: http://guides.rubyonrails.org/active_record_querying.html#conditions
Then, a little example to show the benefits of the where:
class User < ActiveRecord::Base
def get_posts(options = {})
str_conditions = ['user_id = ?']
args_conditions = [self.id]
if options.has_key?(:active)
str_conditions << 'active = ?'
args_conditions << options[:active]
end
if options.has_key?(:after)
str_conditions << 'created_at >= ?'
args_conditions << options[:after]
end
if options.has_key?(:limit)
Post.find(:conditions => [str_conditions.join(' OR '), *args_conditions], :limit => options[:limit])
else
Post.find(:conditions => [str_conditions.join(' OR '), *args_conditions])
end
end
Different usages:
user = User.first
user.get_posts(:active => true, :after => Date.today, :limit => 10)
user.get_posts
The same method, but using the where method (very nice for chain-scoping):
def get_posts(options = {})
scope = self.posts
scope = scope.where(active: options[:active]) if options.has_key?(:active)
scope = scope.where('created_at >= ?', options[:after]) if options.has_key?(:after)
scope = scope.limit(options[:limit]) if options.has_key?(:limit)
return scope
end
Keep in mind that you can chain scope with the .where method:
User.where(active: true).where('created_at < ?', Date.today-1.weeks).includes(:posts).where(posts: { name: "Name of a specific post" })

Rails: multiple params (filter) with has_scope

I'm using has_scope gem and I want to create filtering with two params — it may be one param or two same time.
Mymodel (Product):
scope :brand, proc { |brand| joins(:product_values).where('product_values.value_id' => brand) }
scope :zamena, proc { |zamena| joins(:product_values).where('product_values.value_id' => zamena) }
Index action of controller:
#products = apply_scopes(Product).all
It works, but only by one :(
/products?brand=12 - Ok
/products?zamena=24 - Ok
/products?brand=12&zamena=24 - Fail (sorted only by 'zamena', not by both params)
2nd. variant (not works too)
In my controller:
query = Product.scoped
query = query.brand(params[:brand]) if params[:brand]
query = query.zamena(params[:zamena]) if params[:zamena]
#products = query.all
Works by one, but not both (0 results).
My answer. Maybe not elegant, but works nice.
fcount = 0
fcount += 1 if params[:brand]
fcount += 1 if params[:zamena]
prods = []
if params[:brand]
Product.all.each do |p|
prods << p if p.product_values.where(value_id: params[:brand]).count > 0
end
end
if params[:zamena]
Product.all.each do |p|
prods << p if p.product_values.where(value_id: params[:zamena]).count > 0
end
end
#products = prods.select{|item| prods.count(item) == fcount}.uniq
No scopes needed. You can use a lot of filters using this way.

Best way to refactor this without making as many calls as I am?

I am trying to cycle through a few of these blocks. They basically narrow down a number of people that fulfill a bunch of attributes.
I apologize if this seems really messy, but my database is really taking a toll processing this, and I know there's a better way. I'm just lost on strategy right now.
My Code:
def count_of_distribution
#beginning with an array..
array_of_users = []
# any matching zip codes? ..
# zip_codes
#zip_codes = self.distributions.map(&:zip_code).compact
unless #zip_codes.nil? || #zip_codes.empty?
#matched_zips = CardSignup.all.map(&:zip_code) & #zip_codes
#matched_zips.each do |mz|
CardSignup.find(:all, :conditions => ["zip_code = ?", mz]).each do |cs|
array_of_users << cs.id
end
end
end
# any matching interests?..
# interest
#topics = self.distributions.map(&:me_topic).compact
unless #topics.nil? || #topics.empty?
#matched_topics = MeTopic.all.map(&:name) & #topics
#matched_topics.each do |mt|
MeTopic.find(:all, :conditions => ["name = ?", mt]).each do |mt2|
mt2.users.each do |u|
array_of_users << u.card_signup.id if u.card_signup
end
end
end
end
# any matching sexes?..
# sex
#sexes = self.distributions.map(&:sex).compact
unless #sexes.nil? || #sexes.empty?
#matched_sexes = CardSignup.all.map(&:sex) & #sexes
#matched_sexes.each do |ms|
CardSignup.find(:all, :conditions => ["sex = ?", ms]).each do |cs|
array_of_users << cs.id
end
end
end
total_number = array_of_users.compact.uniq
return total_number
end
This is the most embarressing results ever :
Completed in 51801ms (View: 43903, DB: 7623) | 200 OK [http://localhost/admin/emails/3/distributions/new]
UPDATED ANSWER It is truncated but still takes a huge toll on the DB
array_of_users = []
#zip_codes = self.distributions.map(&:zip_code).compact
#sexes = self.distributions.map(&:sex).compact
#zips_and_sexes = CardSignup.find(:all, :conditions => ["gender IN (?) OR zip_code IN (?)", my_sexes, my_zips])
#zips_and_sexes.each{|cs| array_of_users << cs.id }
#topics = self.distributions.map(&:me_topic).compact
#all_topics = MeTopic.find(:all, :conditions => ["name IN (?)", #topics])
array_of_users << CardSignup.find(:all, :conditions => ["user_id IN (?)", #all_topics.map(&:users)]).map(&:id)
You are trying to let rails do all the computation through series of loops; no wonder it's taking so long.
It's hard to follow, but perhaps instead of using .each loops, try to pull out everything you are after right away, and then use a .group_by(&:attribute)
OR if your end result is just card signups.
It seems you are trying to get all the users that have something in desired, a zip, a topic, or sex. So, let the database do the work.
my_zips = #zip_codes = self.distributions.map(&:zip_code).compact.join(", ")
my_sexes = #sexes = self.distributions.map(&:sex).compact.join(", ")
all_cards = CardSignup.find(:all, :conditions => ["sex IN (?) OR zip_code IN (?)", my_sexes, my_zips])
my_topics = #topics = self.distributions.map(&:me_topic).compact.join(", ")
all_topics = MeTopic.find(:all, :conditions => ["name = ?", my_topics])
more_cards = all_topics.map{|x| x.users}.map{|n| n.card_signup}
total_number = (all_cards + more_cards).flatten.uniq
I hope this is a better answer.
Here it is. It runs super fast now :
array_of_users = []
# zips and sexes
#zip_codes = self.distributions.map(&:zip_code).compact
#sexes = self.distributions.map(&:sex).compact
#zips_and_sexes = CardSignup.find(:all, :conditions => ["gender IN (?) OR zip_code IN (?)", #sexes, #zip_codes])
#zips_and_sexes.each{|cs| array_of_users << cs.id }
# interest
#topics = self.distributions.map(&:me_topic).compact
#selected_topics = MeTopic.find(:all, :conditions => ["name in (?)", #topics]).map(&:id)
#matched_users = ActiveRecord::Base.connection.execute("SELECT * FROM `me_topics_users` WHERE (me_topic_id IN ('#{#selected_topics.join("', '")}') )")
#list_of_user_ids = []
#matched_users.each{|a| #list_of_user_ids << a[0] }
#list_of_user_ids.uniq!
array_of_users << CardSignup.find(:all, :conditions => ["user_id IN (?)", #list_of_user_ids]).map(&:id)
# age
#ages = self.distributions.map(&:age).compact
#ages_array = []
#ages.each{|a| #ages_array << how_old(a) }
#ages_array.each{|aa| array_of_users << aa.id}
array_of_users << CardSignup.all.map(&:id) if array_of_users.flatten.empty?
total_number = array_of_users.flatten.uniq
return total_number

Refactor: Multiple params in find query

Need some help refactoring this if/else block that builds the conditions for a find query.
if params[:status] && params[:carrier]
conditions = ["actual_delivery IS NOT NULL AND actual_delivery > scheduled_delivery AND status_id = ? AND carrier_id = ?", status.id, carrier.id]
elsif params[:status]
conditions = ["actual_delivery IS NOT NULL AND actual_delivery > scheduled_delivery AND status_id = ?", status.id]
elsif params[:carrier]
conditions = ["actual_delivery IS NOT NULL AND actual_delivery > scheduled_delivery AND carrier_id = ?", carrier.id]
else
conditions = ["actual_delivery IS NOT NULL AND actual_delivery > scheduled_delivery"]
end
#packages = Package.find(:all, :conditions => conditions)
I recommend creating a scope in your model, to take care of the first part of your query that is always the same in this action:
class Package < ActiveRecord::Base
named_scope :late_deliveries, :conditions => "actual_delivery IS NOT NULL AND actual_delivery > scheduled_delivery"
end
Now you can refactor your action like this:
def index
conditions = {}
[:status, :carrer].each{|param| conditions[param] = params[param] if params[param]}
#packages = Package.late_deliveries.find(:conditions => conditions)
end
If :carrier and :status are the only two parameters to this action, then it's even simpler:
def index
#packages = Package.late_deliveries.find(:conditions => params)
end
I hope this helps!
You could do:
conditions = "actual_delivery IS NOT NULL AND actual_delivery > scheduled_delivery"
conditions += " AND status_id = #{status.id}" if params[:status]
conditions += " AND carrier_id = #{carrier.id}" if params[:carrier]
#packages = Package.all(:conditions => [conditions])

Resources