如何使用ActiveRecord将STI表中的不同类组合成单个结果集?

Pet*_*xey 5 activerecord finder-sql sti ruby-on-rails-3.2

我们正在构建一个应用程序来创建类似于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
Run Code Online (Sandbox Code Playgroud)

我希望能得到 page.posts_and_replies

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

top_messages = page.posts_and_replies.order_by('total_likes DESC').limit(10)
Run Code Online (Sandbox Code Playgroud)

一起获得帖子和回复很棘手

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)
Run Code Online (Sandbox Code Playgroud)

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


可能的解决方案:

归一化数据......

我们可以复制page_id到回复以及帖子,但我真的想避免这样做,如果可能的话,因为非规范化数据往往以泪水结束.

使用自定义 finder_sql

我们可以使用自定义finder 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
Run Code Online (Sandbox Code Playgroud)

自定义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
Run Code Online (Sandbox Code Playgroud)

*但是这会回到错误的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)
Run Code Online (Sandbox Code Playgroud)

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

如何使关联扩展尊重所述的finder_sql?

问题似乎已经归结为关联扩展没有正确使用finder_sql.有没有办法强制执行此操作?

Tre*_*eyE 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
Run Code Online (Sandbox Code Playgroud)

这里的另一个好处是,您还不会返回实际的列表,而只会返回范围代理。如果您愿意,您现在可以将您的限制或您想要的任何声明链接到该限制上。

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

class Page < ActiveRecord::Base

  def popular_posts_and_replies
    BasePost.popular_posts_for_page(self.id)
  end

end
Run Code Online (Sandbox Code Playgroud)

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

my_page.popular_posts_and_replies.limit(10)
Run Code Online (Sandbox Code Playgroud)

应该完全按照您的预期行事。

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