Получение самых дешевых продуктов в категориях в рельсах эффективно - PullRequest
2 голосов
/ 05 марта 2012

У меня две модели

class Product < ActiveRecord::Base
  belongs_to :category
end

class Product < ActiveRecord::Base
  belongs_to :category
end

Теперь я хочу перечислить все категории с их самыми дешевыми продуктами. Это очень легко сделать путем получения всех категорий, итерации по ним и поиска самого дешевого продукта по отдельности, но это делает много запросов и очень медленный. Я могу сделать это довольно легко с SQL - что-то вроде

SELECT products.*, categories.*
  FROM products
  JOIN categories ON (categories.id = products.owner_id)
  LEFT JOIN products as cheaper_products ON 
   cheaper_products.category_id = epochs.category_id AND
   cheaper_products.price < products.price
  WHERE cheaper_products.owner_id IS NULL

что является хорошим трюком для SQL, когда мы «СЛЕДУЕМ ПРИСОЕДИНЯТЬСЯ» ко всем более дешевым продуктам в пределах категории к каждому продукту, а не к тем, у кого их нет.

Я хотел бы знать, как нечто подобное можно сделать с помощью отношений Rails3 - я использую squeel, чтобы его можно было использовать также.

Наблюдение : Я думал об определении отношения: cheap_products в продуктах, но, похоже, это тоже не помогает.

Другая идея : ее также можно решить с помощью подзапросов, возвращающих идентификаторы всех самых дешевых продуктов в их категории, но это также не позволило мне найти решение (и менее элегантно).

Примечание : Я знаю, как это сделать с помощью bruteforce (selector_sql), но мне бы очень хотелось узнать больше о рельсах 3 способа сделать это.

Ответы [ 2 ]

2 голосов
/ 05 марта 2012

Это интересная проблема, и я думаю, что вы на правильном пути. Вероятно, вы могли бы решить эту проблему, используя чистый SQL-запрос, легче, чем иметь дело с ActiveRecord, так как вы не можете избежать написания SQL-кода.

Один из способов - объединить готовую загрузку с SQL в объединениях, чтобы выполнялся только один запрос:

# app/models/category.rb
class Category < ActiveRecord::Base
  has_many :products
end

# app/models/product.rb
class Product < ActiveRecord::Base
  belongs_to :category

  def self.cheapest
    joins(:category, "LEFT OUTER JOIN products AS cheaper_products ON (products.category_id = cheaper_products.category_id AND cheaper_products.price < products.price)").
    where("cheaper_products.category_id IS ?", nil).
    includes(:category)
  end

end

При тестировании в консоли

1.9.3-p125 :001 > Product.cheapest.to_sql
 => "SELECT \"products\".* FROM \"products\" INNER JOIN \"categories\" ON \"categories\".\"id\" = \"products\".\"category_id\" LEFT OUTER JOIN products\n      AS cheaper_products\n      ON (products.category_id = cheaper_products.category_id\n      AND cheaper_products.price < products.price) WHERE (cheaper_products.category_id IS NULL)" 

1.9.3-p125 :002 > Product.where(:category_id => 1).minimum(:price)
   (0.2ms)  SELECT MIN("products"."price") AS min_id FROM "products" WHERE "products"."category_id" = 1
 => 16.0 
1.9.3-p125 :003 > Product.where(:category_id => 2).minimum(:price)
   (0.3ms)  SELECT MIN("products"."price") AS min_id FROM "products" WHERE "products"."category_id" = 2
 => 7.0 
1.9.3-p125 :004 > Product.where(:category_id => 3).minimum(:price)
   (0.3ms)  SELECT MIN("products"."price") AS min_id FROM "products" WHERE "products"."category_id" = 3
 => 19.0 

1.9.3-p125 :005 > cheap_products = Product.cheapest
...

1.9.3-p125 :006 > cheap_products.each {|product| p [product.price, product.category.name] }
[16.0, "Category 0"]
[7.0, "Category 1"]
[19.0, "Category 2"]
0 голосов
/ 28 июля 2012

Вы можете попробовать это:

categories = Category.includes(:products).group('categories.id,products.id').having('MIN(products.price)')

categories.each {|c| p c.products.first} # List of cheapest product in each category

Сюда не входят категории, в которых нет товаров.

...