如何从has_many through关系中显示唯一记录?

122

我在想,在Rails3中,从一个has_many, through关系中展示唯一的记录的最佳方法是什么。

我有三个模型:

class User < ActiveRecord::Base
    has_many :orders
    has_many :products, :through => :orders
end

class Products < ActiveRecord::Base
    has_many :orders
    has_many :users, :through => :orders
end

class Order < ActiveRecord::Base
    belongs_to :user, :counter_cache => true 
    belongs_to :product, :counter_cache => true 
end

假设我想在客户的展示页面上列出他们订购的所有产品。

他们可能多次订购某些产品,因此我使用 counter_cache 按订单数量的降序排列显示。

但是,如果他们多次订购了某个产品,则需要确保每个产品仅列出一次。

@products = @user.products.ranked(:limit => 10).uniq!

如果一个产品有多个订单记录,则此功能有效,但如果该产品仅被订购一次,则会生成错误。(ranked是在其他地方定义的自定义排序函数)

另一种选择是:


@products = @user.products.ranked(:limit => 10, :select => "DISTINCT(ID)")

我不太确定我的做法是否正确。

有没有其他人尝试过这个?你遇到了什么问题?我在哪里可以找到关于 .unique! 和 DISTINCT() 之间的区别的更多信息?

通过 has_many, through 关系生成一个唯一记录列表的最佳方法是什么?

谢谢。

5个回答

271

你尝试过在 has_many 关联上指定 :uniq 选项吗:

has_many :products, :through => :orders, :uniq => true

来自Rails文档:

:uniq

如果设置为 true,则从集合中省略重复项。与 :through 结合使用时非常有用。

Rails 4更新:

在Rails 4中,has_many :products, :through => :orders, :uniq => true已被废弃。相反,你应该现在写成has_many :products, -> { distinct }, through: :orders。请参阅ActiveRecord Associations文档中的 has_many: :through关系的 distinct 部分获取更多信息。感谢Kurt Mueller在评论中指出这一点。


6
区别并不在于你是在模型(model)还是控制器(controller)中删除重复项,而在于你是否在关联(association)中使用了 :uniq 选项(如我的回答所示),或者使用 SQL 的 DISTINCT 声明(例如:has_many :products, :through => :orders, :select => "DISTINCT products.*)。在第一种情况下,所有记录都被获取,Rails 会替你删除重复项;而在后一种情况下,仅从数据库获取非重复记录,因此如果结果集很大,这可能会提高性能。 - mbreining
70
在Rails 4中,has_many :products, :through => :orders, :uniq => true已经被废弃。现在,你应该这样写:has_many :products, -> { uniq }, through: :orders - Kurt Mueller
8
请注意,此处的{uniq}仅是{distinct}的别名。http://apidock.com/rails/v4.1.8/ActiveRecord/QueryMethods/uniq 该术语出现在SQL中而非Ruby中。 - engineerDave
10
如果您在使用 DISTINCTORDER BY 子句时发生冲突,您可以始终使用 has_many :products, -> { unscope(:order).distinct }, through: :orders - fagiani
3
谢谢 @fagiani,如果你的模型有一个 JSON 列并且你使用 psql 数据库,那么情况会变得更加复杂,你需要像这样做:has_many :subscribed_locations, -> { unscope(:order).select("DISTINCT ON (locations.id) locations.*") },through: :people_publication_subscription_locations, class_name: 'Location', source: :location,否则你将会出现 rails ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: could not identify an equality operator for type json 的错误。 - ryan2johnson9
显示剩余3条评论

51

请注意,uniq: true选项已从Rails 4的has_many选项中移除。

在Rails 4中,您需要提供一个作用域来配置这种行为。可以通过lambda表达式来提供作用域,如下所示:

has_many :products, -> { uniq }, :through => :orders

Rails指南介绍了如何使用scopes过滤关系查询的方法,您可以查看第4.3.3节以及其他内容:

http://guides.rubyonrails.org/association_basics.html#has-many-association-reference


7
在 Rails 5.1 中,我一直收到“undefined method 'except' for #<Array...” 的错误提示,这是因为我使用了.uniq而不是.distinct - stevenspiel

12
在Rails6中,在scope中使用-> { distinct }将起作用。
class Person
  has_many :readings
  has_many :articles, -> { distinct }, through: :readings
end
 
person = Person.create(name: 'Honda')
article   = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
Reading.all.inspect     # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]

感谢这个解决方案。 - Darlan Dieterich

6
在Rails 6上,我成功地让它运作起来了:
  has_many :regions, -> { order(:name).distinct }, through: :sites

我尝试了其他答案,但都无法解决问题。


Rails 5.2+相同。 - Glenn
https://dev59.com/km025IYBdhLWcg3wtoQ_#68084151 可以在 Rails 6 中使用。 - fkoessler

5
你可以使用group_by。例如,我有一个照片库购物车,希望订单项按照哪张照片排序(每张照片可以多次订购并以不同的尺寸打印)。然后返回一个哈希表,产品(照片)作为键,可以列出每次订购的上下文中的照片(或不列出)。使用这种技术,你实际上可以输出每个给定产品的订单历史记录。不确定这对您在此上下文中是否有帮助,但我发现它非常有用。以下是代码:
OrdersController#show
  @order = Order.find(params[:id])
  @order_items_by_photo = @order.order_items.group_by(&:photo)

@order_items_by_photo看起来像这样:

=> {#<Photo id: 128>=>[#<OrderItem id: 2, photo_id: 128>, #<OrderItem id: 19, photo_id: 128>]

因此,你可以这样做:

@orders_by_product = @user.orders.group_by(&:product)

然后,当您在视图中获得此内容时,只需按照以下方式循环即可:
- for product, orders in @user.orders_by_product
  - "#{product.name}: #{orders.size}"
  - for order in orders
    - output_order_details

这样做可以避免仅返回一个产品时出现的问题,因为您始终知道它将返回带有产品作为键和订单数组的哈希。

这可能对您要尝试的内容来说过于繁琐,但是它确实为您提供了一些不错的选项(例如:订购日期等),除了数量外还可以使用。


感谢您提供详细的答案。这可能比我需要的多一些,但仍然很有趣(实际上我可能会在应用程序的其他地方使用它)。您对本页面中提到的各种方法的性能有何想法? - Andy Harvey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接