Reputation: 545
I'm trying to implement a "liking" system in my app. I render a table with orders, then the current user is able to "like" an order so she will get notifications when the status of the order changes. The problem is that I'm in an N+1 issue, since each time the table gets rendered, the program makes as many queries as orders are displayed to detect if an order has already been "liked" by the user.
I have read that this can be avoided by using "includes" to eager load the associated records, but I can't wrap my head around how to do it, particularly in my case.
I have these models and associations:
user.rb
Did I include the likes? the method which is triggering the N+1 alert:
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
.
.
.
For each row of the table I render this partial to show if the order is liked or not:
<% 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%>
This is the query:
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. I add the index action in case it is useful:
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
Upvotes: 1
Views: 113
Reputation: 6552
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
The root cause behind your problem to me seems to be in the way you have implemented the likes?(order)
method in User
model
def likes?(order)
order.likes.where(user_id: id).any?
end
Every-time you invoke this method on a loaded User
, it first loads Order
instance, then on that loaded Order, loads its associated Like
instances and on those loaded Like
instances applies the user_id
filter.
Update
The liked_orders
association should be defined as
has_many :liked_orders, through: :likes, source: :order
Upvotes: 2
Reputation: 4640
It is in the OrdersController show or index action, right? You need to redefine instance variable like this:
@orders = current_user.orders.includes(:likes)
or
@order = current_user.orders.find(params[:id]).includes(:likes)
And move likes?
method to the Order model (change it liked_by
for example).
def liked_by?(user)
likes.where(user_id: user.id).exists?
end
In the view you'll have
<% if order.liked_by?(current_user) %>
In this case likes will be preloaded and you avoid N+1 issue.
It is a good idea to add bullet gem to the app, it will warn you about N+1 queries and give suggestions about includes
UPDATE:
Just add includes
to the existing @orders
@orders = orders.includes(:likes).order("#{sort_column} #
{sort_direction}").page(params[:page]).per(params[:paginas])
Upvotes: 0
Reputation: 6418
What generally I do in this situation is that as you have the orders
already on the view and you have the user
so I fetch:
likes = current_user.likes.where(order: orders)
liked_order_ids = likes.pluck(:order_id)
And I will pass the liked_order_ids
each time to the _likes
partial and check liked_order_ids.include?(order.id)
I have not fetched the user.likes
directly because there may be many orders
he has liked and all are not present on the current page. If they are you can fetch them directly like this:
liked_order_ids = current_user.likes.pluck(:order_id)
So this way it will not execute any new queries or cached queries too.
The way you are trying to do is search in the order
likes, so going through order
object. Instead you have the user
through which you can find the likes
as it belongs to
him too. As order
will always be multiple and user
will be single it will execute a single query to find it instead of searching multiple times in database using order
.
Obviously there are many more ways around it. The choice will depend on you and your situation.
Upvotes: 2