6

I have a pretty generic Article model, with m2m relation to Tag model. I want to keep count of each tag usage, i think the best way would be to denormalise count field on Tag model and update it each time Article being saved. How can i accomplish this, or maybe there's a better way?

Paco
  • 4,520
  • 3
  • 29
  • 53
Dmitry Shevchenko
  • 31,814
  • 10
  • 56
  • 62

2 Answers2

3

This is a new feature in Django 1.2: http://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed

Christian Oudard
  • 48,140
  • 25
  • 66
  • 69
  • 2
    Watch out for the m2m_changed signal though because there's no way to know what exactly has changed inside the handler. – Adam Nelson Sep 23 '10 at 15:43
  • Does it not send an action argument? – dd. Feb 18 '11 at 03:54
  • Basically, you know what the current state is, but you don't have any way of knowing what the state was before the signal was sent. In the case given by the original question though, you only need to know the number of attachments, so this is fine. – Christian Oudard Feb 18 '11 at 18:49
2

You can do this by creating an intermediate model for the M2M relationship and use it as your hook for the post_save and post_delete signals to update the denormalised column in the Article table.

For example, I do this for favourited Question counts in soclone, where Users have a M2M relationship with Questions:

from django.contrib.auth.models import User
from django.db import connection, models, transaction
from django.db.models.signals import post_delete, post_save

class Question(models.Model):
    # ...
    favourite_count = models.PositiveIntegerField(default=0)

class FavouriteQuestion(models.Model):
    question = models.ForeignKey(Question)
    user     = models.ForeignKey(User)

def update_question_favourite_count(instance, **kwargs):
    """
    Updates the favourite count for the Question related to the given
    FavouriteQuestion.
    """
    if kwargs.get('raw', False):
        return
    cursor = connection.cursor()
    cursor.execute(
        'UPDATE soclone_question SET favourite_count = ('
            'SELECT COUNT(*) from soclone_favouritequestion '
            'WHERE soclone_favouritequestion.question_id = soclone_question.id'
        ') '
        'WHERE id = %s', [instance.question_id])
    transaction.commit_unless_managed()

post_save.connect(update_question_favourite_count, sender=FavouriteQuestion)
post_delete.connect(update_question_favourite_count, sender=FavouriteQuestion)

# Very, very naughty
User.add_to_class('favourite_questions',
                  models.ManyToManyField(Question, through=FavouriteQuestion,
                                         related_name='favourited_by'))

There's been a bit of discussion on the django-developers mailing list about implementing a means of declaratively declaring denormalisations to avoid having to write code like the above:

Jonny Buchanan
  • 61,926
  • 17
  • 143
  • 150
  • There's a gotcha with this technique though: if you want to use these classes in a form, form.save_m2m will no longer work – Rob Mar 20 '09 at 13:34