6

在我的 Rails 应用程序中,我有userswhich 可以有很多invoices,而后者又可以有很多payments.

现在在dashboard视图中,我想总结所有paymentsauser收到的信息,按年、季度或月排序。payments还细分为毛额净额税额

用户.rb

class User < ActiveRecord::Base

  has_many  :invoices
  has_many  :payments

  def years
    (first_year..current_year).to_a.reverse
  end

  def year_ranges
    years.map { |y| Date.new(y,1,1)..Date.new(y,-1,-1) }
  end

  def quarter_ranges
    ...
  end

  def month_ranges
    ...
  end

  def revenue_between(range, kind)
    payments_with_invoice ||= payments.includes(:invoice => :items).all
    payments_with_invoice.select { |x| range.cover? x.date }.sum(&:"#{kind}_amount") 
  end

end

发票.rb

class Invoice < ActiveRecord::Base

  belongs_to :user
  has_many :items
  has_many :payments

  def total
    items.sum(&:total)
  end

  def subtotal
    items.sum(&:subtotal)
  end

  def total_tax
    items.sum(&:total_tax)
  end

end

付款.rb

class Payment < ActiveRecord::Base

  belongs_to :user
  belongs_to :invoice  

  def percent_of_invoice_total
    (100 / (invoice.total / amount.to_d)).abs.round(2)
  end

  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

end

仪表板_控制器:

class DashboardsController < ApplicationController

  def index    
    if %w[year quarter month].include?(params[:by])   
      range = params[:by]
    else
      range = "year"
    end
    @ranges = @user.send("#{range}_ranges")
  end

end

index.html.erb

<% @ranges.each do |range| %>

  <%= render :partial => 'range', :object => range %>

<% end %>

_range.html.erb

<%= @user.revenue_between(range, :gross) %>
<%= @user.revenue_between(range, :taxable) %>
<%= @user.revenue_between(range, :net) %>

现在的问题是这种方法有效,但也会产生大量的 SQL 查询。在典型的dashboard视图中,我得到100 多个SQL 查询。在添加之前.includes(:invoice),还有更多的查询。

我认为主要问题之一是每张发票的subtotal,total_taxtotal并没有存储在数据库中的任何位置,而是根据每个请求进行计算。

谁能告诉我如何在这里加快速度?我不太熟悉 SQL 和 ActiveRecord 的内部工作原理,所以这可能是这里的问题。

谢谢你的帮助。

4

3 回答 3

4

Whenever revenue_between is called, it fetches the payments in the given time range and the associated invoices and items from the db. Since the time ranges have lot of overlap (month, quarter, year), same records are being fetched over and over again.

I think it is better to fetch all the payments of the user once, then filter and summarize them in Ruby.

To implement, change the revenue_between method as follows:

def revenue_between(range, kind) 
   #store the all the payments as instance variable to avoid duplicate queries
   @payments_with_invoice ||= payments.includes(:invoice => :items).all
   @payments_with_invoice.select{|x| range.cover? x.created_at}.sum(&:"#{kind}_amount") 
end

This would eager load all the payments along with associated invoices and items.

Also change the invoice summation methods so that it uses the eager loaded items

class Invoice < ActiveRecord::Base

   def total
     items.map(&:total).sum
   end

   def subtotal
     items.map(&:subtotal).sum
   end

   def total_tax
     items.map(&:total_tax).sum
   end

end
于 2013-09-30T04:39:15.530 回答
2

Apart from the memoizing strategy proposed by @tihom, I suggest you have a look at the Bullet gem, that as they say in the description, it will help you kill N+1 queries and unused eager loading.

于 2013-09-30T11:23:23.960 回答
1

您的大部分数据不需要是实时的。你可以有一个服务计算统计数据并将它们存储在你想要的任何地方(Redis,缓存......)。然后每 10 分钟或根据用户的要求刷新它们。

In the first place, render your page without stats and load them with ajax.

于 2013-09-29T12:26:01.560 回答