M2M отношение и post_save сигнал

Уже давно известно о небольшой(?) проблеме, связанной с 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

  1. Иван Сагалаев написал:

    Я бы, честно говоря, вообще бы не стал смотреть в сторону решения автоматической рассылки оповещений по изменениям в модели. Функциональность добавления комментария находится на уровне view, потому что только там можно точно знать, что именно происходит. На уровне моделей невозможно понять, зачем именно добавляется строчка в таблицу. Например возможно захочется написать некую функциональность, где комментарий сначала добавляется в базу, потом с ним что-то проверяется, потом он тут же удаляется. Или захочется в shell'е перетащить массово комментарии от одного поста к другому. Во всех таких случаях, хоть операции на уровне M2M-связи и делаются, никаких уведомлений слать не хочется.

    Мне кажется, идеальным дизайном для такой ситуации было бы генерация во view добавления комментария определенного сигнала, на который бы реагировало приложение с уведомлениями по почте. Связь слабая, зависимости не создает, действие происходит четко по месту.

    P.S. Кстати, а зачем M2M? Разве один комментарий может быть привязан к нескольким постам?

    Оставлен 30 Декабрь 2007 в 17:00
  2. Александр Кошелев написал:

    Я бы, честно говоря, вообще бы не стал смотреть в сторону решения автоматической рассылки оповещений по изменениям в модели. Функциональность добавления комментария находится на уровне view, потому что только там можно точно знать, что именно происходит. На уровне моделей невозможно понять, зачем именно добавляется строчка в таблицу. Например возможно захочется написать некую функциональность, где комментарий сначала добавляется в базу, потом с ним что-то проверяется, потом он тут же удаляется. Или захочется в shell'е перетащить массово комментарии от одного поста к другому. Во всех таких случаях, хоть операции на уровне M2M-связи и делаются, никаких уведомлений слать не хочется.

    Абсолютно согласен. Просто сделать завязку на post_save - самое первое, что пришло в голову. Хотелось очень быстро опробовать систему оповещений в деле. А тут ещё и m2m особенность выплыла. Решил поковырять.

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

    Кстати вопрос связи объекта в коде(некого класса) и его представления в базе в последнее время меня очень волнует. В схему классического орм не очень вписывается. Может не очень понятно выразил мысль, но такая тема есть. Наверно скоро попытаюсь её охватит более подробно в посте.

    Мне кажется, идеальным дизайном для такой ситуации было бы генерация во view добавления комментария определенного сигнала, на который бы реагировало приложение с уведомлениями по почте. Связь слабая, зависимости не создает, действие происходит четко по месту.

    Ага. Такая мысль тоже пришла в голову. Бросать что-то типа comment_added сигнала именно из view.

    P.S. Кстати, а зачем M2M? Разве один комментарий может быть привязан к нескольким постам?

    Просто Comment находится совсем в другом приложении и о блоге не знает ничего и применяется не только в нем. Как и Tag кстати. Это всё последствия моего приступа декомпозиции. Который сейчас меня накрыл второй волной в процессе написания тестов для реализации пингбека:) Ужасно интересная вещь, декомпозиция компонентов системы, которая меня тоже очень сильно волнует:)

    Оставлен 30 Декабрь 2007 в 17:39
  3. Александр Кошелев написал:

    Ооо, баг дизайна вылез на большом комментарии:) Будем исправлять...

    Оставлен 30 Декабрь 2007 в 17:42
  4. crash написал:

    Спасибо большое, искал решение подобной проблемы 2 дня :)

    Оставлен 17 Январь 2008 в 19:07
  5. Александр Кошелев написал:

    Спасибо большое, искал решение подобной проблемы 2 дня :)

    Рад, что пригодилось. Приходите ещё:)

    Оставлен 17 Январь 2008 в 19:19
  6. Arnolds написал:

    Спасибо за решение. Два дня просидел над етой проблеме!

    maximum global respect :)

    Оставлен 17 Апрель 2008 в 10:29