Henrik
Henrik

Reputation: 1995

Best place to increase a counter field in Django REST

Let's say I have two Models in Django:

Book:

class Book(models.Model):
    title = models.CharField(max_length=100, blank=False)
    number_of_readers = models.PositiveIntegerField(default=0)

Reader:

class Reader(models.Model):
    book = models.ForeignKey(Book)
    name_of_reader = models.CharField(max_length=100, blank=False)

Everytime I add a new Reader to the database I want to increase number_of_readers in the Book model by 1. I do not want to dynamically count number of rows Reader rows, related to a particular Book, for performance reasons.

Where would be the best place to increase the number_of_readers field? In the Serializer or in the Model? And what method shall I use? Should I override .save in the Model? Or something else in the Serializer?

Even better if someone could provide a full blown example on how to modify the Book table when doing a post of a new Reader.

Thanks.

Upvotes: 2

Views: 2385

Answers (2)

bakkal
bakkal

Reputation: 55448

I wouldn't do this on the REST API level, I'd do it on the model level, because then the +1 increase will always happen, regardless of where it happened (not only when you hit a particular REST view/serializer)

Django signals

Everytime I add a new Reader to the database I want to increase number_of_readers in the Book model by 1

I'd implement a post_save signal that triggers when a model (Reader) is created

There is a parameter to that signal called created, that is True when the model is created, which makes more convenient than the Model.save() override

Example outline

from django.db.models.signals import post_save

def my_callback(sender, instance, created, **kwargs):
    if created:
        reader = instance
        book = reader.book
        book.number_of_readers += 1 # prone to race condition, more on that below
        book.save(update_fields='number_of_readers') # save the counter field only

post_save.connect(my_callback, sender=your.models.Reader)

https://docs.djangoproject.com/en/1.8/ref/signals/#django.db.models.signals.post_save

Race Conditions

In the above code snippet, if you'd like to avoid a race condition (can happen when many threads updating the same counter), you can also replace the book.number_of_readers += 1 part with an F expression F('number_of_readers') + 1, which makes the read/write on the DB level instead of Python,

book.number_of_readers = F('number_of_readers') + 1
book.save(update_fields='number_of_readers')

more on that here: https://docs.djangoproject.com/en/1.8/ref/models/expressions/#avoiding-race-conditions-using-f

There is a post_delete signal too, to reverse the counter, if you ever think of supporting "unreading" a book :)

Batch or periodic updates

If you wish to have batch imports of readers, or need to periodically update (or "reflow") the reader counts (e.g. once a week), you can in addition of the above, implement a function that recounts the readers and update the Book.number_of_readers

Upvotes: 8

Ivan
Ivan

Reputation: 6013

It depends on the design of your app and particularly on where you will reuse this logic.

For example, if you want the same logic for adding Readers everywhere in your app, do it in a signal, as bakkal suggests or in save. If it depends on the API endpoint, you might want to do it in a view.

It will also depend if you are doing bulk inserts of readers: if you do it in save or a pre_/post_save it will not work for bulk updates, so it would be better to do it in QuerySet's create and bulk_create methods etc.

From performance point of view, you might want to use F expressions, no matter where you do it:

book.number_of_readers = F('number_of_readers') + added_readers_count

Upvotes: 1

Related Questions