5

我们正在构建一个创建类似于 Facebook 的群组页面的群组页面的应用程序。有人可以在页面上发帖,并且该帖子可以有回复。由于它们具有非常相似的属性,因此帖子和回复被合并到同一个 STI 表中

class Page < ActiveRecord::Base
  has_many :posts
  has_many :replies, through: :posts
end

class BasePost < ActiveRecord::Base
  ...
end

class Post < BasePost
  belongs_to :page
  ...
end

class Reply < BasePost
  belongs_to :post
  ...
end

我希望能够得到page.posts_and_replies

为了找到页面的“最喜欢的帖子或回复”之类的内容,我们必须将帖子和回复结合起来,以便我们可以获得结果集,例如:

top_messages = page.posts_and_replies.order_by('total_likes DESC').limit(10)

将帖子和回复放在一起很棘手

posts_and_replies尽管我们通常需要基于单个连接查询它们,但要作为单个结果集进行排序:

class Page < ActiveRecord::Base
  has_many :posts
  has_many :replies, through: :posts
  has_many :posts_and_replies, class_name: 'BasePost'
end

# ideally we could then do queries such as
most_recent_messages = @page.posts_and_replies.order('created_at DESC').limit(10)

我们不能这样做,因为page_id只存在于posts. “回复”仅通过他们所属的帖子引用该页面。所以这个加入给了我们帖子而不是回复。


可能的解决方案:

对数据进行非规范化...

我们可以将其复制page_id到回复和帖子上,但如果可能的话,我真的很想避免这样做,因为非规范化的数据往往会以泪水告终。

使用自定义finder_sql

我们可以使用自定义查找器 sql 来分别获取帖子和回复,然后将它们组合起来:

class Page < ActiveRecord::Base
  has_many :posts
  has_many :replies
  has_many :posts_and_replies, class_name: 'BasePost', finder_sql: Proc.new{ 
    "select base_posts.* from base_posts 
      where page_id = #{self.id}
    UNION
      select 
      base_posts.*
      from base_posts left join base_posts as head_posts
      on base_posts.post_id = head_posts.id
      where head_posts.page_id = #{self.id}"
  }
end

自定义 finder_sql 有效,但不适用于关联扩展

上面的 finder_sql 实际上确实有效,但是没有一个关联扩展有效。每当我尝试使用关联扩展(例如.where)时,它都会退回到内置的 finder_sql:

这有效

 Page.find(8).posts_and_replies
 => select base_posts.* from base_posts 
      where page_id = 8
    UNION
      select 
      base_posts.*
      from base_posts left join base_posts as head_posts
      on base_posts.post_id = head_posts.id
      where head_posts.page_id = 8

*然而,这又回到了不正确的 finder_sql*

  Page.find(8).posts_and_replies.where('total_likes > 1')
  => select "base_posts".* from "base_posts" where "base_posts"."page_id" = 8 
     and (total_likes > 12)

由于某种原因,关联扩展没有使用正确的finder_sql

如何让关联扩展尊重声明的 finder_sql?

问题似乎归结为未正确使用 finder_sql 的关联扩展。有什么办法可以强制执行吗?

4

1 回答 1

0

解决此问题的最简单方法可能是使用一些命名范围,并完全避免关联。这是一种有点笨拙的方法,但可能会给你你正在寻找的东西——我不确定 Rails 关联是否真的会给你你想要的行为。另外,请注意下面我将使用 ActiveRecord 的安全参数插值 - 你永远不应该自己将字符串插值到 SQL 语句中 - 它会在你不注意的时候召唤邪恶的灵魂来感染你的数据库。

首先,您将在 BasePost 模型中定义一个命名范围,该范围执行以下操作:

class BasePost < ActiveRecord::Base

  scope :popular_posts_for_page, lambda { |page_id|
    where("base_posts.page_id = ? OR parent_posts.page_id = ?", page_id, page_id).
     joins("left outer join base_posts parent_posts on parent_posts.id = base_posts.post_id").
     order("total_likes DESC")
  }

end

这里还有一个好处是你现在还不会得到一个实际的列表,只有一个范围代理。如果你愿意,你现在可以将你的限制或任何你想要的语句链接到它上面。

如果您希望它可以在您的 Page 模型上访问,一种方法是:

class Page < ActiveRecord::Base

  def popular_posts_and_replies
    BasePost.popular_posts_for_page(self.id)
  end

end

由于您之前将其定义为命名范围,并且该范围将返回一个范围对象,而不是实际的结果列表 - 它与关联非常接近,以至于差异应该是不可见的。例如:

my_page.popular_posts_and_replies.limit(10)

应该完全按照你的预期去做。

对于我的 SQL 中的任何错误,我深表歉意。

于 2013-05-21T22:36:18.583 回答