views:

20

answers:

2

I've got an Invoice model, and I want to keep the total up to date. So I've added a property like this

@property
def item_total(self):
    if not hasattr(self, 'item_total_cache'):
        self.item_total_cache = self.items.aggregate(t=Sum('amount'))['t']
    return self.item_total_cache

Because item_total might get called a few times during one page request and I don't want to hit the DB every time. Is this a good way to do it? Will my Invoice object exist beyond one page request and thus the item_total might actually get stale?

Ideally, I'd like to compute the item_total at the same time the invoices are requested... I know I can do this manually, but the Django admin site, for instance, doesn't know it's going to need the sum. Is there someway I can "inject" this extra tidbit of information so that all Invoice.objects.all/filter/get() requests run the aggregate at the same time?

+2  A: 

Can I suggest another solution? Why not store the count and make it auto increment/decrement by using signals?

It depends on the amount of reads/writes you have ofcourse, but caching something like this is probably the more efficient solution. And possible quite a bit easyer too.

Either way, you are caching the count properly. The Invoice object shouldn't become stale unless you cache it some other way. This cache is bound to the instance and with a new request you can assume that you get a new instance aswell. Unless you're getting your items from memcached that is.

As for injecting the cache when needed. How would that work if you use get() or filter()? In that case your count would be lower and your cache incorrect. It should be fairly doable for all() though. You can simply overwrite the default manager to add this data automatically while fetching. But that would mean that for every Item you request, your Invoice will be fetched aswell.

WoLpH
I'm untrusting of signals... if the signal isn't received, the whole app falls apart. A minor syntax error could throw everything out of sync, and it's hard to debug. It's a possible sol'n though.
Mark
@Mark: If you don't trust the signals you can put it in the `save()` method of the model. I agree that it's a little harder to debug but it doesn't have to be that hard. And if you think it's out of sync, simply rebuilding the cache is done in a few lines.
WoLpH
+2  A: 

This is a good solution, one I've used and recommended before. Your Invoice object won't persist beyond a single page request as long as you don't keep any references to it in a global scope - if it's just created in a view and passed to a template, it will be fine.

There are a couple of ways to get the admin site to generate the aggregate automatically. One is to define a custom Manager and override get_query_set to return self.annotate(Sum('amount')) - this will ensure that the aggregate is always created for every single query on the Invoice model, which may not be what you want.

An alternative is to define a separate method on the manager - with_aggregate, for example - which returns that, but then override the ModelAdmin's get_queryset method to return the result of that method. This gives you more control to decide whether or not to use the aggregate in your own views.

Daniel Roseman
Forgot I could override the manager... never needed to do that before. That ought to work well! Glad my current solution isn't so awful too; might put this off until the more pressing features are complete then :)
Mark