Посты с тегом сигналы (8)

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 ...

Композиция: ForeignAttributeField

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

С момента прошлого поста я успел значительно улучшить базовый CompositionField и решить несколько концептуальных проблем.

Итак что же новое появилось:

  • Сделал низкоуровневый CF полноценным классом, который теперь удобно сабклассить и добавлять новый функционал
  • Появилось место для интроспекции. Присоединение к хост модели стало более интеллектуальным из-за возможного отложенного присоединения.
  • Чуть-чуть изменился интерфейс - параметр commit теперь может быть задан для конкретного триггера. Это сугубо практическое изменение, которое помогло решить одну проблему с бесконечной рекурсией.
  • По совету Вани Сагалаева добавил генерацию freeze_FOO метода, который включает/выключает обработку сигналов. Полезно когда надо обработать какие-то данные скопом, а потом так же скопом пересчитать денормализованные поля.

Я грозился написать высокоуровневые обертки над CF чтобы облегчить использование и упростить конечный интерфейс. Сегодня я расскажу о первом сабклассе CF: ForeignAttributeField - поле которое отслеживает изменения некого внешнего поля в связанном объекте. Причем, поле может находится на любом уровне вложенности связи, что иногда очень удобно. Как я уже писал в прошлый раз - поля объектов, на которые непосредственно ссылается модель, так денормализовывать смыла мало. Проще использовать select_related. Но если заветный атрибут лежит глубже и для доступа к нему надо приджоинить, допустим, пять таблиц, то тут уже другой расклад и ...

Красивая композиция

Нет, я не про музыку решил написать. А про композицию данный, агрегацию если хотите.

В джанге на данный момент агрегации в ORM нет. Но как известно скоро должна появиться, а значит жизнь в очередной раз станет проще.

Зачем?

Но на много ли? Да, писать запросы с агрегацией станет проще. Но в большинстве случаешь выполнять запрос для сервера легче не станет. Вот ту на помощь приходит техника денормализации.

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

Наверно многие из вас в своих проектах писали код наподобие такого, тем более, если писали "свой блог движок":

class Post(models.Model):
   comment_count = models.PositiveIntegerField(default=0)

class Comment(models.Model):
   post = models.ForeignKey(Post, related_name="comments")

   def save(self):
      super(Comment, self).save()
      self.post.comment_count = self.post.comments.count()

   def delete(self):
      super(Comment, self).delete ...

А вы поймали новые сигналы?

Вчера джанга сделал ещё один существенный шаг на пути к долгожданному релизу 1.0. На этот раз посчастливилось обновиться инфраструктуре сигналов. Основное и главное отличие новой подсистемы сигналов - это почти двукратное увеличение производительности.

Исторически сигналы в джанге были сделаны на базе пакета pyDispatcher. который почти без изменений был скопирован в django.dispatch. Со временем выяснилось, что он избыточен и медленен.

Сигналы практически везде пронизывают код джанги. Повесить обработчик можно и на изменение объекта, и на начало и конец обработчик запроса и рендеринг шаблона и много чего ещё. Причем они не только предоставляют некий интерфейс взаимодействия с внешним миром, но и активно применяются внутри самой джанги. Например, удаление загруженных файлов вслед за объектами и закрытие соединения с базой при завершении обработки запроса(!).

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

Возможности погонять тесты у меня нет, так что поверим разработчикам на слово. Лучше ...

Кеширование. Инвалидация сигналами. Трапеза

Так. продолжаем процесс употребления кеширования, основанного на сигналах в django. О теории уже было сказано достаточно, перейдем к практике, а для этого немного пофантазируем.

Итак, у нас есть приложения блога и в нем такие модели:

class Post( models.Model ):
    title = models.CharField(max_length=100)
    date = models.DateTimeField(default=datetime.now)
    text = models.TextField()

class Comment( models.Model ):
    post = models.ForeignKey( Post, related_name = "comments" )
    text = models.TextField()

Теперь как полагается в хорошем блог движке, мы хотим получить последние 5 постов для того чтобы сообщить о них постетителю. Для этого напишем простую функцию

def get_latest_posts():
    return Post.objects.all().order_by( "-date" )[ :5 ]

Отлично, функция готова и мы её можем применять в нужном нам месте. Но зачем каждый раз мучить базу и просить её вернуть последние посты? Ведь пишу то я не часто (ну так получается:)), поэтому можно это дело закешировать и не тратить ресурсы попусту, т.к. они не часто обновляются.

Но классический вариант с таймаутом тут не очень удобен: если сделать маленький таймаут, то падает эффективность кеширования, если же учитывая большие интервалы между добавлением новых постов, то можно и таймаут побольше поставить, но тут сразу появляется проблема. Я добавил новый пост, а кеш обновится только через сутки (ну реально большие интервалы ...