0
0

current_user関係のN+1を解決したい

Posted at

まえがき

先日からkindleチックなwebアプリを作っていたところcurrent_user関係のN+1問題にぶち当たったので解決方法等をまとめておく

あらすじ

機能として「ログインなしでもセッションを使ってカートに商品は入れられるけど、ログインするときにセッションが残ってたらマージしてユーザーのカートと統合する」みたいなものです。

要するに、ログインなしの情報をユーザに引き継がせるみたいな。
(電子書籍を扱う想定なのでユーザーは同じ電子書籍を2つ以上変えません)

コード

以下の想定。application_controller.rbでセッションの有り無しを判別して、
存在する場合はcartモデルのcombine_and_destroy_other_cart!を呼ぶ。
内容としては

  • 過去に注文していないもの
  • カートに既に存在してもの

以上を満たすものをユーザーのカートに追加する。

その後セッションを破壊する。
以下詳細

モデル

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable
  has_one :cart, dependent: :destroy
  has_many :orders, dependent: :destroy
  has_many :order_items, dependent: :destroy
  has_many :ordered_books, through: :order_items, source: :book
  validates :name, presence: true
  ...
class Book < ApplicationRecord
  has_many :cart_items, dependent: :destroy
  has_many :order_items, dependent: :destroy
  ...
class Cart < ApplicationRecord
  belongs_to :user, optional: true
  has_many :cart_items, dependent: :destroy
  has_many :books, through: :cart_items

  def combine_and_destroy_other_cart!(other_cart)
    transaction do
      other_books = other_cart.books.where.not(id: user.ordered_books)
      self.books = (books + other_books).uniq
      other_cart.destroy!
    end
  end
end
class CartItem < ApplicationRecord
  belongs_to :cart
  belongs_to :book

  validates :cart_id, uniqueness: { scope: :book_id }
  ...

コントローラ

application_controller.rb
class ApplicationController < ActionController::Base
  helper_method :current_cart

  private

  def current_cart
    if user_signed_in?
      current_cart = current_user.cart || current_user.create_cart!
      if session[:cart_id]
        guest_cart = Cart.find_by(id: session[:cart_id])
        if guest_cart.present?
          current_cart.combine_and_destroy_other_cart!(guest_cart)
          session.delete(:cart_id)
        end
      end
    else
      current_cart = Cart.find_by(id: session[:cart_id]) || Cart.create
      session[:cart_id] ||= current_cart.id
    end
    current_cart
  end
end
carts_controller.rb
class CartsController < ApplicationController
  def show
    @cart_items = current_cart.cart_items.preload(book: :cover_img_attachment).order_by_oldest
  end
end

起こったこと

bulletをtest環境でも適応したら以下の事象が発生。

     Bullet::Notification::UnoptimizedQueryError:
       user: user
       GET /cart
       USE eager loading detected
         Cart => [:user]
         Add to your query: .includes([:user])

       Call stack
         /app/models/cart.rb:9:in `block in combine_and_destroy_other_cart!'

user参照してるやんけ!なんでや!とは思いつつ
combine_and_destroy_other_cart!でuser.ordered_booksにアクセスしてるのでそん時にuserが事前に呼ばれてないから改めて呼んでるでって怒られてるのかと解釈。
current_cartでuserに関するcartは作ってるしなぁということで色々試してみた。

やってみたこと1

current_user.cart.includes(:user)にしてみた

     Failure/Error: current_cart = current_user.cart.includes(:user) || current_user.create_cart!
     
     NoMethodError:
       undefined method `includes' for an instance of Cart

まぁ怒られるなと思った。
ActiveRecordに対してじゃなくてインスタンスに向けてincludesしてるからまぁ怒られるわなと。

やってみたこと2

current_cartのメソッドを修正

current_cart = Cart.find_or_create_by(user: current_user)

これだと通った。
以下参考だがcurrent_userが悪さしてると思ったんだけどどうなんだろ〜

https://madogiwa0124.hatenablog.com/entry/2018/07/08/230956

ちょっとしらべよ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0