Related
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?
In Ruby 2.1.5 and 2.2.4, creating a new Collector returns the correct result.
require 'ostruct'
module ResourceResponses
class Collector < OpenStruct
def initialize
super
#table = Hash.new {|h,k| h[k] = Response.new }
end
end
class Response
attr_reader :publish_formats, :publish_block, :blocks, :block_order
def initialize
#publish_formats = []
#blocks = {}
#block_order = []
end
end
end
> Collector.new
=> #<ResourceResponses::Collector>
Collector.new.responses
=> #<ResourceResponses::Response:0x007fb3f409ae98 #block_order=[], #blocks= {}, #publish_formats=[]>
When I upgrade to Ruby 2.3.1, it starts returning back nil instead.
> Collector.new
=> #<ResourceResponses::Collector>
> Collector.new.responses
=> nil
I've done a lot of reading around how OpenStruct is now 10x faster in 2.3 but I'm not seeing what change was made that would break the relationship between Collector and Response. Any help is very appreciated. Rails is at version 4.2.7.1.
Let's have a look at the implementation of method_missing in the current implementation:
def method_missing(mid, *args) # :nodoc:
len = args.length
if mname = mid[/.*(?==\z)/m]
if len != 1
raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
end
modifiable?[new_ostruct_member!(mname)] = args[0]
elsif len == 0
if #table.key?(mid)
new_ostruct_member!(mid) unless frozen?
#table[mid]
end
else
err = NoMethodError.new "undefined method `#{mid}' for #{self}", mid, args
err.set_backtrace caller(1)
raise err
end
end
The interesting part is the block in the middle that runs when the method name didn't end with an = and when there are no addition arguments:
if #table.key?(mid)
new_ostruct_member!(mid) unless frozen?
#table[mid]
end
As you can see the implementation first checks if the key exists, before actually reading the value.
This breaks your implementation with the hash that returns a new Response.new when a key/value is not set. Because just calling key? doesn't trigger the setting of the default value:
hash = Hash.new { |h,k| h[k] = :bar }
hash.has_key?(:foo)
#=> false
hash
#=> {}
hash[:foo]
#=> :bar
hash
#=> { :foo => :bar }
Ruby 2.2 didn't have this optimization. It just returned #table[mid] without checking #table.key? first.
I have a web-service that allows clients to search for articles with query parameters.It works fine if only one parameter is included but fails if I combine search_query and category. This is based on Comfortable_Mexican_Sofa where for_category is found. Even if I remove the order statement i get this error.
error
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY
expressions must appear in select list LINE 1:
...ms_categories"."label" = 'Company News' ORDER BY pg_search_...
^ : SELECT DISTINCT "comfy_cms_pages".* FROM "comfy_cms_pages" INNER JOIN
"comfy_cms_categorizations" ON
"comfy_cms_categorizations"."categorized_id" = "comfy_cms_pages"."id"
AND "comfy_cms_categorizations"."categorized_type" = $1 INNER JOIN
"comfy_cms_categories" ON "comfy_cms_categories"."id" =
"comfy_cms_categorizations"."category_id" INNER JOIN (SELECT
"comfy_cms_pages"."id" AS pg_search_id,
(ts_rank((to_tsvector('simple',
coalesce("comfy_cms_pages"."content_cache"::text, '')) ||
to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))),
(to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':')), 0)) AS
rank FROM "comfy_cms_pages" WHERE (((to_tsvector('simple',
coalesce("comfy_cms_pages"."content_cache"::text, '')) ||
to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, '')))
## (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':')))))
pg_search_comfy_cms_pages ON "comfy_cms_pages"."id" =
pg_search_comfy_cms_pages.pg_search_id WHERE (layout_id = '1' AND
is_published = 't') AND "comfy_cms_categories"."label" = 'Company
News' ORDER BY pg_search_comfy_cms_pages.rank DESC,
"comfy_cms_pages"."id" ASC, "comfy_cms_pages"."created_at" DESC
app/models/article.rb
class Article < Comfy::Cms::Page
cms_is_categorized
include PgSearch
pg_search_scope :search_by_keywords, against: [:content_cache, :label], using: { tsearch: { any_word: true, prefix: true } }
app/commands/search_articles_command.rb
class SearchArticlesCommand
def initialize(params = {})
#since = params[:since_date]
#keys = params[:search_query]
#category = params[:category]
end
def execute
Article.unscoped do
query = if #since.present?
Article.article.since_date(#since)
else
Article.published_article
end
query = query.for_category(#category) if #category.present?
query = query.search_by_keywords(#keys) if #keys.present?
query.where('').order(created_at: :desc)
end
end
end
comfortable-mexican-sofa/lib/comfortable_mexican_sofa/extensions/is_categorized.rb
module ComfortableMexicanSofa::IsCategorized
def self.included(base)
base.send :extend, ClassMethods
end
module ClassMethods
def cms_is_categorized
include ComfortableMexicanSofa::IsCategorized::InstanceMethods
has_many :categorizations,
:as => :categorized,
:class_name => 'Comfy::Cms::Categorization',
:dependent => :destroy
has_many :categories,
:through => :categorizations,
:class_name => 'Comfy::Cms::Category'
attr_accessor :category_ids
after_save :sync_categories
scope :for_category, lambda { |*categories|
if (categories = [categories].flatten.compact).present?
self.distinct.
joins(:categorizations => :category).
where('comfy_cms_categories.label' => categories)
end
}
end
end
module InstanceMethods
def sync_categories
(self.category_ids || {}).each do |category_id, flag|
case flag.to_i
when 1
if category = Comfy::Cms::Category.find_by_id(category_id)
category.categorizations.create(:categorized => self)
end
when 0
self.categorizations.where(:category_id => category_id).destroy_all
end
end
end
end
end
ActiveRecord::Base.send :include, ComfortableMexicanSofa::IsCategorized
Updated Error
PG::SyntaxError: ERROR: syntax error at or near "."
LINE 4: ...e = 'Class' AND categorized_id = 'comfy_cms_pages'.'id' AND ...
^
: SELECT "comfy_cms_pages".* FROM "comfy_cms_pages" INNER JOIN (SELECT "comfy_cms_pages"."id" AS pg_search_id, (ts_rank((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))), (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':*')), 0)) AS rank FROM "comfy_cms_pages" WHERE (((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))) ## (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':*'))))) pg_search_comfy_cms_pages ON "comfy_cms_pages"."id" = pg_search_comfy_cms_pages.pg_search_id WHERE "comfy_cms_pages"."layout_id" = $1 AND "comfy_cms_pages"."is_published" = $2 AND (
EXISTS (
SELECT 1 FROM categorizations
WHERE categorized_type = 'Class' AND categorized_id = 'comfy_cms_pages'.'id' AND category_id IN (2)
)) ORDER BY pg_search_comfy_cms_pages.rank DESC, "comfy_cms_pages"."id" ASC
working solution but not a scope and have to be careful of order its being called
def self.for_category(_category)
Comfy::Cms::Categorization.includes(:category).references(:category).select(:categorized).pluck(:categorized_id)
find(ids)
end
I think it's best to override for_category built-in filter of your CMS. Too many joins in that query.
Override for_category like this:
scope :for_category, lambda { |*categories|
if (categories = [categories].flatten.compact).present?
self_ids = "{connection.quote_table_name(self.table_name)}.#{connection.quote_column_name(self.primary_key)}"
self.where(
"EXISTS (" +
Comfy::Cms::Categorization.select('1').
where(categorized_type: self.name).
where('categorized_id' => self_ids).
where(category_id: Comfy::Cms::Category.where(label: categories).pluck(:id)).to_sql +
")"
)
end
}
More on SQL EXISTS usage in Rails you can read in my Rails: SQL EXISTS brief how-to.
More on why you bump into that error you can read in question and answer here.
Specifically, pg_search wants order your results by rank. And for_category wants to select distinct fields of Article only, and doesn't care about search rank. Changing its code to use simple EXISTS instead of complex JOIN query will fix that.
I was able to solve this problem by applying reorder to the result of the pg_search result.
search_command.rb
class SearchArticlesCommand
def initialize(params = {})
#since = params['since_date']
#keys = params['search_query']
#category = params['category']
end
def execute
Article.unscoped do
query = Article.article
query = if #since.present?
query.since_date(#since)
else
query.published
end
query = query.for_category(#category) if #category.present?
query = query.search_by_keywords(#keys).reorder('updated_at DESC') if #keys.present?
query
end
end
end
I also overrode for_category (not required)
article.rb
scope :for_category, (lambda do |category|
published
.joins(:categories)
.group(:id)
.where('comfy_cms_categories.label' => category)
.select('comfy_cms_pages.*')
end)
I'm trying to create a search form (rails 3.1) where one of the search parameters allows the user to pick math symbols like <, >, = etc. I then want to use the value chosen as part of my query. The only problem is it puts quotation marks around it and results in invalid sql.
simplified example
params[:comparison] = '>'
params[:rank] = '3'
.where("rank ? ?", params[:comparison], params[:rank].to_i)
results in
PGError: ERROR: syntax error at or near "3"
LINE 1: ... WHERE (rank '>' 3)
I want to make it so its
WHERE (rank > 3)
How can I create this active record query without the quotation marks around the greater than symbol in a way thats argument safe and not vulnerable to SQL injection exploits?
In this very specific case, I would suggest you just check the value of params[:comparison] since you can easily "whitelist" it against the known safe values you are expecting which I assume are <, > and =
Example code:
known_comparisons = %w{< > =}
params_comparison = ">"
if known_comparisons.any? { |i| i === params_comparison }
puts "were good"
else
puts "bad value"
end
Then embed the value directly with string interpolation since you now are sure it is safe.
.where("rank #{params[:comparison]} ?", params[:rank].to_i)
This problem reminds me Query class at Redmine. Source code at here.
class Query < ActiveRecord::Base
##operators = { "=" => :label_equals,
"!" => :label_not_equals,
"o" => :label_open_issues,
"c" => :label_closed_issues,
"!*" => :label_none,
"*" => :label_all,
">=" => :label_greater_or_equal,
"<=" => :label_less_or_equal,
"<t+" => :label_in_less_than,
">t+" => :label_in_more_than,
"t+" => :label_in,
"t" => :label_today,
"w" => :label_this_week,
">t-" => :label_less_than_ago,
"<t-" => :label_more_than_ago,
"t-" => :label_ago,
"~" => :label_contains,
"!~" => :label_not_contains }
cattr_reader :operators
##operators_by_filter_type = { :list => [ "=", "!" ],
:list_status => [ "o", "=", "!", "c", "*" ],
:list_optional => [ "=", "!", "!*", "*" ],
:list_subprojects => [ "*", "!*", "=" ],
:date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t", "w" ],
:string => [ "=", "~", "!", "!~" ],
:text => [ "~", "!~" ],
:integer => [ "=", ">=", "<=", "!*", "*" ] }
def statement
# filters clauses
filters_clauses = []
filters.each_key do |field|
next if field == "subproject_id"
v = values_for(field).clone
next unless v and !v.empty?
operator = operator_for(field)
# "me" value subsitution
if %w(assigned_to_id author_id watcher_id).include?(field)
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
end
sql = ''
if field =~ /^cf_(\d+)$/
# custom field
db_table = CustomValue.table_name
db_field = 'value'
is_custom_filter = true
sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
elsif field == 'watcher_id'
db_table = Watcher.table_name
db_field = 'user_id'
sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
elsif field == "member_of_group" # named field
if operator == '*' # Any group
groups = Group.all
operator = '=' # Override the operator since we want to find by assigned_to
elsif operator == "!*"
groups = Group.all
operator = '!' # Override the operator since we want to find by assigned_to
else
groups = Group.find_all_by_id(v)
end
groups ||= []
members_of_groups = groups.inject([]) {|user_ids, group|
if group && group.user_ids.present?
user_ids << group.user_ids
end
user_ids.flatten.uniq.compact
}.sort.collect(&:to_s)
sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
elsif field == "assigned_to_role" # named field
if operator == "*" # Any Role
roles = Role.givable
operator = '=' # Override the operator since we want to find by assigned_to
elsif operator == "!*" # No role
roles = Role.givable
operator = '!' # Override the operator since we want to find by assigned_to
else
roles = Role.givable.find_all_by_id(v)
end
roles ||= []
members_of_roles = roles.inject([]) {|user_ids, role|
if role && role.members
user_ids << role.members.collect(&:user_id)
end
user_ids.flatten.uniq.compact
}.sort.collect(&:to_s)
sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
else
# regular field
db_table = Issue.table_name
db_field = field
sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
end
filters_clauses << sql
end if filters and valid?
filters_clauses << project_statement
filters_clauses.reject!(&:blank?)
filters_clauses.any? ? filters_clauses.join(' AND ') : nil
end
private
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
sql = ''
case operator
when "="
if value.any?
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
else
# IN an empty set
sql = "1=0"
end
when "!"
if value.any?
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
else
# NOT IN an empty set
sql = "1=1"
end
when "!*"
sql = "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
when "<="
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
when "o"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
when "<t-"
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
when "t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
when ">t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
when "<t+"
sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
when "t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
when "t"
sql = date_range_clause(db_table, db_field, 0, 0)
when "w"
first_day_of_week = l(:general_first_day_of_week).to_i
day_of_week = Date.today.cwday
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
when "~"
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
when "!~"
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
end
return sql
end
...
end
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