Search code examples
pythondjangodatabasedjango-annotate

Django Queryset with annotate


I am writing one method in Django Manager model. I want to write method that finds out number of all sold copies (books) per author.

I have two models and method written in Manager. My problem is that method should also be chainable from any Author queryset, for example something like

Author.objects.filter(...).exlucde(...).total_copies_sold()

should also work.

Example:

author = Author.objects.create(...)
Book.objects.create(..., author=author, copies_sold=10)
Book.objects.create(..., author=author, copies_sold=20)

author_total_books = Author.objects.total_copies_sold().first()
>>> author_total_books.copies
30

Below my code. It works like in example above, but then I try something like:

author_books = Author.objects.filter(id=2).total_copies_sold()

I got

'QuerySet' object has no attribute 'annotate'

class AuthorManager(models.Manager):

    def total_copies_sold(self):
        return self.get_queryset().annotate(copies=Sum('book__copies_sold')



class Author(models.Model):
    first_name = models.CharField(max_length=120)
    last_name = models.CharField(max_length=120)

    objects = AuthorManager()


class Book(models.Model):
    title = models.CharField(max_length=120)
    copies_sold = models.PositiveIntegerField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')

[Edited]

Thank you schillingt for reply. I added:

class AuthorQueryset(models.QuerySet):
    def total_copies_sold(self):
        return self.annotate(copies=Sum('books__copies_sold'))

I tried something like:

author_books = Author.objects.filter(id=2).total_copies_sold()

>>> author_books.copies

I got

'AuthorQueryset' object has no attribute 'copies'


Solution

  • What you are lookig for is :

    from django.db import models
    from django.db.models import Sum
    from django.db.models.functions import Coalesce
    
    
    class AuthorManager(models.Manager):
        def get_queryset(self):
            return AuthorQuerySet(self.model, using=self._db)
    
        def annotate_with_copies_sold(self):
            return self.get_queryset().annotate_with_copies_sold()
    
    
    class AuthorQuerySet(models.QuerySet):
        def annotate_with_copies_sold(self):
            return self.annotate(copies_sold=Sum('books__copies_sold'))
    
    
    class Author(models.Model):
        objects = AuthorManager()
        first_name = models.CharField(max_length=30)
        last_name = models.CharField(max_length=30)
    
    
    class Book(models.Model):
        title = models.CharField(max_length=30)
        copies_sold = models.PositiveIntegerField()
        author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
    

    Now it is possible to chain queries e.g.:

    author_total_books = Author.objects.total_copies_sold().first()
    

    However you will no be able to use it on QuerySet object like:

    author_books = Author.objects.filter(id=2).total_copies_sold()
    

    That is because you are annotating Author object, not a QuerySet. To obtain that result you should execute:

    Author.objects.annotate_with_copies_sold().get(id=2)
    author.copies_sold 
    15