Índice de contenido
Vamos ahora con las relaciones inversas o invertidas. Entiéndase que tenemos el modelo de Categorías y queremos obtener sus Elementos relacionados; esto es perfectamente factible en Django.
Esto es un tema que viene muy ligado al problema de N+1 que presentamos antes, ya que, es debido a las relaciones.
Para esta prueba, no utilizaremos el proyecto completo. Voy a realizar la consulta directamente en la vista para no perder tiempo creando un template adicional. Obtendremos todas las categorías y practicaremos cómo acceder a sus datos vinculados:
categorias = Category.objects.prefetch_related('element_set').all()
for cat in categorias:
print(f"Categoría: {cat.title}")
# Aquí Django hace una consulta a la DB por CADA categoría para buscar sus elementos
for el in cat.element_set.all():
print(f" - Elemento: {el.title}")Accediendo al _set por Defecto
Si tenemos una relación donde un Elemento pertenece a una Categoría, y estamos parados en el objeto Categoría, ¿cómo obtenemos sus elementos? Esa es la relación inversa:
class Category(models.Model):
***
class Element(models.Model):
***
category = models.ForeignKey(Category, on_delete=models.CASCADE)Por defecto, Django nombra esta relación como nombre_del_modelo_minúscula_set (en este caso, element_set). No tenemos que configurar nada extra. Es el proceso inverso a lo que hacíamos antes: si antes obteníamos la categoría desde el elemento, ahora obtenemos los elementos desde la categoría.
Personalización con related_name
Si el nombre element_set no te gusta, puedes personalizarlo en el modelo usando el atributo related_name. Por ejemplo:
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='elements')Si haces esto, Django ya no reconocerá element_set, sino que deberás usar simplemente elements. Es opcional, pero muy útil para que el código sea más legible:
for el in cat.elements.all():
print(f" - Elemento: {el.title}")El Problema de las Consultas N+1
Para ver qué ocurre tras bambalinas, activé el log de SQL en la consola. Si no optimizamos la consulta, al acceder a la relación inversa dentro de un bucle, Django realiza una consulta a la base de datos por cada categoría para buscar sus elementos. Esto se conoce como el problema de N+1.
Si tienes 50 categorías, ¡Django hará 51 consultas! Esto puede matar el rendimiento de tu servidor. Al revisar la consola, verás un montón de sentencias SELECT repetitivas, lo cual es una clara señal de alerta.
DEBUG (0.000) SELECT COUNT(*) AS "__count" FROM "elements_element"; args=(); alias=default
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" LIMIT 8; args=(); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHERE "elements_category"."id" = 1 LIMIT 21; args=(1,); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHERE "elements_category"."id" = 1 LIMIT 21; args=(1,); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHERE "elements_category"."id" = 1 LIMIT 21; args=(1,); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHERE "elements_category"."id" = 1 LIMIT 21; args=(1,); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHERE "elements_category"."id" = 1 LIMIT 21; args=(1,); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHER" FROM "elements_category" WHERE "elements_category"."id" = 1 LIMIT 21; args=(1,); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHERE "elements_category"."id" = 2 LIMIT 21; args=(2,); alias=default
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category" WHERE "elements_category"."id" = 2 LIMIT 21; args=(2,); alias=defaultOptimización con prefetch_related
¿Cómo solucionamos esto sin depender de configuraciones globales? Usamos prefetch_related. A diferencia de select_related (que usa un JOIN), prefetch_related realiza una segunda consulta separada para traer todos los elementos relacionados de una sola vez y luego los une en Python.
Ejemplo de uso:
categories = Category.objects.prefetch_related('element_set').all()Al ejecutar esto y revisar los logs, verás que ahora solo se realizan dos consultas:
Categoría: Cate 1.1
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" = 1; args=(1,); alias=default
- Elemento: Test 1
- Elemento: ELement 2
- Elemento: asasas
- Elemento: Elem 3asas
- Elemento: asasasa
- Elemento: ELem 1sasas
Categoría: Cate 2
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" = 2; args=(2,); alias=default
- Elemento: asasas
- Elemento: Element
Categoría: asaas
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" = 4; args=(4,); alias=default
Categoría: Laravel
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" = 7; args=(7,); alias=default
Categoría: FastAPI
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" = 8; args=(8,); alias=default
Categoría: Cate 5
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" = 9; args=(9,); alias=default
Categoría: cate-6
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" = 10; args=(10,); alias=default
Categoría: Django 6Una para traer todas las categorías.
Un SELECT anidado (o con un IN) que trae todos los elementos relacionados con esas categorías.
De esta forma, pasamos de tener cientos de consultas a solo dos, optimizando drásticamente el acceso a la base de datos:
DEBUG (0.000) SELECT "elements_category"."id", "elements_category"."title", "elements_category"."slug" FROM "elements_category"; args=(); alias=default
DEBUG (0.000) SELECT "elements_element"."id", "elements_element"."title", "elements_element"."slug", "elements_element"."description", "elements_element"."price", "elements_element"."category_id", "elements_element"."created", "elements_element"."updated", "elements_element"."type_id" FROM "elements_element" WHERE "elements_element"."category_id" IN (1, 2, 4, 7, 8, 9, 10, 11, 12); args=(1, 2, 4, 7, 8, 9, 10, 11, 12); alias=default