has_one关联不使用包含

时间:2018-06-01 00:08:41

标签: ruby-on-rails postgresql activerecord rails-activerecord

在组合has_one关联和includes时,我一直试图找出一些奇怪的行为。

class Post < ApplicationRecord
  has_many :comments

  has_one :latest_comment, -> { order('comments.id DESC').limit(1) }, class_name: 'Comment'
end

class Comment < ApplicationRecord
  belongs_to :post
end

为了测试这个,我创建了两个帖子,每个帖子都有两条评论。以下是一些显示奇怪行为的rails控制台命令。当我们使用includes时,它会忽略latest_comment关联的顺序。

posts = Post.includes(:latest_comment).references(:latest_comment)
posts.map {|p| p.latest_comment.id}
=> [1, 3]

posts.map {|p| p.comments.last.id}
=> [2, 4]

我希望这些命令具有相同的输出。 posts.map {|p| p.latest_comment.id}应该返回[2, 4]。由于n + 1个查询问题,我无法使用第二个命令。

如果您单独调用最新评论(类似于上面的comments.last),那么事情就会按预期进行。

[Post.first.latest_comment.id, Post.last.latest_comment.id]
 => [2, 4]

如果您有其他方法可以实现此行为,我欢迎您提供意见。这个令我困惑。

1 个答案:

答案 0 :(得分:1)

我认为使用PostgreSQL最简单的方法是使用数据库视图来支持has_one :latest_comment关联。数据库视图或多或少是一个命名查询,其作用类似于只读表。

这里有三个广泛的选择:

  1. 使用大量查询:一个用于获取帖子,然后一个用于每个帖子以获取其最新评论。
  2. 将最新评论归结为帖子或其自己的表格。
  3. 使用window function删除comments表中的最新评论。
  4. (1)是我们要避免的。 (2)往往导致一系列过度并发症和错误。 (3)很好,因为它可以让数据库完成它的功能(管理和查询数据),但ActiveRecord对SQL的理解有限,因此需要一些额外的机制来使其表现。

    我们可以使用row_number窗口功能查找每篇文章的最新评论:

    select *
    from (
      select comments.*,
             row_number() over (partition by post_id order by created_at desc) as rn
      from comments
    ) dt
    where dt.rn = 1
    

    使用psql中的内部查询,您应该看到row_number()正在做什么。

    如果我们在latest_comments视图中包装该查询并在其前面加上LatestComment模型,您可以has_one :latest_comment并且事情会有效。当然,这并不容易:

    1. ActiveRecord无法理解迁移中的观看次数,因此您可以尝试使用scenic或切换from schema.rb to structure.sql之类的内容。

    2. 创建视图:

      class CreateLatestComments < ActiveRecord::Migration[5.2]
        def up
          connection.execute(%q(
            create view latest_comments (id, post_id, created_at, ...) as
            select id, post_id, created_at, ...
            from (
              select id, post_id, created_at, ...,
                     row_number() over (partition by post_id order by created_at desc) as rn
              from comments
            ) dt
            where dt. rn = 1
          ))
        end
        def down
          connection.execute('drop view latest_comments')
        end
      end
      

      如果您使用的是风景,那么看起来更像是正常的Rails迁移。我不知道你的comments表的结构,因此那里有...个;如果您愿意,可以使用select *,并且不介意rn中的迷路LatestComment列。您可能希望在comments上查看索引,以提高此查询的效率,但无论如何您迟早都会这样做。

    3. 创建模型,不要忘记手动设置主键,或includesreferences不会预加载任何内容(但preload会:)

      class LatestComment < ApplicationRecord
        self.primary_key = :id
        belongs_to :post
      end
      
    4. 将您现有的has_one简化为:

      has_one :latest_comment
      
    5. 可能会在测试套件中添加快速测试,以确保CommentLatestComment具有相同的列。 comments表格更改时,视图不会自动更新,但是一个简单的测试将作为提醒。

    6. 当有人抱怨“数据库中的逻辑”时,告诉他们在你工作的地方把他们的教条带到别处。

    7. 因此,它不会在评论中丢失,您的主要问题是您滥用scope关联中的has_one参数。当你说出这样的话:

      Post.includes(:latest_comment).references(:latest_comment)
      

      scope的{​​{1}}参数会在has_oneincludes添加到查询的LEFT JOIN的连接条件中结束。 ORDER BY在连接条件中没有意义,因此ActiveRecord不包含它,并且您的关联分崩离析。您不能使作用域依赖于实例(即references)将WHERE子句置于连接条件中,然后ActiveRecord将为您提供->(post) { some_query_with_post_in_a_where... },因为ActiveRecord不知道如何使用实例与ArgumentErrorincludes相关的范围。