在这种情况下如何避免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
我正在尝试在我的应用程序中实施 "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