Helper to annotate first or latest value using Subqueries expressions:
from django.db.models import OuterRef, Subquery
from django.db.models.query_utils import DeferredAttribute
def get_value_subquery(model, attr: str, ref: str, outref: str, order_by: str, **kwargs):
"""Annotation helper to annotate first value of related table to queryset
attr - model attr which need to be attached
ref - model attr by which field will be used for join with main table
outref - as field_name (str) of main table to join with ref
order_by - order by field
kwargs - optional extra filtering kwargs for join """
db_field = model._meta.get_field(attr)
ref_field = model._meta.get_field(ref)
filters = {ref_field.attname: OuterRef(outref), **kwargs}
sub_query = db_field.model.objects.filter(**filters).order_by(order_by)
return Subquery(sub_query.values(db_field.attname)[:1], output_field=db_field.__class__())
def last_value_subquery(model, attr, ref, outref, **kwargs):
return get_value_subquery(model, attr=attr, ref=ref, outref=outref, order_by=f"-{attr}", **kwargs)
def first_value_subquery(model, attr, ref, outref, **kwargs):
return get_value_subquery(model, attr=attr, ref=ref, outref=outref, order_by=attr, **kwargs)
Models example
from django.db import models
class Customer(models.Model):
name = models.TextField()
class CustomerVisit(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
datetime = models.DateTimeField()
Usage example
def test_subquery(self):
c = Customer.objects.create(name='a')
first_visit = CustomerVisit.objects.create(customer=c, datetime=datetime.datetime(2010, 1, 1))
second_visit = CustomerVisit.objects.create(customer=c, datetime=datetime.datetime(2011, 1, 1))
third_visit = CustomerVisit.objects.create(customer=c, datetime=datetime.datetime(2013, 1, 1))
main_query = Customer.objects.all()
last_visit_q = last_value_subquery(CustomerVisit, attr='datetime', ref='customer', outref='id')
first_visit_q = first_value_subquery(CustomerVisit, attr='datetime', ref='customer', outref='id')
customer = main_query.annotate(last_visit=last_visit_q, first_visit=first_visit_q).get()
assert customer.last_visit == third_visit.datetime
assert customer.first_visit == first_visit.datetime