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

qinfanpeng · 发布于 2015年11月28日 · 最后由 qinfanpeng 回复于 2015年11月28日 · 975 次阅读
3790

路人皆知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/
共收到 6 条回复
2564
kikyous · #1 · 2015年11月28日

图片出问题了

3790
qinfanpeng · #2 · 2015年11月28日

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

1
Rei · #3 · 2015年11月28日

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

4楼 已删除
3790
qinfanpeng · #5 · 2015年11月28日

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

1667
serco · #6 · 2015年11月28日

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

TransactionHistory.with_item_named_after('item_1').preload(:transaction_items)
3790
qinfanpeng · #7 · 2015年11月28日

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

3790 qinfanpeng Rails (3&4) 预加载 (preload) 的 3 种方式 中提及了此贴 10月09日 17:56
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册