Django me llevó de los tutoriales a proyectos reales

2 de agosto de 2026 code djangolearningpython

Django es el primer framework donde un proyecto de tutorial se convirtió en algo sobre lo que seguí construyendo después en lugar de borrar la carpeta.

Había hecho el clásico tutorial de "construye un blog con Django" como hace todo el mundo, cerré la carpeta, y seguí adelante. Lo que en realidad se quedó vino después: un CRM que construí para gestionar unos datos de clientes ficticios (clientes, notas, seguimientos), una app de chat en vivo, y una web de menú de restaurante. Tres proyectos distintos, tres razones distintas por las que el enfoque de pilas incluidas de Django dejó de sentirse excesivo y empezó a sentirse como la razón por la que no tuve que construir mi propio sistema de auth por cuarta vez.

la página de lista del CRM que tardaba cuatro segundos sin motivo

La página de lista de clientes del CRM muestra cada cliente junto con cuántas notas hay en su ficha. Con unos doscientos clientes ficticios en la base de datos de pruebas, esa página tardaba más de cuatro segundos en cargar, lo cual parecía absurdo para algo que debería estar a una sola consulta de base de datos de ser instantáneo.

# views.py, the version that was slow
def customer_list(request):
    customers = Customer.objects.all()
    return render(request, "crm/customer_list.html", {"customers": customers})
{% for customer in customers %}
  <tr>
    <td>{{ customer.name }}</td>
    <td>{{ customer.notes.count }}</td>
  </tr>
{% endfor %}

Se ve completamente razonable. customer.notes.count en la plantilla, una vez por cliente. Nada aquí grita "este es el bug".

lo que en realidad encontré cuando fui a mirar

Había oído hablar de Django Debug Toolbar pero nunca me molesté en instalarla hasta que esta página me dio curiosidad de verdad. Mostró algo así al final de la página:

SQL queries: 201 (198 similar)

Doscientas una consultas, para renderizar doscientos clientes. Una consulta para obtener la lista de clientes, y luego una consulta más por cliente, disparada por ese inocente customer.notes.count en la plantilla. Cada iteración del bucle golpeaba la base de datos otra vez, por separado, porque el ORM de Django es perezoso: customer.notes no se obtiene hasta que algo lo pide de verdad, y la plantilla pidiéndolo dentro de un bucle significa pedirlo doscientas veces por separado.

lo que busqué en Google, y el término que no sabía que necesitaba

"django template loop slow multiple database queries" me llevó casi de inmediato al nombre real de esto: el problema N+1 de consultas. Una consulta para obtener N filas, luego N consultas más para obtener los datos relacionados de cada fila, una a la vez, en lugar de una sola consulta que trae todo por adelantado. En cuanto tuve el nombre, el arreglo fue una línea.

# views.py, the fixed version
from django.db.models import Count

def customer_list(request):
    customers = Customer.objects.annotate(note_count=Count("notes"))
    return render(request, "crm/customer_list.html", {"customers": customers})
{% for customer in customers %}
  <tr>
    <td>{{ customer.name }}</td>
    <td>{{ customer.note_count }}</td>
  </tr>
{% endfor %}

annotate con Count hace el conteo dentro de la única consulta a la base de datos, como un GROUP BY, en lugar de una vez por fila en Python después. Los mismos doscientos clientes, una consulta en total, el tiempo de carga de la página bajó de cuatro segundos a algo que en realidad no pude percibir que tardara nada en absoluto.

el ORM dejó de ser una caja negra por aquí

Viniendo de escribir SQL en bruto en scripts más pequeños, el ORM de Django al principio se sentía como si estuviera haciendo algo mágico y un poco sospechoso, hasta el mismísimo momento en que de verdad necesité añadir un campo al modelo de cliente del CRM cuando ya había datos reales en la base de datos, y las migraciones hicieron lo que se supone que deben hacer sin que yo tuviera que escribir a mano un ALTER TABLE.

python manage.py makemigrations
Migrations for 'crm':
  crm/migrations/0004_add_customer_notes.py
python manage.py migrate

Ejecutando migraciones, luego arrancando el servidor de desarrollo

Ese es el momento en que Django dejó de ser "un montón de archivos que todavía no entiendo" y empezó a ser una herramienta en la que confiaba para no destruir mis datos en silencio, además de ser lo que acababa de costarme en silencio doscientas consultas extra una carga de página atrás.

la app de chat en vivo me rompió el modelo mental durante toda una tarde

El ciclo normal de petición-respuesta de Django asume que llega una petición, haces algo, envías una respuesta, listo. Un chat en vivo necesita una conexión que se quede abierta, lo cual significó aprender Channels y consumers asíncronos. Primer intento, simplemente lo corrí con el servidor de desarrollo normal:

python manage.py runserver

Que arranca contento, sirve páginas normales contento, y luego no hace absolutamente nada útil en el momento en que un cliente intenta abrir una conexión WebSocket. Sin error, sin fallo, solo una conexión que nunca se actualiza. El servidor de desarrollo WSGI por defecto de Django no sabe qué hacer con una petición de actualización de protocolo en absoluto, está construido para el modelo de petición-respuesta y nada más.

# consumers.py
class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.channel_layer.group_add("chat", self.channel_name)
        await self.accept()

    async def receive(self, text_data):
        await self.channel_layer.group_send(
            "chat", {"type": "chat.message", "message": text_data}
        )

    async def chat_message(self, event):
        await self.send(text_data=event["message"])

El código del consumer en sí estaba bien, más o menos al primer intento real. El arreglo real fue darme cuenta de que necesitaba correrlo a través de Daphne, un servidor ASGI, en lugar del servidor de desarrollo WSGI normal, ya que ASGI es el protocolo que en realidad entiende las conexiones de larga duración de entrada.

daphne -p 8001 myproject.asgi:application

En cuanto tuve el servidor correcto hablando de verdad el protocolo correcto, el consumer que había parecido roto durante toda una tarde funcionó a la primera prueba real. No había roto nada fundamental del framework. Simplemente lo había estado corriendo a través del único servidor que nunca iba a funcionar para esto.

el proyecto del menú de restaurante me enseñó que el panel de admin está infravalorado

Nada complicado aquí, solo elementos de menú, categorías, precios, una página pública. La parte interesante fue darme cuenta de que el panel de admin incorporado significaba que nunca tuve que construir un CMS para que un cliente actualizara los precios él mismo.

# admin.py
from django.contrib import admin
from .models import MenuItem, Category

admin.site.register(Category)
admin.site.register(MenuItem)

Dos líneas, y de repente hay una interfaz de administración funcional y con permisos para editar cada elemento del menú, sin necesidad de construir un CMS aparte. Se sintió como hacer trampa la primera vez, de la buena manera.

los datos de demo casi salieron hacia una persona real

Un aparte pequeño y algo bochornoso: los datos ficticios del CRM que usaba para pruebas incluían un cliente llamado "Test Testerson" con un campo de nota que simplemente decía "esto es falso, ignorar". Una vez estuve a punto de enviarle a alguien una grabación de pantalla del CRM como demo sin comprobar antes qué ficha de cliente estaba en pantalla. Lo detecté antes de enviarlo, pero ahora es un hábito permanente escanear de verdad las filas visibles antes de grabar nada, no solo confiar en que los datos de prueba se quedan contenidos en mi propia pantalla.

lo que le diría a alguien que empieza con Django después de Flask

Flask, que es donde vivían sobre todo mis anteriores apps de tareas por CLI y GUI, te obliga a construirlo todo, lo cual te enseña mucho. Django asume que quieres las pilas incluidas, más rápido para un proyecto real y más lento de entender de verdad al principio, ya que hay más framework de por medio entre ti y la petición. Ambos tienen razón por motivos distintos. Simplemente necesité los tres, el CRM, la app de chat, y la web del menú, más una tarde genuinamente mala de lentitud inexplicable, antes de que la versión de "más rápido" de Django empezara a sentirse merecida en lugar de como magia que todavía no había pagado.

qué haría distinto

Instalar Django Debug Toolbar el día uno de cualquier proyecto nuevo de Django, no después de que una página ya me haya dado suficiente sospecha como para ir a buscarla. El problema N+1 llevaba ahí desde la primerísima versión de esa vista. Simplemente no tuve manera de verlo hasta que fui y conseguí una.

0 comentarios

Inicia sesión para comentar.

Iniciar sesión

¿No tienes cuenta?