依靠has_many关系排序

时间:2015-08-10 11:11:02

标签: mysql ruby-on-rails ruby ruby-on-rails-4 activerecord

这是我经常遇到的问题。关于这个问题有一些类似的问题,但是没有一个问题非常完整(而且它们可能已经过时了,因为Rails 4可能引入了有助于解决这个问题的新功能)。

让我举一个问题的简单例子和解决问题的已知方法:

说我有User模型和Post模型,以及User has_many :posts

现在,我希望得到帖子最多的五位用户。

以下是我所知道的选项,但它们都有各自的缺点:

1)

users = User.all
@top_users = users.sort {|a,b| a.posts.count <=> b.posts.count}.take(5)

缺点:为每个用户提出了一个数据库请求,使得这个解决方案非常慢。

2)直接使用SQL代码加入 (例如参见this question and answer

select('users.*, COUNT(posts.id) AS posts_count').joins(:posts).group('users.id').order('posts_count DESC').take(5)

这将运行DataBase中的所有排序逻辑。但是:

  • 我们使用了很多特定于数据库的代码(例如在PostgreSQL中我们需要其他语法)。如果可能的话,最好使用ActiveRecord方法。
  • 使用内部联接意味着永远不会返回没有任何帖子的用户。当我们想要返回没有帖子的用户时,这是一个问题。

3)直接使用SQL与外部联接(参见例如this question and answers

User.select("users.*, COUNT(posts.id) as posts_count").joins("LEFT OUTER JOIN posts ON posts.user_id = users.id").group("posts.id").order("posts_count DESC")

这也会返回没有帖子的用户。缺点:

  • 更多特定于数据库的代码为#2,甚至更难阅读。

4)使用计数器缓存列 (有关此技术的完整说明,请参阅this Railscasts episode

基本上,在User上创建一个新列,通过在每次创建或删除新帖子时更改字段中的值来跟踪该用户的当前计数posts。 / p>

这是非常快速和可读的。缺点是我们只能在User上定义新字段后才能使用它。对于许多情况,这是可以接受的,但是更难以灵活,因为需要更改用户表,以便按照我们可能想要创建前五的关联工作。此外,由于这是一个缓存字段,因此存在不会触发字段更新的数据库操作。

有没有更好(可读和有效)的方法来实现这一目标?优选使用内置ActiveRecord方法的东西。

4 个答案:

答案 0 :(得分:5)

另一种方法,有一些限制可能使它更像是一个部分解决方案:

User.where(:id => Post.group(:user_id).
                       order("count(*) desc").
                       limit(5).
                       keys)

在查找具有最多帖子数量的五个用户时,这在数据库方面非常有效,因为它只需要扫描posts表的user_id列上的索引,因此对于非常大的数据集会有好处。它也是非常“干净”的Rails / ActiveRecord代码,应该与数据库无关。

如果以后计数顺序返回用户是关键的,那么一旦识别出这五个,就可以使用效率较低的排序方法,或者可以在ruby中使用密钥的检索顺序来对返回的用户进行排序。 / p>

答案 1 :(得分:1)

这是一个值得关注的方法:

User.joins("left join posts on posts.user_id = users.id").
     group(:id).
     order("count(*) desc").
     limit(5)

它在联接中有点手册,但如果你知道至少有五个用户有帖子,或者没有想要列出任何没有帖子的用户,那么你可以使用定期加入:

User.joins(:posts).
     group(:id).
     order("count(*) desc").
     limit(5)

如果你有其他has_many联接,则count(*)不一定是健壮的,但在这种情况下,你可能想要生成一个查询,例如:

select ...
from   users ...
order by (select count(*) from posts where posts.user_id = users.id)

P.S。在PostgreSQL上测试过。 ID列上的GROUP BY肯定不会在Oracle上工作,不确定其他人。

答案 2 :(得分:1)

这个选项可能值得研究,没有测试,所以可能需要一些调整。

class Post < ActiveRecord::Base
  belongs_to :user, counter_cache: true
end

使用counter_cache,您将点击数据库中的一个表。

class User < ActiveRecord::Base
  has_many :posts

  def self.top_5
    order('post_counts DESC').limit(5)
  end
end

在users表中添加posts_count整数列,默认为0。

class AddPostsCountToUsers < ActiveRecord::Migration
  def change
    add_column :users, :posts_count, :integer, default: 0
  end
end

如果您的数据库中已有现有用户。

您需要在控制台中运行以下命令,或者如果您需要运行几次,请将其转换为rake任务:

User.find_each { |user| User.reset_counters(user.id, :posts) }

答案 3 :(得分:0)

你也可以这样做 -

User.joins(:posts).select('users.*, count(*) as posts_count').group('users.id').order('posts_count')