Как сделать вложенные комментарии в Ruby on Rails? Часть первая: Модели.
декабря 26, 2010 | Published in Ruby on Rails, Ruby on Rails 3 | 12 Comments
На YouTube вы могли заметить замечательную систему комментирования, где вы можете комментировать не только видео, но и отвечать на комментарии других людей, что по-сути является комментированием других комментариев.
Если вам нравится такая система комментирования, и вы хотите добавить что-то похожее в свое приложение, то вы могли бы создать две таблицы, например: PostComment и CommentComment или что-то подобное. Однако лучшим вариантом будет использование полиморфных ассоциаций. Полиморфизм позволяет комментариям принадлежать как посту, так и другим комментариям и еще чему угодно. «Полиморфные ассоциации» вероятно испугали вас, однако использовать их проще, чем вы могли предположить.
Внимание! В статье описывается то, как это делается в Ruby on Rails 3. Исходные коды вы можете скачать на .
Итак, давайте перейдем от слов к делу и создадим модель Post:
rails g model post title:string body:text
rake db:migrate
Мы делаем модель очень простой и не заморачиваемся с еще какими-либо вещами типа аутентификации пользователя и т.д., так как, это лишь пример демонстрирующий то, как можно создать систему комментирования с возможностью комментировать имеющиеся комментарии. Наша модель Post замечательна в своей простоте, и мы не нуждаемся в дальнейшем изменении файла миграции.
Теперь давайте создадим модель Comment для наших комментариев:
rails g model comment title:string body:text commentable_id:integer commentable_type:string
Здесь начинается магия и волшебство! Мы не указываем нашим комментариям «вы должны принадлежать(belongs_to) посту!» потому, что комментарии могут принадлежать чему угодно. Поэтому мы не можем добавить поле «post_id». Вместо этого мы используем специальное имя для нашей ассоциации: «commentable», что означает, что комментарии могут принадлежать всему, что может комментироваться (логично, не правда ли?). Мы добавляем поле commentable_id для хранения ID объекта которому принадлежит текущий комментарий. Мы также добавляем поле commentable_type для хранения типа объекта которому принадлежит комментарий, например «Post», «Comment», что угодно еще. Поле commentable_type необходимо потому, что комментарии и посты могут иметь одинаковые ID и мы не сможем понять чему же принадлежит комментарий, другому комментарию или посту?
Прежде чем мы добавим комментарии в базу данных, нам необходимо сделать небольшую правку файла миграции:
def self.up create_table :comments do |t| t.string :title t.string :body t.integer :commentable_id t.string :commentable_type t.timestamps end add_index :comments, [:commentable_id, :commentable_type] end
Мы добавили одиночный индекс в комбинацию полей commentable, который ускорит работу нашего приложения. Теперь мы можем запустить миграцию и применить наши изменения:
rake db:migrate
А теперь давайте установим ассоциации в наших моделях начиная с Post:
class Post < ActiveRecord::Base has_many :comments, :as => :commentable end
Обычно, если вы указываете модели Post на то, что она должена содержать(has_many) комментарии, то Post проверяет таблицу комментариев на наличие в ней поля post_id. Это невозможно, поскольку мы используем полиморфизм и поля post_id попросту нет. Поэтому мы сообщим Post имя нашей полиморфной ассоции: «commentable».
Теперь добавим ассоциации вв немного более сложную модель комментариев:
class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true has_many :comments, :as => :commentable end
Сначала мы установили часть «belongs_to» полиморфизма. Наш комментарий принадлежит «commentable» — так мы назвали нашу полиморфную ассоциацию. Мы также указываем на полиморфность при помощь параметра polimorphic: true. Иначе, Ruby on Rails будет безуспешно искать модель Commentable.
Вы, наверное, думаете, что работать с полиморфными ассоциациями будет сложнее чем с обычными? Хорошая новость в том, что сложная часть работы закончилась! Далее все работает абсолютно так же как и с обычными belongs_to и has_many не использующими полиморфные ассоциации. Давайте поиграемся с нашим кодом в консоли Ruby on Rails:
post = Post.create :title => 'First Post' => # comment = post.comments.create :title => 'First Comment' => # reply = comment.comments.create :title => 'First Reply' => #
Мы можем добавить комментарий к нашему посту, а к этому комментарию еще один комментарий. Фреймворк Ruby on Rails работает заполняя commentable_id и commentable_type точно так же как если бы заполнял post_id в случае, когда комментарий может принадлежать только посту.
Автор оригинала статьи пообещал написать вторую часть, где он расскажет как работать со вложенными комментариями в контроллерах и представлениях. Ждемся! ;-)
Оригинал статьи на английском:
Нашли ошибки или неточности в тексте? — Сообщите пожалуйста!
Лучшая благодарность автору — ваши комментарии и пиар блога в интернет. Буду благодарен за ссылки. ;-)
декабря 26, 2010 at 10:06 (#)
Спасибо, полезная статья!) Ждем вторую часть)
декабря 27, 2010 at 01:32 (#)
отличное применение полиморфных связей, наверно самый простой и один из самых распространенных вариантов их использования
декабря 27, 2010 at 23:13 (#)
а почему не воспользоваться просто древовидным хранением комментариев? Я для этого использую awesome_nested_set (). Про сам способ хранения дерева комментариев рекомендую почитать вот эту статью:
А главный недостаток метода, который тут описан — нельзя получить одним запросом сразу все комменты к посту вместе со всеми дочерними комментами.
декабря 27, 2010 at 23:33 (#)
yas, вы абсолютно правы. Недостаток данного способа в том, что для каждого commentable будет полностью просматриваться вся таблица комментариев.
Для приложения с небольшим количеством комментариев это не страшно, но мало-мальски нагруженный сервис может быть сильно загружен. Здесь может спасти положение кэширование, но помимо этого я имею еще несколько идей, которые в силу своего малого опыта не могу реализовать:
1. Счетчик на количество вложенных комментариев 1 го уровня у каждого commentable. Тогда для постов и комментариев без ответов и вовсе не будет производиться поиск принадлежащих им комментариев, а для остальных поиск будет завершен при нахождении всех вложенных комментариев.
2. Это наверное тупой способ, но все же: мы можем хранить для каждого commentable специальную строку, где будут записываться все вложенные комментарии 1го уровня по отношению к данному commentable. Что-то типа:
Мы просто берем этот хэш для каждого commentable и по нему выбираем все вложенные комментарии и, например, файлы или что-либо еще. Однако это противоречит принципам проектирование БД, если я не ошибаюсь. Если хватит энтузиазма постараюсь более подробно рассмотреть данный вопрос.
И да, спасибо за ссылки!
Кстати, в планах есть создание одной очень сложной в плане отношений между моделями системы. Опять-таки, если руки дойдут то обязательно поделюсь кодом, по крайней мере техникой реализации с реальными примерами.
декабря 29, 2010 at 22:28 (#)
Админчик,ты — самый-самый.Ты это знаешь …
декабря 29, 2010 at 22:55 (#)
Спасибо, Ксюшенька =)
декабря 30, 2010 at 00:23 (#)
На счет второго варианта, это будет называться cache ;) Вот неплохая гемка
декабря 30, 2010 at 00:34 (#)
Locke23rus, я почему-то считал, что кэш, это хранение частоиспользуемых, неизменяемыхданных в базе или на диске с целью экономии вычислительного ресурса. Ну типа мы не тратим время на рендеринг страниц, а выдаем уже готовые закэшированные на диск или в БД html-файлы/html-код. Внесение индексов принадлежащих commentable элементов за кэширование не считаю, хотя я могу ошибаться. Вы то уверенны в своих словах? Просто не хочется разбираться в вопросах терминологии =)
декабря 30, 2010 at 10:40 (#)
> yas, вы абсолютно правы. Недостаток данного способа в том, что для каждого commentable будет полностью просматриваться вся таблица комментариев.
С чего вдруг будет вся таблица просматривать? У метода acts_as_nested_set есть такой замечательный параметр как :scope. Там указываете например поле с id материла, или например если у вас несколько типов материала могут иметь комменты и в таблице комментариев у вас полиморфная связь, то можно и массив передать. Затем при создании самого материала, который потом будут комментить в after_save делаете создание рутового коммента, который потом отображаться нигде не будет, а новые комменты будут типа как ответы на этот коммент. И всё.
Звучит сложно, но на самом деле всё проще, чем тут может показаться. И одним запросом получаются все комменты к этому материалу. Минусом этого метода является то, что при записи коммента апдейтится целая пачка комментов из этого дерева (считай для этого материала), у них обновляются rgt и lgt. Но в случае комментариев помоему чаще их надо получить из базы, чем добавить новый. Поэтому в сравнении с вашим методом, когда у вас при каждом обращении к странице будет куча запросов, этот метод лучше.
ps я вот не так давно начинал писать плагинчик acts_as_commentable. Там пока организовано хранение данных, но со временем я и хелперы для удобного вывода туда наверное тоже добавлю. просто на моём проекте мне пока что надо было только со старой базы проимпортировать в новую и поэтому написал только хранение. Может кому пригодится в качестве точки старта
мая 25, 2012 at 22:15 (#)
Помогите пожалуйста! Как сделать так, чтобы на отдельной странице блога показывалось последние 5 комментариев?
мая 28, 2012 at 21:03 (#)
Таня, вот так: Comment.order(‘id desc’).limit(5)
октября 29, 2013 at 19:13 (#)
[i]Помогите пожалуйста! Как сделать так, чтобы на отдельной странице блога показывалось последние 5 комментариев?[/i]
Comment.last 5