【问题标题】:has_one association not working with includeshas_one 关联不适用于包含
【发布时间】:2018-06-01 00:08:40
【问题描述】:

当结合 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

为了测试这一点,我创建了两个帖子,每个帖子都有两个 cmets。下面是一些显示奇怪行为的 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]

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

【问题讨论】:

  • 您似乎使用了错误的顺序。您使用ASC 但您的期望使用p.comments.last.id 的范围实际上是DESC 订单
  • 我认为你不能那样做。您的 has_onescope 参数最终出现在查询产生的 LEFT JOIN 的连接条件中(请参阅Post.includes(:latest_comment).references(:latest_comment).to_sql),并且 ORDER BY 在连接条件中没有任何意义,因此 AR 不会费心将其放入。如果您尝试使用 WHERE 子句(将在连接条件中)使范围实例依赖 (-&gt;(post) { ... }) 以获取最新的,那么您将收到 ArgumentError。
  • @yeuem1vannam - 是的,应该是 DESC。我更新了。
  • @muistooshort - 是的。这一切都说得通。有没有其他方法可以做到这一点?这是我现在的解决方法,Post.left_joins(:latest_comment).distinct
  • 你有点滥用has_one 及其范围参数,所以它只是部分工作。您使用的是哪个数据库?

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


【解决方案1】:

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

这里有三种广泛的选择:

  1. 使用大量查询:一个用于获取帖子,然后一个用于每个帖子以获取其最新评论。
  2. 将最新评论非规范化到帖子或其自己的表格中。
  3. 使用window functioncomments 表中剥离最新的cmets。

(1) 是我们试图避免的。 (2) 往往会导致一连串的过度复杂化和错误。 (3) 很好,因为它可以让数据库做它擅长的事情(管理和查询数据),但是 ActiveRecord 对 SQL 的理解有限,因此需要一些额外的机制来使其正常运行。 p>

我们可以使用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 表的结构,因此那里的所有...s;如果您愿意并且不介意LatestComment 中的杂散rn 列,您可以使用select *。您可能想查看您在 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. 当有人抱怨“数据库中的逻辑”时,告诉他们把他们的教条带到别处,因为你有工作要做。


为了避免在 cmets 中丢失,您的主要问题是您在 has_one 关联中滥用了 scope 参数。当你说这样的话:

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

has_onescope 参数以 includesreferences 添加到查询的 LEFT JOIN 的连接条件结束。 ORDER BY 在连接条件中没有意义,因此 ActiveRecord 不包含它,您的关联就会分崩离析。您不能使作用域与实例相关(即-&gt;(post) { some_query_with_post_in_a_where... })以将 WHERE 子句加入连接条件,然后 ActiveRecord 将为您提供ArgumentError,因为 ActiveRecord 不知道如何使用实例相关作用域includesreferences

【讨论】:

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