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

Давным давно описывал решение проблемы с сигналами и ManyToMany полями. Вопрос остался актуальным и по сей день и частенько всплывает в форумах.

Напомню суть: в save методе (или в обработчике pre_/post_save сигналов) модели родителе (Post) нельзя узнать об изменениях в отношениях с чайлдами (Comment). Это связано с тем, что чайлды присоединяются к родителю после его сохранения.

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

В версии 1.0 нам дали возможность задать для M2M связывания свою модель. Применительно к моему старому примеру это будет выглядеть так:

class Comment(models.Model):
    #...

class Post(models.Model):
    comments = models.ManyToManyField(Comment, through='PostToCommentRelation')
    #...

class PostToCommentRelation(models.Model):
    post = models.ForeignKey(Post)
    comment = models.ForeignKey(Comment)

И вот как только мы сделали эту модель и явно указали её в качестве промежуточной, у нас появилась возможность абсолютно стандартным путем отслеживать изменения связей. Поскольку это обычная модель, то она сама имеет этот самый пресловутый post_save сигнал, повесив обработчик на который, вы всегда сможете отследить какие два объекта были связаны и как-то на это прореагировать.

def relation_change_handler(sender, instance, created, **kwargs):
    # работа с instance.post или instance.comment
    pass

signals.post_save.connect(relation_change_handler, sender=PostToCommentRelation)

Теперь такой подход мне кажется более рациональным и религиозно правильным:-)

P.S.: Очередное обновление в блоге - каптчи больше нет. Подружил движок с Akismet и теперь как все белые люди защищен им. Так что если вы не пользуетесь OpenID (кому легко уже сейчас), то комментировать стало ещё проще! Welcome!:-)

Комментарии 8

  1. Arcady Chumachenko написал:

    Такой вариант все равно не даст удобно обновлять какой-нибудь comment_count (которому нужно обрабатывать всю пачку комментов целиком).

    Оставлен 09 Апрель 2009 в 15:22
  2. Александр Кошелев написал:

    Хм... Это как посмотреть. Такой подход дает нам понять, когда "что-то поменялось". И мы можем просто пересчитывать в этом случае денормализованное поле:

    def relation_change_handler(sender, instance, created, **kwargs):
        instance.post.comment_count = instance.post.comment_set.count()
        instance.post.save()
    

    А вообще, в таких случаях конечно нужно пользоваться композиционным полем:)

    Оставлен 09 Апрель 2009 в 16:05
  3. Arcady Chumachenko написал:

    Дык это то же самое, только в профиль, в смысле - в одно поле.

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

    Оставлен 09 Апрель 2009 в 16:16
  4. http://lit-crash.livejournal.com/ написал:

    Избыточность конечно есть, но зато способ действительно очень прост

    Оставлен 09 Апрель 2009 в 16:23
  5. sevenov написал:

    Опа, полезно.

    Оставлен 09 Апрель 2009 в 19:59
  6. Иван Сагалаев написал:

    Если честно, я вообще никогда не понимал этой проблемы -- описывать где-то в моделях поведение на сохранение M2M-связей. Потому что на самом деле это должно делаться на уровне бизнес-логики -- во view, формах и т.д. Обычно это не только удобней, но и эффективней, потому что пересчет чего-либо можно вызвать один раз при добавлении нескольких связанных объектов, а не на каждый.

    Оставлен 10 Апрель 2009 в 00:09
  7. Александр Кошелев написал:

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

    Оставлен 10 Апрель 2009 в 13:18
  8. Иван Сагалаев написал:

    М-да... Ну что ж, по крайней мере, меня нельзя упрекнуть в непоследовательности :-)

    Оставлен 10 Апрель 2009 в 15:57