Django: Древовидные комментарии.
Django 2008-03-24 Dovbush PavelВступление: Противников велосипедостроения просьба сразу удалиться сюда: django-threadedcomments или прокрутить до списка ссылок на алгоритмы в конце статьи. Опытные джанговоды также не найдут здесь ничего интересного.
Одной из наиболее важных для меня фишек являются древовидные комментарии. Они позволяют легче следить за ходом обсуждения, что по-моему очень важно. Их отсутствие или кривость реализации в open-source блог-движках стало одной из причин написать свой.
Постановка задачи
- Отсутствие регистрации (зачем мучить пользователя? будут проблемы со спамом - будем думать).
- Запоминание данных комментатора в cookie.
- Маркер авторитетного комментария и комментария автора. (При большом количестве комментариев лично я всегда выискиваю комментарии автора и других авторитетных людей.)
- Favicon сайта комментатора в качестве его аватарки.
- Выборка всех комментариев к статье одним запросом. Добавление не должно перелопачивать все комментарии.
- Валидация, просмотр и добавление без перезагрузки страницы. Корректная работа без JavaScript
- PS. Не забыть где-нибудь использовать Django :)
Для начала, реализуем все на Django, потом прикрутим JavaScript.
Модель
Модель комментария:
class Comment(models.Model): article=models.ForeignKey(Article) parent=models.ForeignKey('self', blank=True, null=True, related_name='child_set') author_name=models.CharField(max_length=32) author_email=models.EmailField() author_url=models.URLField(blank=True) text=models.TextField() pub_date=models.DateTimeField('date published', default=datetime.now) admin_comment=models.BooleanField(default=False)
Модель использует стандартные поля Django. Древовидность реализуется ссылкой на родителя.
Может быть пользователя стоило вынести в отдельную модель, но для простоты пока оставлю так. Если нужна регистрация - можно использовать стандартного юзера Django.
Присмотревшись чуть внимательнее к реализации стандартного поля URLField, я понимаю, что оно мне не подходит: при валидации сервер запрашивает введенный урл и смотрит, что ответ не 404. Не хочу. Да и если у человека с сервером проблемы, что ему нельзя написать комментарий?
Однако валидация нужна, чтобы не получались ссылки вида http://dpp.su/blog/ivan.ivanovich.googlepages.com. По этому пишем свой URLField:
class SimpleURLField(CharField): def pre_save(self, model_instance, add): url=getattr(model_instance, self.attname, '') if add and url and not url.startswith('http://'): url='http://%s'%url setattr(model_instance, self.attname, url) return url return CharField.pre_save(self, model_instance, add)
Теперь пришло время задуматься выборкой комментариев из базы. Рекурсивные запросы делать бы очень не хотелось. Хотя, как говорят тут, если убрать рекурсию в хранимую процедуру, то скорость MySQL будет приемлемой. Может быть и так, но мне такой подход не нравится.
Перед нами классическая задача: хранение древовидных структур в БД. Но, все-таки, вооружившись здравым (или не очень) смыслом, начинаем изобретать велосипед.
На баше как раз сегодня цитата очень в тему: гениальную фразу сейчас услышал на семинаре по работе с zend framework - "Можно не изобретать велосипед, а ПОГНУТЬ уже существующий"
Первая мысль: если хранить полный путь до корня, то правильная очередность комментариев достигается простой сортировкой по этому полю. Однако это накладывает ограничение на количество комментариев и глубину дерева. Но давайте посмотрим на это ограничение внимательней.
Если использовать varchar(255) и описывать каждый узел дерева тремя цифрами, то мы получим вложенность 85 и тысячу комментариев на каждом уровне. Если четырьмя - 64 и 10000. Для не очень популярного ресурса вполне хватит.
Если поискать информацию в инете можно заметить, что этот способ почти всегда предлагают первым. Просто и работает быстрее всего. Но только в том случае, если нам не важно ограничение этого метода. Сначала я думал написать сравнение наиболее популярных методов, но начав писать передумал: уж слишком много подобных сравнений. Да и Django оказывается не при чем.
Вернемся к предложенному решению. Если идея вдруг не понятна, приведу пример хранения коментариев в табице. Коментарии:
А вот как они будут храниться в базе:
Отсортировав таблицу по полю path
мы получаем список комментариев в нужном нам порядке. Теперь нужно написать функцию генерирующую значение поля path
. Значение генерируется очень просто - берется значение родителя и дописывается id добавляемого объекта.
path=('%s-%03d'%(parent.path, self.id, ))[:255]
Однако, при добавлении объекта мы не можем узнать его id, что приведет к дополнительному UPDATE добавленного объекта после его сохранения. Также необходимо задуматься о следующем: если использовать id для генерации path, то ограниечение на количество кометариев при этом методе получается общим для всего сайта. 10000 коментариев к статье - это много, а ко всему сайту - не очень. Поэтому использовать id при генерации пути плохо. Придется создать дополнительную колонку в таблице для хранения относительного id.
Используя подход, предлагаемый Django мы должны создать свой тип поля, наследованный от базового поля модели Django, и перекрить у него функцию сохранения. Получаем следущее:
class TreeOrderField(models.CharField): def pre_save(self, model_instance, add): if add: parent=(model_instance.parent or model_instance.article); if parent.seq<1000: parent.seq+=1; parent.save(); else: # update all comments to use 4digit masks pass value='%s%03d'%(getattr(parent, self.attname, ''), parent.seq, )[:255] setattr(model_instance, self.attname, value) return value return models.CharField.pre_save(self, model_instance, add)
Тут можно реализовать по-разному, но избавиться от дополнительного UPDATE не получается ни в одном из случаев. В частном случае PostgreSQL наверно можно использовать sequence и тогда лищних udate`ов не будет.
Для определения отступа комментария в дереве можно использовать принцип построения path:
@property def level(self): return max(0,len(self.path)/3-1)
Таким образом мы получили следующее решение: пример.
А опробовать работоспособность продложенного метода можно: тут.
Про отображение и контроллер комментариев на Django, а также про использование Ajax`а я расскажу в следующий раз (если кто-нибудь попросит).
Ссылки:
- django-threadedcomments - реализация коментариев, использующая рекурсию при выводе.
- django-mptt реализация множественной модели хранения деревьев в БД.