Ruby on Rails 3:按虚拟属性排序索引视图还是创建表列?

时间:2012-09-27 17:08:10

标签: ruby-on-rails-3

我正在建立一个发票应用程序,其中发票有很多物品和付款。

在我的索引视图中,我显示了所有发票的列表,包括两个虚拟属性:

def total
  items.sum { |item| item.total }
end

def balance
  self.payments.sum(:amount) - self.total
end

我注意到显示索引视图需要大量的SQL。是否建议再创建两个表列?到目前为止,我选择不这样做,因为我不喜欢有太多的冗余数据。

这是我的控制器

def index
  result = current_user.invoices.includes(:items, :payments)
  @invoices = paginate(result)
end

index.html.erb

<table id="index">
  <thead>
    <tr>
      <th>Number</th>
      <th>Date</th>
      <th>Total</th>
      <th>Balance</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <%= render @invoices %>
  </tbody>
</table>
<%= will_paginate @invoices %>

_invoice.html.erb

<tr>
<td>
    <%= link_to invoice.number, invoice_path(invoice) %>
</td>
<td>
    <%= l invoice.date %>
</td>
<td>
    <%= number_to_currency(invoice.total) %>
</td>
<td>
    <%= number_to_currency(invoice.balance) %>
</td>       
<td>
    <%= destroy_link(invoice) %>
</td>
</tr>

对于索引视图中的每个发票,正在生成这四个SQL查询:

(0.1ms)  SELECT SUM("payments"."amount") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = 19
CACHE (0.0ms)  SELECT "items".* FROM "items" WHERE "items"."invoice_id" = 19
CACHE (0.0ms)  SELECT SUM("payments"."amount") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = 19
CACHE (0.0ms)  SELECT "items".* FROM "items" WHERE "items"."invoice_id" = 19

(每个索引页面有40个SQL查询。)

我必须承认我对Rails相对较新。所以我想知道是否有最佳实践可以效仿?

1 个答案:

答案 0 :(得分:1)

你可以在这里使用的最简单的改进可能是急切的加载。

当你加载Invoice个对象时,如果你急于加载相关的项目和付款,你会做3个查询而不是很多。

所以,如果你有一个控制器动作做这样的事情:

def index
  @invoices = Invoice.all
end

您可以将其更改为:

@invoices = Invoice.includes(:payments, :items).all

此更改应该可以加快速度,并且不需要您更改totalbalance方法 - 它们仍然会执行相同的操作,但会获取所需的所有对象一次,而不是一次几次。

现在,这仍然(可能至少)将大量对象加载到内存中。如果您要在该视图中显示与各个项目和付款相关的各种数据,则可能无需执行任何操作。但是,如果所有视图需求都是总计和余额值,您可以让数据库为您执行此操作并跳过实例化对象,如下所示:

@invoices = Invoice.select("invoices.*, sum(items.total) as item_total, (sum(payments.amount) - sum(items.total)) as remaining_balance").joins(:items, :payments).group('invoices.id')

当您使用这样的自定义select子句时,额外列将作为具有列名称的属性移植到返回的对象上,因此您可以执行以下操作:

@invoices.first.remaining_balance

我为这些列使用了不同的名称,以便它们不会与您现有的余额和总方法重叠 - 您可能需要也可能不需要这些名称,因为这些值仅在它们被调用的对象上存在时才存在自定义选择描述。如果在没有它的情况下加载它们,尝试调用.remaining_balance将产生NoMethodError。

警告:我正在使用PostgreSQL 9.1.3,如果你不是,上面的内容可能需要稍加修改。此外,适配器很容易将这些值作为字符串返回,因此您可能需要调用.to_f或类似的东西。