Search code examples
ruby-on-railsactiverecordeager-loading

How to avoid N+1 in this situation


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

Solution

  • 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