Records not showing up in the database - ruby-on-rails

We start an operation by making sure a customer has enough items with which to work. So we begin by collecting all their current items in an array:
#items = SOrder.where(:user_id => current_user.id).order("order")
Then we determine how many items they should have. If someone has a free account, they should have 5 items. If it is a paid account they should have 20 items:
if current_user.paid
should_have = 19 # one less than 20 because of 0 position in the array
else
should_have = 4
end
Then, in case we need to add blank records, we figure out where we should start:
if #items.empty?
start = 0
else
start = #items.length + 1
end
If the start is less than or equal to what someone should have, then we add blank records:
if start <= should_have
value = [start .. should_have].each do |v|
SOrder.create(:user_id => current_user.id, :order => v, :item_id => 0 )
end
#items = SOrder.where(:user_id => current_user.id).order("order") # reload array
end
The records that should be added are not showing up in the database.
Where is the error?

Try
value = (start .. should_have).each do |v|
instead of
value = [start .. should_have].each do |v|
[start .. should_have] will just return an array with a single range element in it. (start .. should_have) will return a range, upon which the each enumerator will work as you expect.

The error may come from calling .length from an Arel object and not a record set.
#items = SOrder.where(:user_id => current_user.id).order("order").all
However, since you only need a count for the first query, I'd suggest using .count. If I was writing this I'd do something like:
number_of_items = SOrder.where(:user_id => current_user.id).count
number_of_blank_items_to_add = current_user.allowed_items - number_of_items
if number_of_blank_items_to_add > 0
number_of_blank_items_to_add.times do |num|
SOrder.create(:user_id => current_user.id, :order => (number_of_items + num), :item_id => 0 )
end
end
#str_order = SOrder.where(:user_id => current_user.id).order("order")
In User model:
def allowed_items
if paid
20
else
5
end
end
Better Yet
In User model:
has_many :s_orders, :order => "s_orders.order asc"
def add_extra_blank_orders
number_of_items = s_orders.count
number_of_blank_items_to_add = allowed_items - number_of_items
if number_of_blank_items_to_add > 0
number_of_blank_items_to_add.times do |num|
s_orders.create(:order => (number_of_items + num), :item_id => 0 )
end
end
def allowed_items
if paid
20
else
5
end
end
In controller:
current_user.add_extra_blank_orders
#str_order = current_user.s_orders
While I am sure that you have a good reason, I am questioning why blank items need to be in the database at all. And, if a after_create hook could be used here.

Try this code to ensure your code is entering the loop of creating records by adding puts "entered the loop" inside the loop like this:
if start <= should_have
(start .. should_have).each do |v|
puts "entered loop"
SOrder.create(:user_id => current_user.id, :order => v, :item_id => 0 )
end
#items = SOrder.where(:user_id => current_user.id).order("order") # reload array
end
If "entered loop" is getting printed, try .create! to make sure all the validations are passed(If any of them are failed ActiveRecord error will be raised stating the validation)
if start <= should_have
(start .. should_have).each do |order|
SOrder.create!(:user_id => current_user.id, :order => order, :item_id => 0 )
end
#str_order = SOrder.where(:user_id => current_user.id).order("order") # reload array
end

I don't see where you're using value and not sure why you're using it.
Can you use this?:
if start <= should_have
(start .. should_have).each do |order|
SOrder.create(:user_id => current_user.id, :order => order, :item_id => 0 )
end
end
#str_order = SOrder.where(:user_id => current_user.id).order("order") # reload
Edit: I moved #str_order outside of your if statement to make sure you'd always be reloading the array, if this is undesired just switch it back.

Related

Rails caching not working for manual query?

I'm trying to cache a lot of data (100 000) that i took with an SQL query, but the caching is not working (take about 30sec to write into the cache and the same amount of time to read it) What I'm I doing wrong? my config variable is already set to true
query = "SELECT inscriptions.`id`, banners.`id`, banners.`name`, inscriptions.`registered_at`,
inscriptions.`synched_at`, inscriptions.`state`
FROM inscriptions
JOIN firm_offices
ON inscriptions.`firm_office_1_id` = firm_offices.`id`
JOIN firms
ON firm_offices.`firm_id` = firms.`id`
JOIN banners
ON firms.`banner_id` = banners.`id`
GROUP BY inscriptions.`id`"
result = ActiveRecord::Base.connection.execute(query)
Rails.cache.fetch 'huge-array' do
data = []
result.each do |r|
data.push({ :id => r[0],
:banner_id => r[1],
:banner_name => r[2],
:registered_at => r[3],
:synched_at => r[4],
:state => r[5]})
end
data
end
#data = Rails.cache.read("huge-array")
Move it all inside your fetch block:
#data ||= Rails.cache.fetch 'huge-array' do
query = "SELECT inscriptions.`id`, banners.`id`, banners.`name`, inscriptions.`registered_at`, inscriptions.`synched_at`, inscriptions.`state`
FROM inscriptions
JOIN firm_offices
ON inscriptions.`firm_office_1_id` = firm_offices.`id`
JOIN firms
ON firm_offices.`firm_id` = firms.`id`
JOIN banners
ON firms.`banner_id` = banners.`id`
GROUP BY inscriptions.`id`"
result = ActiveRecord::Base.connection.execute(query)
data = []
result.each do |r|
data.push({ :id => r[0],
:banner_id => r[1],
:banner_name => r[2],
:registered_at => r[3],
:synched_at => r[4],
:state => r[5]})
end
data
end
Notes:
You don't need to actually move all of it into the block, just the expensive parts (e.g., execute(query)).
Your big SQL query looks like it could translate pretty easily into an AR query. You might want to translate it into an AR query, and then use to_sql if that turns out to be more efficient.
There's no need to run the query and the fetch block each time this function is called. Try something like
#data = Rails.cache.read("huge-array")
if #data.empty?
result = ActiveRecord::Base.connection.execute(query)
#data = []
result.each do |r|
#data.push({ :id => r[0],
:banner_id => r[1],
:banner_name => r[2],
:registered_at => r[3],
:synched_at => r[4],
:state => r[5]})
end
Rails.cache.write("huge-array", #data)
end
return #data
This way you only have to do the expensive query + array creation if the data does not already exist in cache.

Rails: Faster way to perform updates on many records

In our Rails 3.2.13 app (Ruby 2.0.0 + Postgres on Heroku), we are often retreiving a large amount of Order data from an API, and then we need to update or create each order in our database, as well as the associations. A single order creates/updates itself plus approx. 10-15 associcated objects, and we are importing up to 500 orders at a time.
The below code works, but the problem is it's not at all efficient in terms of speed. Creating/updating 500 records takes approx. 1 minute and generates 6500+ db queries!
def add_details(shop, shopify_orders)
shopify_orders.each do |shopify_order|
order = Order.where(:order_id => shopify_order.id.to_s, :shop_id => shop.id).first_or_create
order.update_details(order,shopify_order,shop) #This calls update_attributes for the Order
ShippingLine.add_details(order, shopify_order.shipping_lines)
LineItem.add_details(order, shopify_order.line_items)
Taxline.add_details(order, shopify_order.tax_lines)
Fulfillment.add_details(order, shopify_order.fulfillments)
Note.add_details(order, shopify_order.note_attributes)
Discount.add_details(order, shopify_order.discount_codes)
billing_address = shopify_order.billing_address rescue nil
if !billing_address.blank?
BillingAddress.add_details(order, billing_address)
end
shipping_address = shopify_order.shipping_address rescue nil
if !shipping_address.blank?
ShippingAddress.add_details(order, shipping_address)
end
payment_details = shopify_order.payment_details rescue nil
if !payment_details.blank?
PaymentDetail.add_details(order, payment_details)
end
end
end
def update_details(order,shopify_order,shop)
order.update_attributes(
:order_name => shopify_order.name,
:order_created_at => shopify_order.created_at,
:order_updated_at => shopify_order.updated_at,
:status => Order.get_status(shopify_order),
:payment_status => shopify_order.financial_status,
:fulfillment_status => Order.get_fulfillment_status(shopify_order),
:payment_method => shopify_order.processing_method,
:gateway => shopify_order.gateway,
:currency => shopify_order.currency,
:subtotal_price => shopify_order.subtotal_price,
:subtotal_tax => shopify_order.total_tax,
:total_discounts => shopify_order.total_discounts,
:total_line_items_price => shopify_order.total_line_items_price,
:total_price => shopify_order.total_price,
:total_tax => shopify_order.total_tax,
:total_weight => shopify_order.total_weight,
:taxes_included => shopify_order.taxes_included,
:shop_id => shop.id,
:email => shopify_order.email,
:order_note => shopify_order.note
)
end
So as you can see, we are looping through each order, finding out if it exists or not (then either loading the existing Order or creating the new Order), and then calling update_attributes to pass in the details for the Order. After that we create or update each of the associations. Each associated model looks very similar to this:
class << self
def add_details(order, tax_lines)
tax_lines.each do |shopify_tax_line|
taxline = Taxline.find_or_create_by_order_id(:order_id => order.id)
taxline.update_details(shopify_tax_line)
end
end
end
def update_details(tax_line)
self.update_attributes(:price => tax_line.price, :rate => tax_line.rate, :title => tax_line.title)
end
I've looked into the activerecord-import gem but unfortunately it seems to be more geared towards creation of records in bulk and not update as we also require.
What is the best way that this can be improved for performance?
Many many thanks in advance.
UPDATE:
I came up with this slight improvement, which essentialy removes the call to update the newly created Orders (one query less per order).
def add_details(shop, shopify_orders)
shopify_orders.each do |shopify_order|
values = {:order_id => shopify_order.id.to_s, :shop_id => shop.id,
:order_name => shopify_order.name,
:order_created_at => shopify_order.created_at,
:order_updated_at => shopify_order.updated_at,
:status => Order.get_status(shopify_order),
:payment_status => shopify_order.financial_status,
:fulfillment_status => Order.get_fulfillment_status(shopify_order),
:payment_method => shopify_order.processing_method,
:gateway => shopify_order.gateway,
:currency => shopify_order.currency,
:subtotal_price => shopify_order.subtotal_price,
:subtotal_tax => shopify_order.total_tax,
:total_discounts => shopify_order.total_discounts,
:total_line_items_price => shopify_order.total_line_items_price,
:total_price => shopify_order.total_price,
:total_tax => shopify_order.total_tax,
:total_weight => shopify_order.total_weight,
:taxes_included => shopify_order.taxes_included,
:email => shopify_order.email,
:order_note => shopify_order.note}
get_order = Order.where(:order_id => shopify_order.id.to_s, :shop_id => shop.id)
if get_order.blank?
order = Order.create(values)
else
order = get_order.first
order.update_attributes(values)
end
ShippingLine.add_details(order, shopify_order.shipping_lines)
LineItem.add_details(order, shopify_order.line_items)
Taxline.add_details(order, shopify_order.tax_lines)
Fulfillment.add_details(order, shopify_order.fulfillments)
Note.add_details(order, shopify_order.note_attributes)
Discount.add_details(order, shopify_order.discount_codes)
billing_address = shopify_order.billing_address rescue nil
if !billing_address.blank?
BillingAddress.add_details(order, billing_address)
end
shipping_address = shopify_order.shipping_address rescue nil
if !shipping_address.blank?
ShippingAddress.add_details(order, shipping_address)
end
payment_details = shopify_order.payment_details rescue nil
if !payment_details.blank?
PaymentDetail.add_details(order, payment_details)
end
end
end
and for the associated objects:
class << self
def add_details(order, tax_lines)
tax_lines.each do |shopify_tax_line|
values = {:order_id => order.id,
:price => tax_line.price,
:rate => tax_line.rate,
:title => tax_line.title}
get_taxline = Taxline.where(:order_id => order.id)
if get_taxline.blank?
taxline = Taxline.create(values)
else
taxline = get_taxline.first
taxline.update_attributes(values)
end
end
end
end
Any better suggestions?
Try wrapping your entire code into a single database transaction. Since you're on Heroku it'll be a Postgres bottom-end. With that many update statements, you can probably benefit greatly by transacting them all at once, so your code executes quicker and basically just leaves a "queue" of 6500 statements to run on Postgres side as the server is able to dequeue them. Depending on the bottom end, you might have to transact into smaller chunks - but even transacting 100 at a time (and then close and re-open the transaction) would greatly improve throughput into Pg.
http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
http://www.postgresql.org/docs/9.2/static/sql-set-transaction.html
So before line 2 you'd add something like:
def add_details(shop, shopify_orders)
Order.transaction do
shopify_orders.each do |shopify_order|
And then at the very end of your method add another end:
if !payment_details.blank?
PaymentDetail.add_details(order, payment_details)
end
end //shopify_orders.each..
end //Order.transaction..
end //method
You can monkey-patch ActiveRecord like this:
class ActiveRecord::Base
#http://stackoverflow.com/questions/15317837/bulk-insert-records-into-active-record-table?lq=1
#https://gist.github.com/jackrg/76ade1724bd816292e4e
# "UPDATE THIS SET <list_of_column_assignments> FROM <table_name> THIS JOIN (VALUES (<csv1>, <csv2>,...) VALS ( <column_names> ) ON <list_of_primary_keys_comparison>"
def self.bulk_update(record_list)
pk = self.primary_key
raise "primary_key not found" unless pk.present?
raise "record_list not an Array of Hashes" unless record_list.is_a?(Array) && record_list.all? {|rec| rec.is_a? Hash }
return nil if record_list.empty?
result = nil
#test if every hash has primary keys, so we can JOIN
record_list.each { |r| raise "Primary Keys '#{self.primary_key.to_s}' not found on record: #{r}" unless hasAllPKs?(r) }
#list of primary keys comparison
pk_comparison_array = []
if (pk).is_a?(Array)
pk.each {|thiskey| pk_comparison_array << "THIS.#{thiskey} = VALS.#{thiskey}" }
else
pk_comparison_array << "THIS.#{pk} = VALS.#{pk}"
end
pk_comparison = pk_comparison_array.join(' AND ')
#SQL
(1..record_list.count).step(1000).each do |start|
key_list, value_list = convert_record_list(record_list[start-1..start+999])
#csv values
csv_vals = value_list.map {|v| "(#{v.join(", ")})" }.join(", ")
#column names
column_names = key_list.join(", ")
#list of columns assignments
columns_assign_array = []
key_list.each {|col|
unless inPK?(col)
columns_assign_array << "THIS.#{col} = VALS.#{col}"
end }
columns_assign = columns_assign_array.join(', ')
sql = "UPDATE THIS SET #{columns_assign} FROM #{self.table_name} THIS JOIN ( VALUES #{csv_vals} ) VALS ( #{column_names} ) ON ( #{pk_comparison} )"
result = self.connection.execute(sql)
return result if result<0
end
return result
end
def self.inPK?(str)
pk = self.primary_key
test = str.to_s
if pk.is_a?(Array)
(pk.include?(test))
else
(pk==test)
end
end
#test if given hash has primary keys included as hash keys and those keys are not empty
def self.hasAllPKs?(hash)
h = hash.stringify_keys
pk = self.primary_key
if pk.is_a?(Array)
(pk.all? {|k| h.key?(k) and h[k].present? })
else
h.key?(pk) and h[pk].present?
end
end
def self.convert_record_list(record_list)
# Build the list of keys
key_list = record_list.map(&:keys).flatten.map(&:to_s).uniq.sort
value_list = record_list.map do |rec|
list = []
key_list.each {|key| list << ActiveRecord::Base.connection.quote(rec[key] || rec[key.to_sym]) }
list
end
# If table has standard timestamps and they're not in the record list then add them to the record list
time = ActiveRecord::Base.connection.quote(Time.now)
for field_name in %w(created_at updated_at)
if self.column_names.include?(field_name) && !(key_list.include?(field_name))
key_list << field_name
value_list.each {|rec| rec << time }
end
end
return [key_list, value_list]
end
end
Then, you can generate a array of hashes containing your models attributes (including theirs primary keys) and do something like:
ActiveRecord::Base.transaction do
Model.bulk_update [ {attr1: val1, attr2: val2,...}, {attr1: val1, attr2: val2,...}, ... ]
end
It will be a single SQL command without Rails callbacks and validations.
For PostgreSQL, there are several issues that the above approach does not address:
You must specify an actual table, not just an alias, in the update target table.
You cannot repeat the target table in the FROM phrase. Since you are joining the target table to a VALUES table (hence there is only one table in the FROM phrase, you won't be able to use JOIN, you must instead use "WHERE ".
You don't get the same "free" casts in a VALUES table that you do in a simple "UPDATE" command, so you must cast date/timestamp values as such (#val_cast does this).
class ActiveRecord::Base
def self.update!(record_list)
raise ArgumentError "record_list not an Array of Hashes" unless record_list.is_a?(Array) && record_list.all? {|rec| rec.is_a? Hash }
return record_list if record_list.empty?
(1..record_list.count).step(1000).each do |start|
field_list, value_list = convert_record_list(record_list[start-1..start+999])
key_field = self.primary_key
non_key_fields = field_list - [%Q["#{self.primary_key}"], %Q["created_at"]]
columns_assign = non_key_fields.map {|field| "#{field} = #{val_cast(field)}"}.join(",")
value_table = value_list.map {|row| "(#{row.join(", ")})" }.join(", ")
sql = "UPDATE #{table_name} AS this SET #{columns_assign} FROM (VALUES #{value_table}) vals (#{field_list.join(", ")}) WHERE this.#{key_field} = vals.#{key_field}"
self.connection.update_sql(sql)
end
return record_list
end
def self.val_cast(field)
field = field.gsub('"', '')
if (column = columns.find{|c| c.name == field }).sql_type =~ /time|date/
"cast (vals.#{field} as #{column.sql_type})"
else
"vals.#{field}"
end
end
def self.convert_record_list(record_list)
# Build the list of fields
field_list = record_list.map(&:keys).flatten.map(&:to_s).uniq.sort
value_list = record_list.map do |rec|
list = []
field_list.each {|field| list << ActiveRecord::Base.connection.quote(rec[field] || rec[field.to_sym]) }
list
end
# If table has standard timestamps and they're not in the record list then add them to the record list
time = ActiveRecord::Base.connection.quote(Time.now)
for field_name in %w(created_at updated_at)
if self.column_names.include?(field_name) && !(field_list.include?(field_name))
field_list << field_name
value_list.each {|rec| rec << time }
end
end
field_list.map! {|field| %Q["#{field}"] }
return [field_list, value_list]
end
end

Nested ActiveRecords: Find many childrens of many parents

In my Rails 3.2 app a Connector has_many Incidents.
To get all incidents of a certain connector I can do this:
(In console)
c = Connector.find(1) # c.class is Connector(id: integer, name: string, ...
i = c.incidents.all # all good, lists incidents of c
But how can I get all incidents of many connectors?
c = Connector.find(1,2) # works fine, but c.class is Array
i = c.incidents.all #=> NoMethodError: undefined method `incidents' for #<Array:0x4cc15e0>
Should be easy! But I don't get it!
Here’s the complete code in my statistics_controller.rb
class StatisticsController < ApplicationController
def index
#connectors = Connector.scoped
if params['connector_tokens']
logger.debug "Following tokens are given: #{params['connector_tokens']}"
#connectors = #connectors.find_all_by_name(params[:connector_tokens].split(','))
end
#start_at = params[:start_at] || 4.weeks.ago.beginning_of_week
#end_at = params[:end_at] || Time.now
##time_line_data = Incident.time_line_data( #start_at, #end_at, 10) #=> That works, but doesn’t limit the result to given connectors
#time_line_data = #connectors.incidents.time_line_data( #start_at, #end_at, 10) #=> undefined method `incidents' for #<ActiveRecord::Relation:0x3f643c8>
respond_to do |format|
format.html # index.html.haml
end
end
end
Edit with reference to first 3 answers below:
Great! With code below I get an array with all incidents of given connectors.
c = Connector.find(1,2)
i = c.map(&:incidents.all).flatten
But idealy I'd like to get an Active Records object instead of the array, because I'd like to call where() on it as you can see in methode time_line_data below.
I could reach my goal with the array, but I would need to change the whole strategy...
This is my time_line_data() in Incidents Model models/incidents.rb
def self.time_line_data(start_at = 8.weeks.ago, end_at = Time.now, lim = 10)
total = {}
rickshaw = []
arr = []
inc = where(created_at: start_at.to_time.beginning_of_day..end_at.to_time.end_of_day)
# create a hash, number of incidents per day, with day as key
inc.each do |i|
if total[i.created_at.to_date].to_i > 0
total[i.created_at.to_date] += 1
else
total[i.created_at.to_date] = 1
end
end
# create a hash with all days in given timeframe, number of incidents per day, date as key and 0 as value if no incident is in database for this day
(start_at.to_date..end_at.to_date).each do |date|
js_timestamp = date.to_time.to_i
if total[date].to_i > 0
arr.push([js_timestamp, total[date]])
rickshaw.push({x: js_timestamp, y: total[date]})
else
arr.push([js_timestamp, 0])
rickshaw.push({x: js_timestamp, y: 0})
end
end
{ :start_at => start_at,
:end_at => end_at,
:series => rickshaw #arr
}
end
As you only seem to be interested in the time line data you can further expand the map examples given before e.g.:
#time_line_data = #connectors.map do |connector|
connector.incidents.map do |incident|
incident.time_line_data(#start_at, #end_at, 10)
end
end
This will map/collect all the return values of the time_line_data method call on all the incidents in the collection of connectors.
Ref:- map
c = Connector.find(1,2)
i = c.map(&:incidents.all).flatten

Are there any model analytics gems?

I'm working on allowing clients to view analytics per day, week, month, in a period of time, grouped by hours or days or months, etc... All of that is based on the created_at attribute.
Is there any gem out there that already does this? Something like:
Posts.analytics(:by => :day, :period => :this_week, :column => :created_at)
Would return:
{
'2012-06-19' => 14,
'2012-06-20' => 0, // Empty rows padding support*
'2012-06-21' => 3
}
I'm trying to make it from scratch but it seems like a lot of unecessary work if there's already a gem to do the job.
Update
I tried to make an analytics module that gets included into all models for easy analytics generation, But it's really unreliable, Sometimed i get more days than i need, and it's really messy, Could anyone collaborate and rewrite/improve on this:
# Usage:
# include Analytics::Timeline
# Model.timeline(:period => :last_24_hours, :time_by => :hour)
module Analytics
module Timeline
def self.included(base)
base.class_eval {
def self.timeline(*filters)
filters = filters[0]
period = filters[:period] || :this_week
time_by = filters[:time_by] || :days
date_column = filters[:date_column] || :created_at
# Named periods conventions
period_range = case period
when :last_12_hours
[Time.now-12.hours, Time.now]
when :last_24_hours
[Time.now-24.hours, Time.now]
when :last_7_days
[Time.now-7.days, Time.now]
when :last_30_days
[Time.now-30.days, Time.now]
when :this_week
[Time.now.beginning_of_week, Time.now.end_of_week]
when :past_week
[(Time.now - 1.week).beginning_of_week, (Time.now - 1.week).end_of_week]
when :this_month
[Time.now.beginning_of_month, Time.now.end_of_month]
when :past_month
[(Time.now-1.month).beginning_of_month, (Time.now - 1.month).end_of_month]
when :this_year
[Time.now.beginning_of_year, Time.now.end_of_year]
end
period_range = period if period.kind_of?(Array)
period_range = [period, Time.now] if period.is_a?(String)
# determine the SQL group method
group_column = case time_by
when :months
time_suffix = "-01 00:00:00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0].to_date, period_range[1].to_date)
"DATE_FORMAT(#{table_name}.#{date_column.to_s}, '%Y-%m')"
when :days
time_suffix = " 00:00:00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0].to_date, period_range[1].to_date)
"DATE(#{table_name}.#{date_column.to_s})"
when :hours
time_suffix = ":00:00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0], period_range[1])
"DATE_FORMAT(#{table_name}.#{date_column.to_s}, '%Y-%m-%d %H')"
when :minutes
time_suffix = ":00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0], period_range[1])
"DATE_FORMAT(#{table_name}.#{date_column.to_s}, '%Y-%m-%d %H:%M')"
end
# Get counts per cycle
records = records.group(group_column).select("*, count(*) AS series_count, #{group_column} AS series_time")
series = {}
# Generate placeholder series
time_table = { :days => 60*60*24, :hours => 60*60, :minutes => 60, :seconds => 0 }
if time_by == :months
ticks = 12 * (period_range[1].year - period_range[0].year) + (period_range[1].month + 1) - period_range[0].month
else
ticks = (period_range[1] - period_range[0] + 1) / time_table[time_by]
end
ticks.to_i.times do |i|
time = period_range[1]-i.send(time_by)
time = case time_by
when :minutes
time.change(:sec => 0)
when :hours
time.change(:min => 0)
when :days
time.change(:hour => 0)
when :months
time.change(:day => 1, :hour => 0)
end
series[time.to_s(:db)] = 0
end
# Merge real counts with placeholder series
to_merge = {}
records.each do |r|
to_merge[r.series_time.to_s+time_suffix] = r.series_count
end
series.merge!(to_merge)
end
}
end
end
end
The ActiveRecord statistics gem seems like it could be really useful to you.
If the statistics gem doesn't help, the admin_data gem has some analytics built in. Check out the demo. Use of the entire admin system might be overkill but you could at least try to browse the source to mimic the analytics feature.

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

Resources