Reputation: 10207
In my Rails app I have users
who can have many payments
.
class User < ActiveRecord::Base
has_many :invoices
has_many :payments
def year_ranges
...
end
def quarter_ranges
...
end
def month_ranges
...
end
def revenue_between(range, kind)
payments.sum_within_range(range, kind)
end
end
class Invoice < ActiveRecord::Base
belongs_to :user
has_many :items
has_many :payments
...
end
class Payment < ActiveRecord::Base
belongs_to :user
belongs_to :invoice
def net_amount
invoice.subtotal * percent_of_invoice_total / 100
end
def taxable_amount
invoice.total_tax * percent_of_invoice_total / 100
end
def gross_amount
invoice.total * percent_of_invoice_total / 100
end
def self.chart_data(ranges, unit)
ranges.map do |r| {
:range => range_label(r, unit),
:gross_revenue => sum_within_range(r, :gross),
:taxable_revenue => sum_within_range(r, :taxable),
:net_revenue => sum_within_range(r, :net) }
end
end
def self.sum_within_range(range, kind)
@sum ||= includes(:invoice => :items)
@sum.select { |x| range.cover? x.date }.sum(&:"#{kind}_amount")
end
end
In my dashboard
view I am listing the total payments for the ranges
depending on the GET parameter that the user picked. The user can pick either years
, quarters
, or months
.
class DashboardController < ApplicationController
def show
if %w[year quarter month].include?(params[:by])
@unit = params[:by]
else
@unit = 'year'
end
@ranges = @user.send("#{@unit}_ranges")
@paginated_ranges = @ranges.paginate(:page => params[:page], :per_page => 10)
@title = "All your payments"
end
end
The use of the instance variable (@sum
) greatly reduced the number of SQL queries here because the database won't get hit for the same queries over and over again.
The problem is, however, that when a user creates, deletes or changes one of his payments
, this is not reflected in the @sum
instance variable. So how can I reset it? Or is there a better solution to this?
Thanks for any help.
Upvotes: 2
Views: 3760
Reputation: 8003
Instead of storing the association as an instance variable of the Class Payment
, store it as an instance variable of a user
(I know it sounds confusing, I have tried to explain below)
class User < ActiveRecord::Base
has_many :payments
def revenue_between(range)
@payments_with_invoices ||= payments.includes(:invoice => :items).all
# @payments_with_invoices is an array now so cannot use Payment's class method on it
@payments_with_invoices.select { |x| range.cover? x.date }.sum(&:total)
end
end
When you defined @sum
in a class method (class methods are denoted by self.
) it became an instance variable of Class Payment
. That means you can potentially access it as Payment.sum
. So this has nothing to do with a particular user and his/her payments. @sum
is now an attribute of the class Payment
and Rails would cache it the same way it caches the method definitions of a class.
Once @sum
is initialized, it will stay the same, as you noticed, even after user creates new payment or if a different user logs in for that matter! It will change when the app is restarted.
However, if you define @payments_with_invoices
like I show above, it becomes an attribute of a particular instance of User
or in other words instance level instance variable. That means you can potentially access it as some_user.payments_with_invoices
. Since an app can have many users these are not persisted in Rails memory across requests. So whenever the user instance changes its attributes are loaded again.
So if the user creates more payments the @payments_with_invoices
variable would be refreshed since the user instance is re-initialized.
Upvotes: 3
Reputation: 12554
This is incidental to your question, but don't use #select
with a block.
What you're doing is selecting all payments, and then filtering the relation as an array. Use Arel to overcome this :
scope :within_range, ->(range){ where date: range }
This will build an SQL BETWEEN statement. Using #sum
on the resulting relation will build an SQL SUM() statement, which is probably more efficient than loading all the records.
Upvotes: 3
Reputation: 2706
Well your @sum
is basically a cache of the values you need. Like any cache, you need to invalidate it if something happens to the values involved.
You could use after_save
or after_create
filters to call a function which sets @sum = nil
. It may also be useful to also save the range your cache is covering and decide the invalidation by the date of the new or changed payment.
class Payment < ActiveRecord::Base
belongs_to :user
after_save :invalidate_cache
def self.sum_within_range(range)
@cached_range = range
@sum ||= includes(:invoice => :items)
@sum.select { |x| range.cover? x.date }.sum(&total)
end
def self.invalidate_cache
@sum = nil if @cached_range.includes?(payment_date)
end
Upvotes: 0
Reputation: 3311
Maybe you could do it with observers:
# payment.rb
def self.cached_sum(force=false)
if @sum.blank? || force
@sum = includes(:invoice => :items)
end
@sum
end
def self.sum_within_range(range)
@sum = cached_sum
@sum.select { |x| range.cover? x.date }.sum(&total)
end
#payment_observer.rb
class PaymentObserver < ActiveRecord::Observer
# force @sum updating
def after_save(comment)
Payment.cached_sum(true)
end
def after_destroy(comment)
Payment.cached_sum(true)
end
end
You could find more about observers at http://apidock.com/rails/v3.2.13/ActiveRecord/Observer
Upvotes: 0