Уже давно известно о небольшой(?) проблеме, связанной с post_save сигналом и ManyToMany отношением моделей. Для примера, Лориен столкнулся некоторое время назад с ней, о чем есть интересный тред на форуме.
Я же всё как-то раньше мимо проходил этой особенности, но тут столкнулся лицом к лицу. Кратко расскажу предысторию.
В процессе уменьшения TODO листа по улучшению блога, встала задача организации оповещения по почте о новых комментариях. Но делать реализацию жестко завязанную на блоге не хотелось, поскольку задача оповещения в принципе может встать в любом приложении. Поэтому некоторое время я пытался разработать, до некоторой степени, универсальную систему оповещений, легко интегрируемую в любое приложение. Одной из особенностей этой системы была завязка на джанго сигналах. Уж больно направилась такая связь: произошло некоторые событие -> образовался сигнал -> пошло оповещение нужным пользователям.
И когда черновой вариант системы был реализован, я сразу решил бросить ей в бой. Первый вариант не сработал. Повесить оповещение на post_save сигнал у модели Comment не получилось. Точнее получилось конечно, но не был достигнут нужный результат. Модели связаны схематично вот так:
class Comment( models.Model ):
#...
class Post( models.Model ):
comments = models.ManyToManyField( Comment )
#...
В обработчике сигнала я должен был уже получить ссылку на пост, к которому создан комментарий, но не вышло. Сигнал шел раньше чем m2m зависимость между постом и комментарием сохранялась.
Просто решения я не нашел. И было принято волевое решение(:)) слегка доработать ManyToManyField. Точнее отнаследоваться от него и немного по-шаманить. Шаманство заключается в подмене метода add у m2m менеджера. Для этого был написан вот такой код:
def make_add_method( parent ):
def ext_add( self, *objs ):
parent(*objs)
dispatcher.send( signal=signals.m2m_post_save, sender=self.model, instance=self.instance, objects=objs)
ext_add.alters_data = True
return ext_add
class ExtManyRelatedObjectsDescriptor(related.ManyRelatedObjectsDescriptor):
def __get__(self, instance, instance_type=None):
manager = super( ExtManyRelatedObjectsDescriptor, self ).__get__( instance, instance_type )
manager.__class__.add = make_add_method( manager.add )
return manager
class ExtReverseManyRelatedObjectsDescriptor(related.ReverseManyRelatedObjectsDescriptor ):
def __get__(self, instance, instance_type=None):
manager = super( ExtReverseManyRelatedObjectsDescriptor, self ).__get__( instance, instance_type )
manager.__class__.add = make_add_method( manager.add )
return manager
class ExtManyToManyField( related.ManyToManyField ):
def contribute_to_class(self, cls, name):
super(ExtManyToManyField, self).contribute_to_class(cls, name)
# Add the descriptor for the m2m relation
setattr(cls, self.name, ExtReverseManyRelatedObjectsDescriptor(self))
def contribute_to_related_class(self, cls, related):
super(ExtManyToManyField, self).contribute_to_related_class(cls, related)
# m2m relations to self do not have a ManyRelatedObjectsDescriptor,
# as it would be redundant - unless the field is non-symmetrical.
if related.model != related.parent_model or not self.rel.symmetrical:
# Add the descriptor for the m2m relation
setattr(cls, related.get_accessor_name(), ExtManyRelatedObjectsDescriptor(related))
Он не очень прозрачен, но своё дело делает. Немного поясню. m2m_post_save это мною определенный сигнал специально для этого случая. Он посылается уже после того как объект был добавлен в m2m таблицу. В качестве дополнительных параметров(помимо sender) передается объект к которому присоединяется объект сендер (в моём случае этот объект - пост) и последовательность присоединенных сендоров(в моём случае - комментарии).
Для использования этого поля, пришлось немного поменять исходные модули. Но на такую жертву я готов был пойти:)
#...
comments = ExtManyToManyField( Comment )
Да, конечно данный вариант не подходит для тех случаев когда нужно ловить сигналы в сторонних приложениях, где нет возможности заменить ManyTOManyField на модифицированный. Но для приложений собственного производства вполне подходит.
А как вы считаете, не слишком корявое получилось решение?
PS: Кстати, оповещения уже работают в тестовом режиме. Так что можете подписываться на обновления в комментариях. Если какие-то будут ошибки, очень прошу вас мне о них сообщить. Буду очень благодарен.
Комментарии 6
Я бы, честно говоря, вообще бы не стал смотреть в сторону решения автоматической рассылки оповещений по изменениям в модели. Функциональность добавления комментария находится на уровне view, потому что только там можно точно знать, что именно происходит. На уровне моделей невозможно понять, зачем именно добавляется строчка в таблицу. Например возможно захочется написать некую функциональность, где комментарий сначала добавляется в базу, потом с ним что-то проверяется, потом он тут же удаляется. Или захочется в shell'е перетащить массово комментарии от одного поста к другому. Во всех таких случаях, хоть операции на уровне M2M-связи и делаются, никаких уведомлений слать не хочется.
Мне кажется, идеальным дизайном для такой ситуации было бы генерация во view добавления комментария определенного сигнала, на который бы реагировало приложение с уведомлениями по почте. Связь слабая, зависимости не создает, действие происходит четко по месту.
P.S. Кстати, а зачем M2M? Разве один комментарий может быть привязан к нескольким постам?
Оставлен 30 Декабрь 2007 в 17:00 ¶Абсолютно согласен. Просто сделать завязку на post_save - самое первое, что пришло в голову. Хотелось очень быстро опробовать систему оповещений в деле. А тут ещё и m2m особенность выплыла. Решил поковырять.
Да и вообще сигналы не единственный способ связи конечно. Можно просто из любого места кода инициировать некое событие, которое вызовет рассылку.
Кстати вопрос связи объекта в коде(некого класса) и его представления в базе в последнее время меня очень волнует. В схему классического орм не очень вписывается. Может не очень понятно выразил мысль, но такая тема есть. Наверно скоро попытаюсь её охватит более подробно в посте.
Ага. Такая мысль тоже пришла в голову. Бросать что-то типа comment_added сигнала именно из view.
Просто Comment находится совсем в другом приложении и о блоге не знает ничего и применяется не только в нем. Как и Tag кстати. Это всё последствия моего приступа декомпозиции. Который сейчас меня накрыл второй волной в процессе написания тестов для реализации пингбека:) Ужасно интересная вещь, декомпозиция компонентов системы, которая меня тоже очень сильно волнует:)
Оставлен 30 Декабрь 2007 в 17:39 ¶Ооо, баг дизайна вылез на большом комментарии:) Будем исправлять...
Оставлен 30 Декабрь 2007 в 17:42 ¶Спасибо большое, искал решение подобной проблемы 2 дня :)
Оставлен 17 Январь 2008 в 19:07 ¶Рад, что пригодилось. Приходите ещё:)
Оставлен 17 Январь 2008 в 19:19 ¶Спасибо за решение. Два дня просидел над етой проблеме!
maximum global respect :)
Оставьте комментарий