Как сделать вложенные комментарии в Ruby on Rails? Часть первая: Модели.

декабря 26, 2010  |  Published in Ruby on Rails, Ruby on Rails 3  |  12 Comments

ruby on rails polymorphic associationsНа 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 в случае, когда комментарий может принадлежать только посту.

Автор оригинала статьи пообещал написать вторую часть, где он расскажет как работать со вложенными комментариями в контроллерах и представлениях. Ждемся! ;-)

Оригинал статьи на английском:

Нашли ошибки или неточности в тексте? — Сообщите пожалуйста!

Лучшая благодарность автору — ваши комментарии и пиар блога в интернет. Буду благодарен за ссылки. ;-)

Tags: ,

Responses

  1. yerlan_k says:

    декабря 26, 2010 at 10:06 (#)

    Спасибо, полезная статья!) Ждем вторую часть)

  2. rsludge says:

    декабря 27, 2010 at 01:32 (#)

    отличное применение полиморфных связей, наверно самый простой и один из самых распространенных вариантов их использования

  3. says:

    декабря 27, 2010 at 23:13 (#)

    а почему не воспользоваться просто древовидным хранением комментариев? Я для этого использую awesome_nested_set (). Про сам способ хранения дерева комментариев рекомендую почитать вот эту статью:

    А главный недостаток метода, который тут описан — нельзя получить одним запросом сразу все комменты к посту вместе со всеми дочерними комментами.

  4. admin says:

    декабря 27, 2010 at 23:33 (#)

    yas, вы абсолютно правы. Недостаток данного способа в том, что для каждого commentable будет полностью просматриваться вся таблица комментариев.

    Для приложения с небольшим количеством комментариев это не страшно, но мало-мальски нагруженный сервис может быть сильно загружен. Здесь может спасти положение кэширование, но помимо этого я имею еще несколько идей, которые в силу своего малого опыта не могу реализовать:

    1. Счетчик на количество вложенных комментариев 1 го уровня у каждого commentable. Тогда для постов и комментариев без ответов и вовсе не будет производиться поиск принадлежащих им комментариев, а для остальных поиск будет завершен при нахождении всех вложенных комментариев.

    2. Это наверное тупой способ, но все же: мы можем хранить для каждого commentable специальную строку, где будут записываться все вложенные комментарии 1го уровня по отношению к данному commentable. Что-то типа:

    {comments_id: [1,2,3,5,12], files_id: [1,5,7]}

    Мы просто берем этот хэш для каждого commentable и по нему выбираем все вложенные комментарии и, например, файлы или что-либо еще. Однако это противоречит принципам проектирование БД, если я не ошибаюсь. Если хватит энтузиазма постараюсь более подробно рассмотреть данный вопрос.

    И да, спасибо за ссылки!

    Кстати, в планах есть создание одной очень сложной в плане отношений между моделями системы. Опять-таки, если руки дойдут то обязательно поделюсь кодом, по крайней мере техникой реализации с реальными примерами.

  5. says:

    декабря 29, 2010 at 22:28 (#)

    Админчик,ты — самый-самый.Ты это знаешь …

  6. admin says:

    декабря 29, 2010 at 22:55 (#)

    Спасибо, Ксюшенька =)

  7. says:

    декабря 30, 2010 at 00:23 (#)

    На счет второго варианта, это будет называться cache ;) Вот неплохая гемка

  8. admin says:

    декабря 30, 2010 at 00:34 (#)

    Locke23rus, я почему-то считал, что кэш, это хранение частоиспользуемых, неизменяемыхданных в базе или на диске с целью экономии вычислительного ресурса. Ну типа мы не тратим время на рендеринг страниц, а выдаем уже готовые закэшированные на диск или в БД html-файлы/html-код. Внесение индексов принадлежащих commentable элементов за кэширование не считаю, хотя я могу ошибаться. Вы то уверенны в своих словах? Просто не хочется разбираться в вопросах терминологии =)

  9. says:

    декабря 30, 2010 at 10:40 (#)

    > yas, вы абсолютно правы. Недостаток данного способа в том, что для каждого commentable будет полностью просматриваться вся таблица комментариев.

    С чего вдруг будет вся таблица просматривать? У метода acts_as_nested_set есть такой замечательный параметр как :scope. Там указываете например поле с id материла, или например если у вас несколько типов материала могут иметь комменты и в таблице комментариев у вас полиморфная связь, то можно и массив передать. Затем при создании самого материала, который потом будут комментить в after_save делаете создание рутового коммента, который потом отображаться нигде не будет, а новые комменты будут типа как ответы на этот коммент. И всё.

    Звучит сложно, но на самом деле всё проще, чем тут может показаться. И одним запросом получаются все комменты к этому материалу. Минусом этого метода является то, что при записи коммента апдейтится целая пачка комментов из этого дерева (считай для этого материала), у них обновляются rgt и lgt. Но в случае комментариев помоему чаще их надо получить из базы, чем добавить новый. Поэтому в сравнении с вашим методом, когда у вас при каждом обращении к странице будет куча запросов, этот метод лучше.

    ps я вот не так давно начинал писать плагинчик acts_as_commentable. Там пока организовано хранение данных, но со временем я и хелперы для удобного вывода туда наверное тоже добавлю. просто на моём проекте мне пока что надо было только со старой базы проимпортировать в новую и поэтому написал только хранение. Может кому пригодится в качестве точки старта

  10. Таня says:

    мая 25, 2012 at 22:15 (#)

    Помогите пожалуйста! Как сделать так, чтобы на отдельной странице блога показывалось последние 5 комментариев?

  11. admin says:

    мая 28, 2012 at 21:03 (#)

    Таня, вот так: Comment.order(‘id desc’).limit(5)

  12. says:

    октября 29, 2013 at 19:13 (#)

    [i]Помогите пожалуйста! Как сделать так, чтобы на отдельной странице блога показывалось последние 5 комментариев?[/i]

    Comment.last 5

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.