在这种情况下如何避免N + 1

时间:2018-04-26 11:08:58

标签: ruby-on-rails activerecord eager-loading

我正在尝试在我的应用中实现“喜欢”系统。我使用订单呈现一个表,然后当前用户能够“喜欢”订单,这样当订单状态发生变化时,她将收到通知。问题是我遇到了N + 1问题,因为每次表格被渲染时,程序会显示与订单一样多的查询,以检测订单是否已被用户“喜欢”。

我已经读过,可以通过使用“includes”来急切加载相关记录来避免这种情况,但我无法理解如何做到这一点,特别是在我的情况下。

我有这些模特和协会:

user.rb我是否包括喜欢的内容?触发N + 1警报的方法:

class User < ApplicationRecord
  devise :database_authenticatable, :recoverable, :rememberable, :trackable, 
  :validatable
  has_many :likes

  def likes?(order)
    order.likes.where(user_id: id).any?
  end
end

like.rb

class Like < ApplicationRecord
  belongs_to :user
  belongs_to :order
end

order.rb

class Order < ApplicationRecord

 has_many :likes
 .
 .
 .

对于表格的每一行,我会渲染此部分以显示订单是否喜欢:

<% if current_user.likes?(order) %>
  <%= link_to "<i class='fa fa-fire fa-2x fa-like'></i>".html_safe, 
  order_like_path(order), method: :delete, remote: true %>
<%else%>
  <%= link_to "<i class='fa fa-fire fa-2x fa-unlike'></i>".html_safe, 
  order_like_path(order), method: :post, remote: true %>
<%end%>

这是查询:

Rendered orders/_likes.html.erb (135.5ms)
Like Exists (0.5ms)  SELECT  1 AS one FROM "likes" WHERE "likes"."order_id" 
=$1 AND "likes"."user_id" = $2 LIMIT $3  [["order_id", 7875], ["user_id", 
1], ["LIMIT", 1]]

EDIT。我添加索引操作以防它有用:

  def index
    orders = request.query_string.present? ? Order.search(params, 
    current_user) : Order.pendientes
    if params[:button] == 'report'
      build_report(orders)
    else
    @orders = orders.order("#{sort_column} # 
    {sort_direction}").page(params[:page]).per(params[:paginas])
    end
  end

3 个答案:

答案 0 :(得分:2)

在这种情况下我通常做的是,因为您已经在视图上显示orders并且您拥有user所以我会抓取:

likes = current_user.likes.where(order: orders)
liked_order_ids = likes.pluck(:order_id)

我每次都会将liked_order_ids传递给_likes部分并检查liked_order_ids.include?(order.id)

我没有直接提取user.likes,因为可能有很多orders他喜欢并且当前页面上都没有。如果是这样,你可以像这样直接获取它们:

liked_order_ids = current_user.likes.pluck(:order_id)

所以这样它也不会执行任何新查询或缓存查询。

您尝试执行的方式是搜索order个人,然后浏览order个对象。相反,您可以使用user通过likes找到belongs to,因为它order。由于user将始终为多个且order为单一,因此它将执行单个查询以查找它,而不是使用java.sql.Timestamp在数据库中多次搜索。

显然,还有很多方法可以解决这个问题。选择取决于您和您的情况。

答案 1 :(得分:2)

class User < ApplicationRecord
  has_many :likes

  has_many :liked_orders, through: :likes, class_name: 'Order'

  def liked_orders_id
    @liked_orders_id ||= liked_orders.pluck(:id)
  end

  def liked_order?(order_id)
    liked_orders_id.include?(order_id)
  end
end

问题背后的根本原因似乎与您在likes?(order)模型中实施User方法的方式相同

  def likes?(order)
    order.likes.where(user_id: id).any?
  end

每次在加载的User上调用此方法时,它首先加载Order实例,然后在该加载的Order上加载其关联的Like实例以及加载的Like实例。 1}}实例应用user_id过滤器。

<强>更新

liked_orders关联应定义为

  has_many :liked_orders, through: :likes, source: :order

答案 2 :(得分:0)

它在OrdersController中显示或索引动作,对吗?您需要重新定义实例变量,如下所示:

@orders = current_user.orders.includes(:likes)
or 
@order = current_user.orders.find(params[:id]).includes(:likes)

likes?方法移至订单模型(例如,将其更改为liked_by)。

def liked_by?(user)
  likes.where(user_id: user.id).exists?
end

在视图中你有

<% if order.liked_by?(current_user) %>

在这种情况下,喜欢将被预加载并避免N + 1问题。

最好将bullet gem添加到应用中,它会警告您有关N + 1个查询并提供有关includes的建议

更新:

只需将includes添加到现有的@orders

即可
@orders = orders.includes(:likes).order("#{sort_column} # 
    {sort_direction}").page(params[:page]).per(params[:paginas])