Rails 一个 includes (:column_name) 的小陷阱

qinfanpeng · November 28, 2015 · Last by qinfanpeng replied at November 28, 2015 · 3095 hits

路人皆知 Rails 可用includes用来避免N+1问题,但我最近就因 includes 献了一回丑。

class TransactionHistory < ActiveRecord::Base
  has_many :transaction_items
  scope :with_item_named_after, ->(item_name) { joins(:transaction_items).merge TransactionItem.named_after(item_name) }
end

class TransactionItem < ActiveRecord::Base
  belongs_to :transaction_history
  scope :named_after, ->(name) { where(name: name) }
end

现假设我们需要根据 item_name 来过滤 transaction_hitory,只要 transaction_hitory 中有 transaction_item 满足条件就行,但是为了信息完整也要显示匹配 transaction_hitory 中那些不匹配的 transaction_item。

第一次不假思索地写出了下面的代码:

# Controller
@transaction_histories = TransactionHistory.with_item_named_after(params[:query][:item_name]).includes(:transaction_items)

# View
<%- @transaction_histories.each do |transaction_history| %>
  <!-- ... -->
  <%= transaction_history.transaction_number %>
  <!-- ... -->
  <%- transaction_history.transaction_items.each do |transaction_item| %>
    <%= transaction_item.name %>
  <%- end %>
<%- end %>

不幸的是,这并不好使。用 item_name 去过滤,transaction_history 列表倒是对了,但每条 transaction_history 只显示了那些匹配的 transaction_item。这非我所愿。

transaction_history = create :transaction_history
item_1 = transaction_history.transaction_items.create name: 'item_1'
item_2 = transaction_history.transaction_items.create name: 'item_2'

# 下面这行代码返回的transaction_history里面就只包含了item_1,而没包含item_2
TransactionHistory.with_item_named_after('item_1').includes(:transaction_items)

 # 去掉解决 N+1问题的 includes(:items) 就对了,但我们的确需要解决 N+1。
TransactionHistory.with_item_named_after('item_1')

TransactionHistory.with_item_named_after('item_1').includes(:transaction_items)生成的 SQL 类似这样: 带 includes 查询 SQL

TransactionHistory.with_item_named_after('item_1')生成的 SQL 却类似这样: 不带 includes 查询 SQL

现在能想到的办法如下,不够优雅,且分成了两次查询,Rails 如此优雅的东西,肯定有更优雅的解决方案。若有人有漂亮的解决方案,请一定告诉我,不甚感激。

class TransactionHistory < ActiveRecord::Base
  has_many :transaction_items
  scope :with_item_named_after, ->(item_name) do
    matched_transaction_item_ids = TransactionItem.named_after(item_name).map(&:id).uniq
    where(id: matched_transaction_item_ids)
  end 
end    

听我啰嗦了这么久,最后推荐一个N+1 嗅探 gem 包表示感谢: bullet

另外,可能你也没从这学到啥东西,推荐几篇文章,表示我的歉意:

  1. https://robots.thoughtbot.com/using-arel-to-compose-sql-queries
  2. http://www.mitchcrowe.com/10-most-underused-activerecord-relation-methods/
  3. http://tomdallimore.com/blog/includes-vs-joins-in-rails-when-and-where/
  4. http://blog.arkency.com/2013/12/rails4-preloading/

图片出问题了

#1 楼 @kikyous 看起来要等一下,最初我用粘贴方式弄的也是这样,后来上传方法弄的刚开始是好的,现在又这样了。

https://ruby-china.org/topics/28226 你不知道帖子可以编辑吗?

4 Floor has deleted

#3 楼 @rei 是怎样调整地址,把图片给弄出来的。

#5 楼 @qinfanpeng 这是你想要的,2 条 query,虽然不是 1 条,但也不会 N+1

TransactionHistory.with_item_named_after('item_1').preload(:transaction_items)

#6 楼 @serco 谢谢,确实如你所说。说实话,我当时也看到过这篇文章http://blog.arkency.com/2013/12/rails4-preloading/,不过没仔细看,走马观花地浏览了一遍,毫无益处,都是自己粗心、心急所致。打算后面把它翻译了,放到社区来。谢谢。

qinfanpeng in Rails (3&4) 预加载 (preload) 的 3 种方式 mention this topic. 09 Oct 17:56
You need to Sign in before reply, if you don't have an account, please Sign up first.