PyWombat

← Volver a artículos

Django annotate

August 31, 2024

293 views

3 min de lectura

Siguiendo con nuestra serie de Django ORM, el día de hoy me gustaría hablar acerca de como funciona el método annotate en el ORM de Django. Uno de los método más utilizados en el framework.

Será un post sumamente interesante, con muchos ejemplos, así que te invito a que te quedes.

Modelos

Antes de entrar de lleno con el post, debemos tener en cuenta que trabajaremos con los siguientes modelos, con una relación uno a muchos. Un categoría puede tener múltiples producto y un producto le pertenezca a una categoria.

from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE)

    def __str__(self):
        return self.name

Sin más que decir, entremos de lleno al tema.

Annotate

En términos simples el método annotate en Django nos permite añadir nuevos atributos y valores a cada uno de los elementos de un queryset. El método por su parte retorna un nuevo queryset, con lo cual seremos capaces de seguir concatenando filtros o nuevas anotaciones.

Veamos un ejemplo sencillo.

Imaginemos que a nuestras categorías les deseamos añadir un nuevo campo status, que por default será ‘Verdadero’ como string. Este nuevo atributo será un atributo virtual, es decir, no lo necesitamos en el modelo per se, si no que lo generaremos a partir de una consulta con el ORM.

Para ello haremos uso del método annotate.

Al utilizar el método annotate pasaremos los argumentos por nombre (**kwargs).

Category.objects.annotate(status='Active')

Si ejecutamos el código de arriba obtendremos un error, ya que el método annotate esta diseñado para generar nuevos campos a partir de una expresión o un método de agregación. Y lo que estoy haciendo es asignar un valor de tipo string directamente, lo cual no es posible.

Lo que podemos hacer para que el código funcione, y podamos asignar ‘Active’ como valor a el nuevo atributo status es usar la clase Value.

from django.db.models import Value

Category.objects.annotate(status=Value('Active'))

Ahora sí. Al utilizar la clase Value obtendremos como resultado una expresión, expresión que sí podemos usar para crear nuestro atributo status. Este nuevo atributo ya se encuentra disponible para todas nuestras categorías en el queryset.

from django.db.models import Value

categories = Category.objects.annotate(status=Value('Active'))

for category in categories:
    print(category.status)

>>> 'Active'
>>> 'Active'
>>> 'Active'
...
  • Los Valores que podemos usar para la clase Value son string, enteros, flotantes, decimales, booleanos y objetos de tipo date.

Algo que debemos tener presentes es que, utilizando el método annotate podemos pasar como argumentos la N cantidad de valores que deseemos.

E.g

Category.objects.annotate(
    status=Value('Active'),
    min_proucts=Value(10),
    max_products=Value(100),
)

Ese fue un ejemplo bastante sencillo, ya que estamos asignando un mismo valor para todos los objetos del queryset, pero, ¿Qué pasa si deseamos calcular operaciones a partir de objetos relacionados con mi modelo? Bueno, para ello haremos uso de métodos de agregación, como lo pueden ser Sum, Max, Min, Count or Avg. Métodos que encontraremos en django.db.models.

Veamos un par de ejemplos.

Obtengamos la categoría con más productos.

from django.db.models import Count

( 
    Category.objects.annotate(total_products=Count('products'))
    .order('-total_products')
    [0]
)

Obtengamos la categoría más cara.

from django.db.models import Sum

( 
    Category.objects.annotate(total_prices=Sum('products__price'))
    .order('-total_prices')
    [0]
)

Obtener el precio más alto de cada producto por categoría.

from django.db.models import Max

Category.objects.annotate(max_price=Max('products__price'))

Obtengamos todas las categorías con por lo menos 100 productos.

(
    Category.objects.annotate(total_products=Count('products'))
    .filter(total_products_gte=100)
)

Para este ejemplo en particular sucede algo muy interesante, ya que si tienes conocimientos de SQL sabras que, lo que estamos replicando aquí es el clásico having sobre un conteo de una agrupación. Contamos la cantidad de productos sobre una categoría, y sobre ese nuevo resultado condicionamos.

Como puedes observar, el método annotate nos retornar un nuevo queryset, con lo cual podemos seguir encadenando nuevos filtros o nuevos annotates.

De igual forma es importante mencionar que, si en dado caso queremos usar atributos de nuestro mismo modelo o queryset con annotate es possible. Para ello haremos uso de la clase F.

Por ejemplo. Generemos un nuevo atributo real_name a nuestras categorías. Donde este atributo no será más que la concatenación del “PyWombat ” + el nombre de la categoría.

from django.db.models import F, Value
from django.db.models.functions import Concat

categories = (
    Category.objects.annotate(
        real_name=Concat(
            Value('PyWombat '), F('name')
        )
    )
)

for category in categories:
    print(category.real_name)

Lo interesante de la clase F es que podemos usar un atributo en el mismo momento que se esta realizando la consulta. Estos nos permite evitar implementar algún tipo de ciclo for o algoritmo directamente con Python.

Conclusión.

Recuerda, el método annotate nos permite añadir nuevos atributos y valores a todos los objetos de un queryset. Al método annotate retornar un nuevo queryset, es posible encadenar nuevos filtros, ordenamientos o annotaciones.

Ejercicios

Si deseas practicar el tema de anotaciones con los modelos que te acabo de compartir, te invito a que resuelvas las siguientes preguntas.

  • Imprimir en consola la categoría con menos products.
  • Imprimir en consola la categoría con más productos con precios superiores a 1000.
  • Imprimir en consola la categoría con el producto más caro.