Refactor: Multiple params in find query - ruby-on-rails

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])

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?

How to construct where clause in ruby using if

I am finding something like below. Constructing a where clause using condition. Is it possible in ruby? or I need to separate it into two where clause?
Post
.where(tag: "A") if condition A
.where(tag: "B") if condition B
.where(user_id: 1)
.order(....)
Actually, my case is like this. Is there any way to handle?
def this_function
#questions = Question.joins(:comment_threads)
.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
.where(index_where_clause)
.where("questions.created_at < ?", query_from_date_time)
.order(created_at: :desc).limit(5)
end
def index_where_clause
where_clause = {}
where_clause[:user_detail_id] = current_user_detail.id if params[:type] == "my_question"
where_clause[:comments] = {user_detail_id: current_user_detail.id} if params[:type] == "my_answer"
where_clause[:wine_question_score_id] = params[:wine_question_score_id] if params[:wine_question_score_id].present?
where_clause
end
The methods you're using return relations so you can say things like this:
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = #questions.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
#questions = #questions.where(:user_detail_id => current_user_detail.id) if params[:type] == "my_question"
#questions = #questions.where(:comments => { user_detail_id: current_user_detail.id}) if params[:type] == "my_answer"
#questions = #questions.where(:wine_question_score_id => params[:wine_question_score_id]) if params[:wine_question_score_id].present?
#questions = #questions.order(created_at: :desc).limit(5)
and build the query piece by piece depending on what you have in params.
I'd probably break it down a little more:
def whatever
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = with_tag(#questions, tag_variable)
#...
#questions = #questions.order(created_at: :desc).limit(5)
end
private
def with_tag(q, tag)
if tag.present?
q.tagged_with(tag, wild: true, any: true)
else
q
end
end
#...
and bury all the noisy bits in little methods to make things cleaner and easier to read. If you're doing this more than once then you could use scopes to hide the noise in the model class and re-use it as needed.
#tap can be helpful for modifying an object in place to apply conditional logic, in this case the object would be your .where conditions:
Post
.where(
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
)
.order(...)
Or, perhaps it's a little cleaner if you create a helper method:
def specific_conditions
{ user_id: 1 }.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
end
Post.where(specific_conditions).order(...)
But as a side note, if there's a case where condition A and condition B can both be true, the second conditions[:tag] = ... line will override the first. If there is not a case where both can be true, you might try to use some kind of collection to look up the proper value for tag.
CONDITION_TAGS = {
a: 'A'.freeze,
b: 'B'.freeze,
}.freeze
def specific_conditions
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = CONDITION_TAGS[condition_value] if condition_value
end
end
Post.where(specific_conditions).order(...)
#in Question class
scope :with_user_detail, -> (user_detail_id, flag=true) do
where("user_detail_id = ?", user_detail_id) if flag
end
scope :with_user_detail_comments, -> (user_detail_id, flag=true) do
joins(:comment_threads).where("comments.user_detail_id = ?", user_detail_id) if flag
end
scope :with_wine_question_score, -> (wine_question_score_id) do
where("wine_question_score_id = ?", wine_question_score_id) if wine_question_score_id.present?
end
scope :tagged_with_condition, -> (tag_variable, wild, any) do
tagged_with(tag_variable, wild, any) if tag_variable.present?
end
def this_function
my_question_flag = params[:type] == "my_question"
my_answer_flag = params[:type] == "my_answer"
Question.with_user_detail(current_user_detail.id, my_question_flag)
.tagged_with_condition(tag_variable, wild: true, any: true)
.with_user_detail_comments(current_user_detail.id, my_answer_flag)
.with_wine_question_score(params[:wine_question_score_id])
.order(created_at: :desc).limit(5)
end
You can do the following:
condition = {:tag => "A"} if condition A
condition = {:tag => "B"} if condition B
Post
.where(condition)
.where(:user_id => 1)
.order(....)
you have to use scope :
scope :my_scope, -> (variable) { where(some: vatiable) if my_condition }

Rails 4 ActiveRecord append conditions (multiple .where)

I would like to append AND conditions depend on condition like this:
#flag = true || false;
#results = Model.where(conditions).where(conditions_depend_on_flag);
// The simple way:
if (#flag) {
#results = Model.where(conditions);
} else {
#results = Model.where(conditions).where(conditions_depend_on_flag);
}
Example for my expected:
#results = Model.where(conditions).where(conditions_depend_on_flag, #flag == true);
I don't know is it possible or not.
Could you give me some suggestion?
#results = Model.where(conditions)
#results = #results.where(conditions_depend_on_flag) if #flag
We can combine 2 conditions into 1 where statement:
To make it easy I assume:
conditions = created_at < 1.day.ago
conditions_depend_on_flag = updated_at > 1.day.ago
So the query will be:
Model.where(
'created_at < ? AND (? OR updated_at > ?)', 1.day.ago, !#flag, 1.day.ago
)
Beautiful SQL :)
use scopes for sql conditions
in model
scope :conditions_depend_on_flag, ->(flag) { where(....) if flag }
anywhere
#results = Model.where(conditions).conditions_depend_on_flag(#flag)

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" })

Better way to build multiple conditions in Rails 2.3

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+

Resources