如果我在 3 个模型之间有一个多态关联:
评论
belongs_to :book, :class_name => 'Book', :foreign_key => 'ref_id', conditions: "comments.ref_type = 'Book'"
belongs_to :article, :class_name => 'Article', :foreign_key => 'ref_id', conditions: "comments.ref_type = 'Article'"
belongs_to :ref, :polymorphic => true
对于给定的评论列表,如何从
Title
和 Book
模型的 Article
列中选择不同的值?
例如,如果我必须列出某个时间段内已给出评论的书籍和文章的标题,那么我该怎么做?我可以很容易地选择评论列表,但我如何从
Book
和Article
中选择相关的独特标题?
例如:
Book
+--------------+
| Id | Title |
+----+---------+
| 1 | 'Book1' |
| 2 | 'Book2' |
| 3 | 'Book3' |
+--------------+
Article
+-----------------+
| Id | Title |
+----+------------+
| 1 | 'Article1' |
| 2 | 'Article2' |
+-----------------+
Comments
+--------------------------------------+
| Id | comment | ref_id | ref_type |
+----+------------+--------+-----------+
| 1 | 'comment1' | 1 | Book |
| 2 | 'comment2' | 1 | Book |
| 3 | 'comment3' | 1 | Article |
| 4 | 'comment4' | 3 | Book |
+--------------------------------------+
我需要标题列表是
'Book1'
, 'Book3'
, 'Article1'
.
Comment.all.includes(:ref).map { |comment| comment.ref.title }.uniq
获取所有评论,预先加载它们的引用,并返回包含唯一标题的数组。预加载部分并不是绝对必要的,但它可能会表现得更好。 执行了 3 个查询,一个用于评论,一个用于每种引用。 您可以将 all 替换为任何范围。 请注意,这会获取所有评论及其引用,并使用 ruby 将其转换为数组,而不是 SQL。这非常有效,但性能可能会受到影响。通常最好使用 distinct 来获取唯一值并使用 pluck 来获取这些值的数组。
这种方法适用于各种参考。因此,如果我们引入第三种引用,例如Post,会自动包含在这个query中
class Comment < ApplicationRecord
belongs_to :book, class_name: 'Book', foreign_key: 'ref_id'
belongs_to :article, class_name: 'Article', foreign_key: 'ref_id'
belongs_to :ref, polymorphic: true
scope :with_ref_titles, lambda {
book_titles = select('comments.*, books.title').joins(:book)
article_titles = select('comments.*, articles.title').joins(:article)
union = book_titles.union(article_titles)
comment_table = arel_table
from(comment_table.create_table_alias(union, :comments).to_sql)
}
end
此范围使用 arel 和子查询的 UNION 来获取标题。 它基本上是将 ref 的标题添加到评论对象中。由于范围应该是可链接的,因此它返回一个 ActiveRecord 关系而不是一个数组。要获得不同的标题,请附加 distinct.pluck(:title).
comments = Comment.with_ref_titles
comments.distinct.pluck(:title) # => ["Article1", "Book1", "Book3"]
comments.where('title LIKE "Book%"').distinct.pluck(:title) # => ["Book1", "Book3"]
此范围生成的 SQL 查询如下所示:
SELECT DISTINCT "title" FROM ( SELECT comments.*, books.title FROM "comments" INNER JOIN "books" ON "books"."id" = "comments"."ref_id" UNION SELECT comments.*, articles.title FROM "comments" INNER JOIN "articles" ON "articles"."id" = "comments"."ref_id" ) "comments"
使用 rails 5.1.2 和 sqlite 进行测试。
我认为最简单的方法是在不直接使用
Arel
API 或遍历数组的情况下,在 .titles_for_comments
和 Book
上定义 Article
范围,让您在给定集合时从每个表中选择不同的标题comments
。然后为 .distinct_titles
定义一个 Comment
范围,它同时使用 [Book|Article].titles_for_comments
。例如,下面的模型和范围定义让您可以通过调用 Comment::ActiveRecord_Relation
为任何给定的
Comment.distinct_titles
实例找到不同的书籍和文章标题
class Book < ActiveRecord::Base
has_many :comments, as: :ref
def self.titles_for_comments(comment_ids)
joins(:comments).where(comments: { id: comment_ids }).distinct.pluck(:title)
end
end
class Article < ActiveRecord::Base
has_many :comments, as: :ref
def self.titles_for_comments(comment_ids)
joins(:comments).where(comments: { id: comment_ids }).distinct.pluck(:title)
end
end
class Comment < ActiveRecord::Base
belongs_to :ref, polymorphic: true
def self.distinct_titles
comment_ids = ids
Article.titles_for_comments(comment_ids) + Book.titles_for_comments(comment_ids)
end
end
您可以下载此 Gist 并使用
ruby polymorphic_query_test.rb
运行它,测试应该通过 https://gist.github.com/msimonborg/907eb513fdde9ab48ee881d43ddb8378
试试这个
titles_from_books = Comment.joins('INNER JOIN books on comments.ref_id = books.id').where('comments.ref_type = ?','Book').pluck('books.title')
titles_from_articles = Comment.joins('INNER JOIN articles on comments.ref_id = article.id').where('comments.ref_type = ?','Article').pluck('articles.title')
final_titles = (titles_from_books + titles_from_articles).uniq
我建议你看看这个教程:https://web.archive.org/web/20180928102044/http://karimbutt.github.io:80/blog/2015/01/03/step-by-step-导轨中的多态关联指南/ 看看它是否适合您。
如果您控制模型代码,我会为
belong_to
一个通用的可注释对象设置注释。例子:
class Comment < ActiveRecord::Base
belong_to :commentable, :polymorphic => true
end
和
class Book < ActiveRecord::Base
has_many :comments, as: commentable
end
class Article < ActiveRecord::Base
has_many :comments, as: commentable
end
然后给出你可以运行的任何一组评论
Comments.each do |comment|
comment.commentable.title
end.uniq
目前,仅仅获得标题似乎需要做很多工作,但如果你坚持这个项目,我希望书籍和文章能够共享 lot 此类代码,以及可能添加其他可注释对象将来。总体而言,拥有一个通用对象将节省大量工作。