Need help refactoring to use .where clause - ruby on rails - 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?

Related

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+

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

How to properly handle changed attributes in a Rails before_save hook?

I have a model that looks like this:
class StopWord < ActiveRecord::Base
UPDATE_KEYWORDS_BATCH_SIZE = 1000
before_save :update_keywords
def update_keywords
offset = 0
max_id = ((max_kw = Keyword.first(:order => 'id DESC')) and max_kw.id) || 0
while offset <= max_id
begin
conditions = ['id >= ? AND id < ? AND language = ? AND keyword RLIKE ?',
offset, offset + UPDATE_KEYWORDS_BATCH_SIZE, language]
# Clear keywords that matched the old stop word
if #changed_attributes and (old_stop_word = #changed_attributes['stop_word']) and not #new_record
Keyword.update_all 'stopword = 0', conditions + [old_stop_word]
end
Keyword.update_all 'stopword = 1', conditions + [stop_word]
rescue Exception => e
logger.error "Skipping batch of #{UPDATE_KEYWORDS_BATCH_SIZE} keywords at offset #{offset}"
logger.error "#{e.message}: #{e.backtrace.join "\n "}"
ensure
offset += UPDATE_KEYWORDS_BATCH_SIZE
end
end
end
end
This works just fine, as the unit tests show:
class KeywordStopWordTest < ActiveSupport::TestCase
def test_stop_word_applied_on_create
kw = Factory.create :keyword, :keyword => 'foo bar baz', :language => 'en'
assert !kw.stopword, 'keyword is not a stop word by default'
sw = Factory.create :stop_word, :stop_word => kw.keyword.split(' ')[1], :language => kw.language
kw.reload
assert kw.stopword, 'keyword is a stop word'
end
def test_stop_word_applied_on_save
kw = Factory.create :keyword, :keyword => 'foo bar baz', :language => 'en', :stopword => true
sw = Factory.create :keyword_stop_word, :stop_word => kw.keyword.split(' ')[1], :language => kw.language
sw.stop_word = 'blah'
sw.save
kw.reload
assert !kw.stopword, 'keyword is not a stop word'
end
end
But mucking with the #changed_attributes instance variable just feels wrong. Is there a standard Rails-y way to get the old value of an attribute that is being modified on a save?
Update: Thanks to Douglas F Shearer and Simone Carletti (who apparently prefers Murphy's to Guinness), I have a cleaner solution:
def update_keywords
offset = 0
max_id = ((max_kw = Keyword.first(:order => 'id DESC')) and max_kw.id) || 0
while offset <= max_id
begin
conditions = ['id >= ? AND id < ? AND language = ? AND keyword RLIKE ?',
offset, offset + UPDATE_KEYWORDS_BATCH_SIZE, language]
# Clear keywords that matched the old stop word
if stop_word_changed? and not #new_record
Keyword.update_all 'stopword = 0', conditions + [stop_word_was]
end
Keyword.update_all 'stopword = 1', conditions + [stop_word]
rescue StandardError => e
logger.error "Skipping batch of #{UPDATE_KEYWORDS_BATCH_SIZE} keywords at offset #{offset}"
logger.error "#{e.message}: #{e.backtrace.join "\n "}"
ensure
offset += UPDATE_KEYWORDS_BATCH_SIZE
end
end
end
Thanks, guys!
You want ActiveModel::Dirty.
Examples:
person = Person.find_by_name('Uncle Bob')
person.changed? # => false
person.name = 'Bob'
person.changed? # => true
person.name_changed? # => true
person.name_was # => 'Uncle Bob'
person.name_change # => ['Uncle Bob', 'Bob']
Full documentation: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
You're using the right feature but the wrong API.
You should #changes and #changed?.
See this article and the official API.
Two additional notes about your code:
Never rescue Exception directly when you actually want to rescue execution errors. This is Java-style. You should rescue StandardError instead because lower errors are normally compilation error or system error.
You don't need the begin block in this case.
def update_keywords
...
rescue => e
...
ensure
...
end

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