Django 6.1 incorpora dos cambios del ORM con un objetivo común: dejar que la base de datos haga trabajo útil sin volver opaca la aplicación. Los fetch modes cambian lo que ocurre cuando un campo se carga bajo demanda. DB_CASCADE mueve una eliminación en cascada de Python a una restricción SQL. Ambos pueden eliminar mucho trabajo, pero activarlos sin examinar sus efectos secundarios es arriesgado.
Las notas de la versión Django 6.1 presentan estas API como optimizaciones. En la práctica, cambian dónde se realiza el trabajo. Ese es el punto clave antes de actualizar.
Los fetch modes gestionan los campos que no se cargaron
Un queryset no siempre carga todo. Una ForeignKey se resuelve en el primer acceso si no se incluyó con select_related(). Del mismo modo, only() y defer() dejan campos fuera de una instancia de forma intencionada.
Hasta Django 6.0, acceder a una relación ausente se comportaba igual en todas partes: una consulta por instancia. Django 6.1 hace explícita esa decisión con QuerySet.fetch_mode().
from django.db import models
books = Book.objects.fetch_mode(models.FETCH_PEERS)
for book in books:
print(book.author.name)
El modo no se limita a ForeignKey. También se aplica a OneToOneField, a sus accesores inversos, a campos diferidos con only() o defer(), y a relaciones genéricas. Django también copia el modo a los objetos relacionados que carga, algo importante cuando el código recorre varias relaciones.
FETCH_ONE conserva el comportamiento existente
FETCH_ONE es el valor predeterminado. El primer book.author carga únicamente el autor de ese libro. Por tanto, un bucle de 100 libros puede emitir 101 consultas: una para los libros y una para cada autor ausente.
books = Book.objects.fetch_mode(models.FETCH_ONE)
for book in books:
print(book.author.name) # potencialmente 1 + N consultas
Sigue siendo una buena opción cuando solo se usará una instancia o cuando es poco probable acceder a otras relaciones. Además conserva una propiedad importante: acceder a una relación sigue siendo local y predecible.
FETCH_PEERS convierte N+1 en una carga por lote
FETCH_PEERS hace una suposición: si el código lee un campo de un objeto de un lote, probablemente lo leerá en los demás. En el primer acceso a book.author, Django obtiene los autores ausentes de todas las instancias que proceden del mismo queryset.
Para el ejemplo anterior, normalmente son dos consultas: una para los libros y otra para los autores. Es una carga bajo demanda, similar a prefetch_related(), pero solo se activa porque el código necesita de verdad la relación.
books = Book.objects.fetch_mode(models.FETCH_PEERS)
for book in books:
render_book(book.title, book.author.name)
Esto ayuda en capas de renderizado, serializers y reglas de negocio donde es difícil anticipar las relaciones que se consultarán. Django sigue los pares mediante referencias débiles, por lo que las instancias descartadas no permanecen en memoria.
No es una razón para dejar de diseñar querysets. Si una vista siempre muestra un autor, select_related("author") sigue expresando mejor la intención y evita la segunda consulta. FETCH_PEERS es especialmente útil cuando el acceso es condicional o está repartido entre capas, no como sustituto global de una optimización dirigida. Para elegir entre joins y precarga explícita, consulta select_related() frente a prefetch_related().
Tampoco sustituye la precarga de colecciones. Las consultas de gestores inversos, como book.review_set.all(), no se ven afectadas por los fetch modes. Cuando una colección uno-a-muchos o muchos-a-muchos se muestra en un bucle, prefetch_related() sigue siendo la herramienta correcta.
FETCH_RAISE hace visible una regresión
En una ruta crítica, el mejor N+1 suele ser el que hace fallar una prueba. FETCH_RAISE lanza FieldFetchBlocked cuando un acceso provocaría una consulta implícita.
from django.db import models
from django.core.exceptions import FieldFetchBlocked
books = Book.objects.fetch_mode(models.FETCH_RAISE)
for book in books:
print(book.author.name) # FieldFetchBlocked si author no se cargó
Úsalo en una prueba de rendimiento, una exportación grande o una vista en la que toda relación debe declararse. La corrección consiste en cargar explícitamente lo necesario con select_related("author"), prefetch_related("reviews") o incluyendo un campo en only().
Un manager también puede definir una política predeterminada:
class BookManager(models.Manager):
def get_queryset(self):
return super().get_queryset().fetch_mode(models.FETCH_PEERS)
class Book(models.Model):
author = models.ForeignKey("Author", on_delete=models.CASCADE)
objects = BookManager()
Reservaría este patrón para modelos cuyas relaciones se exploran de forma realmente impredecible. Imponerlo globalmente puede cargar datos innecesarios en comandos, tareas y API cuyo perfil es diferente al de una página HTML.
DB_CASCADE delega por fin la cascada en la base de datos
El models.CASCADE existente está controlado por el recolector de borrado de Django. Emula una restricción ON DELETE CASCADE y carga los objetos necesarios antes de borrarlos. Las señales pre_delete y post_delete se envían para cada objeto eliminado, aunque Model.delete() no se llame en cada hijo.
Django 6.1 introduce models.DB_CASCADE: la migración crea una restricción SQL ON DELETE CASCADE y la base de datos elimina por sí misma las filas dependientes.
from django.db import models
class Order(models.Model):
customer = models.ForeignKey(
"Customer",
on_delete=models.DB_CASCADE,
related_name="orders",
)
Al borrar un cliente con un gran número de pedidos, la diferencia puede ser importante. Django ya no necesita cargar pedidos para organizar la cascada. La base ejecuta la operación junto a sus índices y restricciones.
La contrapartida es una decisión de dominio: DB_CASCADE no envía las señales pre_delete ni post_delete para las filas que borra la base. Una auditoría, el borrado de un archivo, la invalidación de caché o la publicación de un evento implementados en esas señales dejarán de ejecutarse.
Las tres opciones DB_* y sus restricciones
Las opciones disponibles son DB_CASCADE, DB_SET_NULL y DB_SET_DEFAULT. Corresponden a las estrategias gestionadas por Python CASCADE, SET_NULL y SET_DEFAULT, pero la acción la ejecuta la base de datos.
class Ticket(models.Model):
assignee = models.ForeignKey(
"User",
null=True,
on_delete=models.DB_SET_NULL,
)
class Event(models.Model):
project = models.ForeignKey(
"Project",
db_default=1,
on_delete=models.DB_SET_DEFAULT,
)
DB_SET_NULL sigue necesitando null=True. DB_SET_DEFAULT requiere un db_default, no solo un default de Python, porque el servidor SQL debe conocer el valor. No es compatible con MySQL ni MariaDB.
La restricción más fácil de pasar por alto es estructural. Django prohíbe mezclar variantes DB_* con variantes gestionadas por Python en el mismo modelo y en los modelos relacionados, excepto DO_NOTHING. Antes de sustituir un CASCADE, mapea toda la cadena de claves foráneas, no solo el campo que cambias.
Una estrategia de migración razonable
Empieza por rutas críticas medidas. Para las lecturas, activa FETCH_RAISE en una prueba o en un queryset de exportación para encontrar accesos implícitos, y después elige entre precarga explícita y FETCH_PEERS. La documentación de fetch modes de Django no elimina la necesidad de medir SQL y memoria.
Para las eliminaciones, busca primero las señales conectadas a los modelos hijo. Si una señal desencadena un efecto de dominio, conserva CASCADE o mueve ese efecto a un servicio explícito antes de adoptar DB_CASCADE. Después inspecciona la migración generada en cada motor de base de datos que soporte tu aplicación.
FETCH_PEERS y DB_CASCADE son útiles porque hacen más visibles los compromisos. El primero decide cuándo agrupar lecturas. El segundo decide si Django o la base posee los efectos de una eliminación. No son interruptores para activar en todas partes. Son decisiones que deben tomarse al nivel del queryset y del modelo.
