51

I am generating aggregates for each item in a QuerySet:

def get_queryset(self):
    from django.db.models import Count
    queryset = Book.objects.annotate(Count('authors'))
    return queryset

But I am not getting the count in the JSON response.

thank you in advance.

Fiver
  • 9,909
  • 9
  • 43
  • 63
panchicore
  • 11,451
  • 12
  • 74
  • 100

4 Answers4

97

The accepted solution will hit the database as many times as results are returned. For each result, a count query to the database will be made.

The question is about adding annotations to the serializer, which is way more effective than doing a count query for each item in the response.

A solution for that:

models.py

class Author(models.Model):
    name = models.CharField(...)
    other_stuff = models...
    ...

class Book(models.Model):
    author = models.ForeignKey(Author)
    title = models.CharField(...)
    publication_year = models...
    ...

serializers.py

class BookSerializer(serializers.ModelSerializer):
    authors = serializers.IntegerField()

    class Meta:
        model = Book
        fields = ('id', 'title', 'authors')

views.py

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.annotate(authors=Count('author'))
    serializer_class = BookSerializer
    ...

That will make the counting at database level, avoiding to hit database to retrieve authors count for each one of the returned Book items.

Paolo Melchiorre
  • 5,716
  • 1
  • 33
  • 52
José L. Patiño
  • 3,683
  • 2
  • 29
  • 28
37

The queryset returned from get_queryset provides the list of things that will go through the serializer, which controls how the objects will be represented. Try adding an additional field in your Book serializer, like:

author_count = serializers.IntegerField(
    source='author_set.count', 
    read_only=True
)

Edit: As others have stated, this is not the most efficient way to add counts for cases where many results are returned, as it will hit the database for each instance. See the answer by @José for a more efficient solution.

Fiver
  • 9,909
  • 9
  • 43
  • 63
  • For me, this answer is also very useful to calculate the number of posts of a single tag. thanks you! – C.K. Nov 19 '18 at 00:07
8

Fiver's solution will hit the db for every instance in the queryset so if you have a large queryset, his solution will create a lot of queries.

I would override the to_representation of your Book serializer, it reuses the result from the annotation. It will look something like:

class BookSerializer(serializers.ModelSerializer):
     def to_representation(self, instance):
        return {'id': instance.pk, 'num_authors': instance.authors__count}

    class Meta:
        model = Book
Tobias
  • 1
  • 2
  • 4
  • 5
    Can you pls explain - why Fiver's solution would generate a lot of queries? I thought the serializer field will be processed after queryset is executed, which will do proper join/grouping? – DmitrySemenov Aug 17 '15 at 23:04
  • I made it with `Field(source='annotated_field')` – outoftime Oct 19 '15 at 11:51
  • 1
    @DmitrySemenov I think the source argument will reference a method not a variable. I had "source='author_set.count'" on my code before but it turned out generating a lot of queries (you can see it from the debugger), most of the queries were repeated, and called the count method. – Tobias Mar 11 '16 at 07:32
0

So, if you make an annotation like

Model.objects.annotate(
    some_new_col=Case(
        When(some_field=some_value, then=Value(something)),
        # etc...
        default=Value(something_default),
        output_field=SomeTypeOfField(),
    )
).filter()#etccc

and the interpreter throws in an error that something is not a model field for the related serializer, there is a workaround. It's not nice but if you add a method some_new_col, it recognizes the value from the query above. The following will do just fine.

def some_new_col(self):
    pass;
Alb
  • 1,063
  • 9
  • 33