如何在Rails模型中更新实例变量?

时间:2013-10-11 08:28:01

标签: ruby-on-rails ruby ruby-on-rails-3 instance-variables

在我的Rails应用中,我users可以拥有多个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

在我的dashboard视图中,我列出了ranges的总付款,具体取决于用户选择的GET参数。用户可以选择yearsquartersmonths

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实例变量中。那我该如何重置呢?或者有更好的解决方案吗?

感谢您的帮助。

4 个答案:

答案 0 :(得分: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变量,因为重新初始化了用户实例。

答案 1 :(得分:3)

这是您的问题的附带条件,但不要将#select与块一起使用。

您正在做的是选择所有付款,然后将关系过滤为数组。使用Arel来克服这个问题:

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

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

答案 2 :(得分: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

找到有关观察员的更多信息

答案 3 :(得分: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