Opened 10 years ago

Closed 10 years ago

Last modified 10 years ago

#22543 closed New feature (wontfix)

Add pre_update and post_update signal for querysets

Reported by: Ben Davis Owned by: nobody
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords: queryset update signals
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

One use case for this would be cache coherence. For example, let's say we're caching an expensive operation based on a model pk, and we want to invalidate that cache when a row is changed in any way. Currently we can only do this with save/delete signals. Consider the following example for calculating points awarded to an account:

def get_points_earned(account):
    key = 'acct-{0}-points'.format(account.id)
    if not cache.get(key):
        point_awards = PointAward.objects.filter(account=account)
        points = point_awards.aggregate(total=Sum('points'))['total']
        cache.set(key, points)
    return cache.get(key)

# Invalidate points earned if a PointAward is added/changed/deleted
@receiver([post_save, post_delete, post_update], sender=PointAward)
def on_point_award_changed(sender, **kwargs):
    instance = kwargs.get('instance')
    if instance:
        key = 'acct-{0}-points'.format(instance.id)
        cache.delete(key)

With this coherence strategy, a call to PointAward.objects.update() will cause the cache to become incoherent, and will cause errors in the calculation.

This can easily be addressed by providing a signal allowing a developer to decide how best to update the cache when update() is called:

# django/db/models/signals.py

pre_update = ModelSignal(providing_args=["queryset", "values", "using"], use_caching=True)
post_update = ModelSignal(providing_args=["queryset", "values", "using"], use_caching=True)

We can now update our receiver to also respond to updates as well as changes and deletions:

@receiver([post_save, post_delete], sender=PointAward)
def on_point_award_changed(sender, **kwargs):
    point_award = kwargs.get('instance')
    if instance and instance.pk:
        # this is a save/delete, invalidate the related account's points
        account_id = point_award.account_id
        key = 'acct-{0}-points'.format(instance.id)
        cache.delete(key)
    
    elif kwargs.get('queryset'):
        # this is an update, invalidate all matching accounts
        qs = kwargs['queryset']
        for acct_id in qs.objects.distinct().values_list('account_id', flat=True)
            key = 'acct-{0}-points'.format(acct_id)
            cache.delete(key)

Change History (2)

comment:1 by loic84, 10 years ago

Resolution: duplicate
Status: newclosed

Hi @bendavis78, this feature is discussed at #21461.

comment:2 by Tim Graham, 10 years ago

Resolution: duplicatewontfix

Seems this is similar to #17824 which was won't fixed. Needs discussion on the django-developers mailing list in order to be accepted.

Note: See TracTickets for help on using tickets.
Back to Top