Django中如何为每个Author标注Book的copies_sold?代码报错排查
Let's walk through your two test failures and get your annotation working correctly.
First Error: AttributeError: 'Author' object has no attribute 'copies_sold'
This one's straightforward: you're trying to access a copies_sold attribute on an Author instance that wasn't annotated with it. The copies_sold sum doesn't exist on the base Author model—it needs to be added dynamically via Django's annotate() method on your queryset.
Fix: Annotate the Author Queryset
Before fetching any Author objects (whether via get(), all(), or filter()), you need to use annotate() with Sum to calculate the total copies sold across their books.
Example code:
from django.db.models import Sum # Get all authors with their total book sales annotated authors_with_sales = Author.objects.annotate( copies_sold=Sum('books__copies_sold') # 'books' is your related_name from Book to Author ) # Now you can access copies_sold on any instance from this queryset author = authors_with_sales.get(name="J.R.R. Tolkien") print(author.copies_sold) # No more AttributeError!
Make sure your test test_should_annotate uses this annotated queryset before fetching the Author object. Don't call Author.objects.get() directly without the annotate() step first.
Second Error: AssertionError: 3 != 2 (Filtering Support)
This means your annotation is including all books for an author, even when you're trying to filter down to a subset of books (e.g., only books from a certain year, or a specific genre). The issue is likely that your filter is being applied to the Author queryset instead of the related Book objects, or you're not isolating the filtered books for the sum calculation.
Fix: Use FilteredRelation or Subqueries for Filtered Annotations
There are two reliable ways to calculate the sum of only filtered books for each author:
Option 1: FilteredRelation (Cleanest for Simple Filters)
Use FilteredRelation to define a filtered subset of the author's books, then sum against that subset:
from django.db.models import FilteredRelation, Q, Sum # Calculate total sales only for books published in 2023 authors_filtered_sales = Author.objects.annotate( # Define a filtered relation to books matching our condition filtered_books=FilteredRelation( 'books', condition=Q(books__publication_year=2023) # Your filter condition here ), # Sum copies sold from the filtered books copies_sold=Sum('filtered_books__copies_sold') )
This keeps all authors in the queryset (even those with no matching books—their copies_sold will be None, which you can convert to 0 with Coalesce if needed).
Option 2: Subquery (More Flexible for Complex Logic)
If you need more control over the filter logic, use a subquery to calculate the sum per author:
from django.db.models import Subquery, OuterRef, Sum, Coalesce # Subquery that calculates total sales for filtered books per author filtered_sales_subquery = Book.objects.filter( author=OuterRef('pk'), # Link to the outer Author queryset publication_year=2023 # Your filter condition ).values('author').annotate( total_sales=Sum('copies_sold') ).values('total_sales') # Annotate authors with the subquery result, defaulting to 0 if no matches authors_filtered_sales = Author.objects.annotate( copies_sold=Coalesce(Subquery(filtered_sales_subquery), 0) )
Update Your Test
For your test_annotating_works_with_filtering test, make sure you're using one of these methods to isolate the filtered books. For example, if your test expects an author with two books (total sales 2) and one excluded book (sales 1), the annotation should only sum the two matching books, giving you 2 instead of 3.
Example test code:
def test_annotating_works_with_filtering(self): # Create test data author = Author.objects.create(name="George Orwell") # Two books we want to include (sales 1 each) Book.objects.create(author=author, copies_sold=1, publication_year=2023) Book.objects.create(author=author, copies_sold=1, publication_year=2023) # One book we want to exclude (sales 1) Book.objects.create(author=author, copies_sold=1, publication_year=2022) # Get annotated author with filtered sales annotated_author = Author.objects.annotate( filtered_books=FilteredRelation( 'books', condition=Q(books__publication_year=2023) ), copies_sold=Sum('filtered_books__copies_sold') ).get(pk=author.pk) # Verify we get the filtered sum (2) instead of total sum (3) self.assertEqual(annotated_author.copies_sold, 2)
Key Takeaways
- Always annotate your Author queryset before accessing
copies_sold—don't use unannotated Author instances. - When filtering books for the sum, use
FilteredRelationor subqueries to ensure you're only summing the subset of books you care about. - Use
Coalesceif you want to replaceNone(for authors with no matching books) with 0.
内容的提问来源于stack exchange,提问作者unknown

