Как я вырвать отличное значение столбца из связанной модели в полиморфной ассоциации?

Если у меня есть полиморфная связь между 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'.

Ответ 1

Простой способ:

Comment.all.includes(:ref).map { |comment| comment.ref.title }.uniq

Извлеките все комментарии, загрузите их ссылки и верните массив, содержащий уникальные заголовки. Желательная загрузочная часть не является строго необходимой, но она может работать лучше. Выполняются 3 запроса, один для комментариев и один для каждого типа ссылок. Вы можете заменить все на любую область. Обратите внимание, что это извлекает все комментарии и их refs и использует ruby, чтобы превратить его в массив, а не SQL. Это работает отлично, но производительность может пострадать. Обычно лучше использовать разные, чтобы получить уникальные значения и вырвать, чтобы получить массив этих значений.

Этот подход работает со всеми видами ссылок. Поэтому, если мы представим третий вид ref, например. Post, он будет автоматически включен в этот запрос.

Многоразовый способ:

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

В этой области используются isl и 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"

Протестировано рельсами 5.1.2 и sqlite.

Ответ 2

Я думаю, что самый простой способ без непосредственного использования API Arel или итерации над массивами - определить области .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

Ответ 3

Попробуйте этот

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

Ответ 4

Я рекомендую вам посмотреть на этот учебник: http://karimbutt.github.io/blog/2015/01/03/step-by-step-guide-to-polymorphic-associations-in-rails/ и посмотреть, насколько это возможно для вас.

Если вы управляете кодом модели, я бы установил комментарии, чтобы принадлежать к универсальному комментаризуемому объекту. Пример:

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

Это может показаться большой работой, чтобы просто получить титулы на данный момент, но если вы придерживаетесь этого проекта, я ожидаю, что книги и статьи будут делиться большим количеством кода этого типа, а также, возможно, добавлять другие комментарии в будущем. В целом наличие общего объекта сэкономит много работы.