【问题标题】:Getting cheapest products in categories in rails effectively有效地在 Rails 中的类别中获取最便宜的产品
【发布时间】:2012-03-04 21:19:22
【问题描述】:

我有两个模型

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,所以它也可以使用。

观察:我想过在 Products 上定义一个关系 :cheaper_products,但似乎也无济于事。

另一个想法:也可以通过返回其类别中所有最便宜产品的 id 的子查询来解决它,但它也没有让我解决(而且不太优雅)。

注意:我知道如何使用 bruteforce (selector_sql) 来做到这一点,但我真的很想了解更多 rails 3 方法。

【问题讨论】:

标签: sql ruby-on-rails-3 scope associations arel


【解决方案1】:

这是一个有趣的问题,我认为您走在正确的道路上。您可能可以使用纯 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"]

【讨论】:

  • 我有点希望我可以避免使用 SQL,但无论如何你的方法是鼓舞人心的,如果有帮助,我应该尝试看看它。一个问题:为什么要加入 :category + 添加与类别无关的 SQL。是否可以先加入 cheper_products 然后加入类别?我自己尝试将 has_many 关联定义到 Products 给我更便宜的产品,但我很难同时使用关联和范围(如果我想保留延迟加载机制)
  • 我这样做是为了防止延迟加载并在一个查询中执行整个事情。如果你避开joins(:category)而离开includes(:cateogry),当你调用Product.cheapest时会额外执行1个查询。如果你避开includes(:category)而离开joins(:category),每次通过产品延迟加载访问类别时都会执行一个查询。如果同时避开joins(:category)includes(:category),也会发生同样的事情。
  • 而且,是的,可以先加入更便宜的产品,然后加入类别。
  • 有更多的rails 3/arel方法吗? (例如,将更便宜的产品定义为关联然后重用它?)
  • @gorn 我相信,您可以在您的类别模型中创建一个范围:scope :with_cheapest_product, includes(:products) &amp; Product.cheapestMore
【解决方案2】:

你可以试试这个:

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

这不包括没有产品的类别。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多