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

How to avoid N+1 in this situation

我正在尝试在我的应用程序中实施 "liking" 系统。我使用订单呈现 table,然后当前用户能够 "like" 订单,这样当订单状态发生变化时她会收到通知。问题是我遇到了 N+1 问题,因为每次呈现 table 时,程序都会根据显示的订单进行尽可能多的查询,以检测订单是否已经 "liked"用户。

我读到可以通过使用 "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
 .
 .
 .

对于 table 的每一行,我渲染这个部分以显示订单是否喜欢:

<% 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" 
= AND "likes"."user_id" =  LIMIT   [["order_id", 7875], ["user_id", 
1], ["LIMIT", 1]]

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

  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

在这种情况下,我通常会做的是,因为您已经在视图中看到了 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.

在数据库中搜索多次

显然还有很多解决方法。选择将取决于您和您的情况。

是在OrdersController show or index action 对吧?您需要像这样重新定义实例变量:

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

并将 likes? 方法移动到 Order 模型(例如更改它 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])
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

对我来说,你的问题背后的根本原因似乎是你在 User 模型

中实现 likes?(order) 方法的方式
  def likes?(order)
    order.likes.where(user_id: id).any?
  end

每次在已加载的 User 上调用此方法时,它首先加载 Order 实例,然后在该已加载的订单上加载其关联的 Like 实例,然后在那些已加载的实例上加载Like 个实例应用 user_id 过滤器。

更新

liked_orders 关联应定义为

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