2012-03-04 3 views
2

У меня есть две моделейПолучения дешевых продуктов в категориях в рельсах эффективно

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 хороший трюк, где мы„LEFT JOIN“все более дешевый продукт в категории для каждого продукта, и чем принимать только эти, кто не имеет.

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

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

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

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

+0

любой, пожалуйста? – gorn

+0

Я знаю, что это не то, о чем вы просите, но вы можете сделать индекс в db. Взгляните на http://www.postgresql.org/docs/current/static/indexes-expressional.html – Automatico

ответ

2

Это интересная проблема, и я думаю, что вы на правильном пути. Возможно, вы, скорее всего, решите его с помощью чистого 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

Я вроде бы надеялся, что я смогу избежать SQL, но ваш подход все равно вдохновляет, я должен попытаться взглянуть при этом, если это поможет. Вопрос: зачем вы присоединяетесь: category + add SQL, который не имеет ничего общего с категориями. Можно ли сначала присоединиться к cheper_products, а затем к категории? Я сам пытался определить has_many связь с продуктами, дающими мне более дешевые продукты, но мне сложно использовать ассоциации и области применения в одно и то же время (если я хочу сохранять ленивые механизмы загрузки) – gorn

+0

Я сделал это, чтобы предотвратить ленивую загрузку и выполнить все в одном запросе. Если вы избегаете 'joins (: category)' и оставляете 'includes (: cateogry)', 1 дополнительный запрос будет выполнен, когда вы вызываете 'Product.cheapest'. Если вы избегаете 'include (: category)' и оставляете 'joins (: category)', один запрос будет выполняться каждый раз при доступе к продукту категории crawler * lazy loading *.То же самое произойдет, если вы избежите «joins (: category)» и «include (: category)». – Krule

+0

И, да, можно было бы сначала присоединиться к более дешевым продуктам, а затем к категории. – Krule

0

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

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 

Это не будет включать категории, в которых нет продуктов.