Search code examples
ruby-on-railsrubyspree

How to do a nested includes for n + 1 in Rails


Here's what happens:

I have in my controller:

@products = Spree::Product.all_active

And in the model:

Spree::Product.class_eval do
  def self.all_active
    includes(:master)
    .where('available_on IS NULL OR available_on < ?', Time.now).where(deleted_at: nil)
  end
end

And in the view I'm calling something that will look like this:

@products.each do |product|
  product.images.each do |image|
    image.attachment.url(:product)
  end
end

The log is showing something along the lines like this for every single product:

  Spree::Image Load (2.6ms)  SELECT "spree_assets".* FROM "spree_assets"  WHERE "spree_assets"."type" IN ('Spree::Image') AND "spree_assets"."viewable_id" = $1 AND "spree_assets"."viewable_type" = $2  ORDER BY "spree_assets"."position" ASC  [["viewable_id", 9], ["viewable_type", "Spree::Variant"]]
  Spree::Price Load (0.3ms)  SELECT  "spree_prices".* FROM "spree_prices"  WHERE "spree_prices"."variant_id" = $1 AND "spree_prices"."currency" = 'USD' LIMIT 1  [["variant_id", 9]]
  CACHE (0.3ms)  SELECT "spree_zone_members".* FROM "spree_zone_members"  WHERE "spree_zone_members"."zone_id" = $1  [["zone_id", 2]]
  CACHE (0.0ms)  SELECT "spree_countries".* FROM "spree_countries"  WHERE "spree_countries"."id" IN (204, 49)
  CACHE (0.2ms)  SELECT "spree_shipping_methods".* FROM "spree_shipping_methods" INNER JOIN "spree_shipping_methods_zones" ON "spree_shipping_methods"."id" = "spree_shipping_methods_zones"."shipping_method_id" WHERE "spree_shipping_methods"."deleted_at" IS NULL AND "spree_shipping_methods_zones"."zone_id" = $1  [["zone_id", 2]]
  CACHE (0.0ms)  SELECT "spree_calculators".* FROM "spree_calculators"  WHERE "spree_calculators"."calculable_type" = 'Spree::ShippingMethod' AND "spree_calculators"."calculable_id" IN (3, 2, 1)
   (0.8ms)  SELECT COUNT(*) FROM "spree_assets"  WHERE "spree_assets"."type" IN ('Spree::Image') AND "spree_assets"."viewable_id" = $1 AND "spree_assets"."viewable_type" = $2  [["viewable_id", 1], ["viewable_type", "Spree::Variant"]]

I also have the bullet gem installed and it is recommending me to do:

N+1 Query detected
  Spree::Variant => [:images]
  Add to your finder: :include => [:images]

N+1 Query detected
  Spree::Variant => [:default_price]
  Add to your finder: :include => [:default_price]

I'm not sure where to place this .includes. to find out where Spree::Variant is being called I went to the Rails console:

2.0.0-p481 :001 > Spree::Product.first
  Spree::Product Load (1.8ms)  SELECT  "spree_products".* FROM "spree_products"  WHERE "spree_products"."deleted_at" IS NULL  ORDER BY "spree_products"."id" ASC LIMIT 1
 => #<Spree::Product id: 1, name: "Ruby on Rails Tote", description: "Debitis facilis impedit natus eos qui vero. Ut qua...", available_on: "2014-07-04 05:44:50", deleted_at: nil, slug: "ruby-on-rails-tote", meta_description: nil, meta_keywords: nil, tax_category_id: 1, shipping_category_id: 1, created_at: "2014-07-04 05:44:51", updated_at: "2014-07-04 05:45:30">
2.0.0-p481 :002 > Spree::Product.first.images
  Spree::Product Load (0.8ms)  SELECT  "spree_products".* FROM "spree_products"  WHERE "spree_products"."deleted_at" IS NULL  ORDER BY "spree_products"."id" ASC LIMIT 1
  Spree::Variant Load (1.0ms)  SELECT  "spree_variants".* FROM "spree_variants"  WHERE "spree_variants"."deleted_at" IS NULL AND "spree_variants"."product_id" = $1 AND "spree_variants"."is_master" = 't' LIMIT 1  [["product_id", 1]]
  Spree::Image Load (0.8ms)  SELECT "spree_assets".* FROM "spree_assets"  WHERE "spree_assets"."type" IN ('Spree::Image') AND "spree_assets"."viewable_id" = $1 AND "spree_assets"."viewable_type" = $2  ORDER BY "spree_assets"."position" ASC  [["viewable_id", 1], ["viewable_type", "Spree::Variant"]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Spree::Image id: 21, viewable_id: 1, viewable_type: "Spree::Variant", attachment_width: 360, attachment_height: 360, attachment_file_size: 31490, position: 1, attachment_content_type: "image/jpeg", attachment_file_name: "ror_tote.jpeg", type: "Spree::Image", attachment_updated_at: "2014-07-04 05:45:28", alt: nil, created_at: "2014-07-04 05:45:29", updated_at: "2014-07-04 05:45:29">, #<Spree::Image id: 22, viewable_id: 1, viewable_type: "Spree::Variant", attachment_width: 360, attachment_height: 360, attachment_file_size: 28620, position: 2, attachment_content_type: "image/jpeg", attachment_file_name: "ror_tote_back.jpeg", type: "Spree::Image", attachment_updated_at: "2014-07-04 05:45:29", alt: nil, created_at: "2014-07-04 05:45:30", updated_at: "2014-07-04 05:45:30">]>

Where would I add :images and :default_price within this context?


Solution

  • try:

    Spree::Product.class_eval do
      def self.all_active
        includes(master: { products: :images } )
        .where('available_on IS NULL OR available_on < ?', Time.now).where(deleted_at: nil)
      end
    end
    

    includes can be used to fetch multiple models, nested or not.