SAPikachu
SAPikachu

Reputation: 619

In django, is there a way to directly annotate a query with a related object in single query?

Consider this query:

query = Novel.objects.< ...some filtering... >.annotate(
    latest_chapter_id=Max("volume__chapter__id")
)

Actually what I need is to annotate each Novel with its latest Chapter object, so after this query, I have to execute another query to select actual objects by annotated IDs. IMO this is ugly. Is there a way to combine them into a single query?

Upvotes: 23

Views: 24696

Answers (4)

mrgreytop
mrgreytop

Reputation: 84

Yes, using Subqueries:

from django.db.models import OuterRef, Subquery

latest_chapters = (
    Chapter.objects
    .filter(novel=OuterRef("pk"))
    .order_by("chapter_order")
)

novels_with_chapter = Novel.objects.annotate(
    latest_chapter=Subquery(latest_chapters.values("chapter")[:1])
)

Tested on Django 3.0

The subquery creates a select statement inside the select statement for the novels, then adds this as an annotation. This means you only hit the database once.

I also prefer this to Rune's answer as it actually annotates a Novel object.

Hope this helps, anyone who came looking like much later like I did.

Upvotes: 5

Possible with Django 3.2+

Make use of django.db.models.functions.JSONObject (added in Django 3.2) to combine multiple fields (in this example, I'm fetching the latest object, however it is possible to fetch any arbitrary object provided that you can get LIMIT 1) to yield your object):

MainModel.objects.annotate(
    last_object=RelatedModel.objects.filter(mainmodel=OuterRef("pk"))
    .order_by("-date_created")
    .values(
        data=JSONObject(
            id="id", body="body", date_created="date_created"
        )
    )[:1]
)

Upvotes: 16

Rune Kaagaard
Rune Kaagaard

Reputation: 6798

Yes, it's possible.

To get a queryset containing all Chapters which are the last in their Novels, simply do:

from django.db.models.expressions import F
from django.db.models.aggregates import Max

Chapters.objects.annotate(last_chapter_pk=Max('novel__chapter__pk')
    ).filter(pk=F('last_chapter_pk'))

Tested on Django 1.7.

Upvotes: 31

thikonom
thikonom

Reputation: 4267

No, it's not possible to combine them into a single query.

You can read the following blog post to find two workarounds.

Upvotes: 4

Related Questions