2013-10-11 50 views
2

在我的Rails应用程序中,我有users谁可以有很多payments如何在Rails模型中更新实例变量?

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 

在我dashboard鉴于我取决于用户选择了GET参数列出了ranges总付款。用户可以选择years,quartersmonths

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 

的使用实例变量(@sum)大大降低SQL的数量在这里查询,因为该数据库将不会越过命中为相同的查询一遍又一遍。

但问题是,当用户创建,删除或更改他的某个payments时,这不会反映在@sum实例变量中。那么我该如何重置它?还是有更好的解决方案呢?

感谢您的任何帮助。

回答

0

也许你可以观察员做到这一点:

# 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 

你可以找到更多关于观察员http://apidock.com/rails/v3.2.13/ActiveRecord/Observer

0

嗯,你@sum基本上是你所需要的值的缓存。像任何缓存一样,如果所涉及的值发生变化,您需要使其无效。

您可以使用after_saveafter_create过滤器调用设置为@sum = nil的函数。还可以保存缓存覆盖的范围,并根据新的或更改的付款日期确定失效情况。

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 
3

而不是存储的关联作为类Payment的一个实例变量,将其存储为一个user的实例变量的(我知道这听起来混乱,我试图下面解释)

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 

当您在类方法中定义@sum(类方法由self.表示)时,它成为类Payment的实例变量。这意味着您可以以Payment.sum的身份访问它。所以这与特定用户和他/她的付款无关。 @sum现在是类Payment的一个属性,Rails将以与缓存类的方法定义相同的方式对其进行缓存。

一旦@sum被初始化,它将保持不变,就像您注意到的那样,即使在用户创建新付款后或者其他用户登录该事件!它会在应用程序重新启动时更改。

但是,如果您像上面显示的那样定义@payments_with_invoices,它将成为User的特定实例的属性,换句话说就是实例级别的实例变量。这意味着您可以将其作为some_user.payments_with_invoices进行访问。由于一个应用程序可以有很多用户,所以这些用户不会跨越请求持久存储在Rails内存中所以无论何时用户实例更改其属性再次加载。

所以如果用户创建更多付款,@payments_with_invoices变量将被刷新,因为用户实例被重新初始化。

+0

+1,很好的解决方法。虽然我会让'sum_within_range'做所有使用SQL的计算(请参阅我的回答) –

+0

@m_x ya通常情况会更好,但由于prev问题,我有一些bkgrnd。这个函数在相同的重叠范围请求中被调用了很多次,所以我认为最好在Rails中进行过滤。但如果不是这样的话,你的解决方案更有意义:) – tihom

+0

谢谢。当我复制你的确切代码时,我仍然可以获得大约80个SQL查询。所以我将'includes(:invoice =>:items)'移动到'Payment'类的select方法的开头,现在每页只有大约10个SQL查询(与之前相同,因此也很好),但是总渲染时间已经增加了很多,从大约300毫秒到大约3000毫秒。这绝对是太多了,我想知道它为什么。 – Tintin81

3

这是附带的问题,但不要使用#select和块。

你在做什么是选择所有付款,然后过滤关系作为一个数组。使用Arel来解决这个问题:

scope :within_range, ->(range){ where date: range } 

这将构建一个SQL BETWEEN语句。在生成的关系上使用#sum将构建一条SQL SUM()语句,该语句可能比加载所有记录更有效。

+0

'select'是一个ActiveRecord查询方法,而不是数组。这只是范围上的另一个链条。 –

+3

不适用,不适用于块。尝试一下:它会加载所有记录。 –

+0

证明:http://apidock.com/rails/ActiveRecord/QueryMethods/select。 “与块,工作就像阵列#选择”看到源... –