Получение списка из различных моделей в Ruby on Rails
В последнее время мне дважды задавался вопрос о том вывести в одном списке несколько Rails-моделей так, чтобы их можно было нормально фильтровать и листать.
Здесь я опишу одно из возможных решений этой проблемы.
Предположим, что у нас в приложении есть модели Post и Question и мы хотим на главной странице их вперемежку упорядоченными по дате создания. Какие возможны решения?
- Можно загрузить списки обоих моделей большей длины и потом их объединить в Ruby-коде. В этом случае основная проблема возникает с организацией постраничного вывода, поскольку мы не знаем сколько моделей каждого типа окажется на странице. И если для первой страницы еще можно загрузить из базы данных по максимальному числу моделей (сколько помещается на странице) каждого типа, то для последующих страниц необходимо еще задавать нижнюю границу – а это намного сложнее.
- Можно использовать наследование и хранить обе модели в одной таблице. Этот способ хорош, но не всегда применим (например, модели могут быть слишком разными, чтобы их хранить в одной таблице или схема базы данных уже может быть зафиксирована). Кроме того, в случае если в разных местах должны выводится различные пары моделей это может привести к тому, что все модели приложения придется поместить в одну таблицу.
- Можно использовать операцию UNION и особенности загрузки моделей в Rails. Об этом и пойдет речь ниже. Основным минусом этого подхода является необходимость использования метода find_by_sql с достаточно громоздким запросом для загрузки моделей.
Итак, метод основан на использовании операции UNION. Она поддерживается основными БД, используемыми при разработке:
- Sqlite – http://www.sqlite.org/syntaxdiagrams.html#compound-operator
- MySQL – http://dev.mysql.com/doc/refman/5.0/en/union.html
- PostgreSQL – http://www.postgresql.org/docs/8.3/interactive/queries-union.html
Суть операции состоит в том, что результаты нескольких запросов объединяются в один, к которому могут быть применены 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
