Получение списка из различных моделей в Ruby on Rails

В последнее время мне дважды задавался вопрос о том вывести в одном списке несколько Rails-моделей так, чтобы их можно было нормально фильтровать и листать.
Здесь я опишу одно из возможных решений этой проблемы.

Предположим, что у нас в приложении есть модели Post и Question и мы хотим на главной странице их вперемежку упорядоченными по дате создания. Какие возможны решения?

  • Можно загрузить списки обоих моделей большей длины и потом их объединить в Ruby-коде. В этом случае основная проблема возникает с организацией постраничного вывода, поскольку мы не знаем сколько моделей каждого типа окажется на странице. И если для первой страницы еще можно загрузить из базы данных по максимальному числу моделей (сколько помещается на странице) каждого типа, то для последующих страниц необходимо еще задавать нижнюю границу – а это намного сложнее.
  • Можно использовать наследование и хранить обе модели в одной таблице. Этот способ хорош, но не всегда применим (например, модели могут быть слишком разными, чтобы их хранить в одной таблице или схема базы данных уже может быть зафиксирована). Кроме того, в случае если в разных местах должны выводится различные пары моделей это может привести к тому, что все модели приложения придется поместить в одну таблицу.
  • Можно использовать операцию UNION и особенности загрузки моделей в Rails. Об этом и пойдет речь ниже. Основным минусом этого подхода является необходимость использования метода find_by_sql с достаточно громоздким запросом для загрузки моделей.

Итак, метод основан на использовании операции UNION. Она поддерживается основными БД, используемыми при разработке:

Суть операции состоит в том, что результаты нескольких запросов объединяются в один, к которому могут быть применены OFFSЕT и LIMIT.

Таким образом, для того, чтобы вывести в одном списке модели Post и Question мы можем сделать что-то вроде:


SELECT * FROM posts UNION SELECT * FROM questions

Однако, не все так просто, есть два подводных камня:

  • Необходимо, чтобы Rails как-то распознали к какому классу относится каждая запись.
  • Необходимо, чтобы количество столбцов в каждом из объединяемых запросов совпадало.

Сначала решим первую проблему. Распознавание класса записи будет производится за счет механизма, используемого в Rails для загрузки моделей с наследованием, а именно наличия поля type. Чтобы корректно это использовать, добавим в каждую модель поле type и установим ему в качестве значения по умолчанию имя класса модели.
Тогда код миграция для создания моделей может выглядеть следующим образом:


create_table :questions do |t|
  t.text :text
  t.string :type, :null => false, :default => 'Question'

  t.timestamps
end

create_table :posts do |t|
  t.string :title
  t.text :description
  t.string :type, :null => false, :default => 'Post'

  t.timestamps
end

В процессе загрузки класс будет определен на основе значения поля type в запросе, что и необходимо в нашем случае. Чтобы убедится, что это действительно так можно заглянуть в исходный код Rails – в методы find_by_sql и instantiate

Теперь перейдем ко второй проблеме. Она решается достаточно просто – необходимо явно перечислить все необходимые столбцы и дополнить их значениями NULL:


SELECT id, type, created_at, updated_at, title, description, NULL AS text FROM posts
UNION SELECT id, type, created_at, updated_at, NULL AS title, NULL AS description, text FROM questions
ORDER BY created_at DESC

Все, практически все сделано. Можно еще добавить LIMIT и OFFSET для постраничного вывода и написать код в контроллере и виде:


class IndexController < ApplicationController

  def index
    @models = Post.find_by_sql("SELECT id, type, created_at, updated_at, title, description, NULL AS text FROM posts UNION SELECT id, type, created_at, updated_at, NULL AS title, NULL AS description, text FROM questions ORDER BY created_at DESC")
  end

end

<% @models.each do |m|%>
  <%=m.inspect%><br />
<%end %>

Все! Для тех, кому интересно посмотреть это в действии я выложил архив с проектом для Rails3

 Подписаться на RSS

 #  #  #  #  #  #  #  #  #  #

blog comments powered by Disqus