まえがき
先日から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 }
...
コントローラ
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
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が悪さしてると思ったんだけどどうなんだろ〜
ちょっとしらべよ。