Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 8 de 45

Modelos: la primera tabla

Una clase de Python que resulta ser una tabla, el SQL que Django escribe por ti, y la columna que aparece sin que la pidas.

⏱ 15 min de lectura

En la lección 47 del módulo 3 escribiste esto a mano:

sql
CREATE TABLE IF NOT EXISTS contactos (
    id       INTEGER PRIMARY KEY,
    nombre   TEXT NOT NULL,
    correo   TEXT NOT NULL UNIQUE,
    telefono TEXT NOT NULL DEFAULT '',
    grupo    TEXT NOT NULL DEFAULT 'Sin grupo',
    notas    TEXT NOT NULL DEFAULT ''
);

Y después, cada consulta con su cadena de SQL, sus interrogantes para los parámetros y su conversión de filas a diccionarios. Funcionaba bien. Lo que no aguanta es el cambio: añadir una columna obligaba a tocar el CREATE TABLE, todos los INSERT, todos los SELECT y el código que leía las filas, y no había nada que avisara si se te olvidaba uno.

Un modelo es esa misma tabla escrita como una clase de Python. De esa clase salen el SQL que la crea, las consultas, la conversión a objetos y los formularios. Esta lección escribe el primero.

La correspondencia#

Es de tres líneas y conviene tenerla clara desde el principio, porque todo el bloque cuelga de ella:

En PythonEn la base de datos
Una clase que hereda de models.ModelUna tabla
Un atributo de clase que es un campoUna columna
Una instancia de esa claseUna fila

Eso es un ORM, de object-relational mapper: la pieza que traduce entre objetos y tablas en los dos sentidos. Lo que ganas no es escribir menos SQL, que también: lo que ganas es que hay un solo sitio donde está definido lo que es un contacto. El día que añadas un campo, lo añades ahí y el resto se entera.

El modelo#

En contactos/models.py, que startapp dejó casi vacío:

contactos/models.py
from django.db import models


class Contacto(models.Model):
        nombre = models.CharField(max_length=100)
    correo = models.EmailField(unique=True)
    telefono = models.CharField(max_length=20, blank=True)
    grupo = models.CharField(max_length=30, default='Sin grupo')
    notas = models.TextField(blank=True)
    creado = models.DateTimeField(auto_now_add=True)

    def __str__(self):
                return self.nombre

Compáralo con el CREATE TABLE de arriba: son las mismas cinco columnas, con la misma información, y una nueva. Punto por punto:

El nombre de la clase va en singular. Contacto, no Contactos. La clase representa un contacto, y la tabla la nombra Django. Es la convención universal en Django y conviene seguirla porque el panel de administración y los mensajes de error la dan por hecha al construir plurales.

Hereda de models.Model. De ahí le vienen el save(), el delete(), el objects con el que se consulta y la maquinaria que detecta los campos.

Cada campo es una clase distinta, no un tipo con opciones. CharField para texto corto, TextField para texto largo, EmailField para un correo, DateTimeField para una fecha con hora. Hay unos veinticinco y son la lección siguiente; aquí basta con cinco.

max_length es obligatorio en CharField y no lo es en TextField. No es un capricho: CharField se traduce a un VARCHAR(n) y ese n hay que decirlo. Si lo olvidas, makemigrations falla antes de tocar nada.

creado no estaba en la versión de Tkinter. auto_now_add=True hace que Django ponga la fecha actual al crear la fila y no la toque nunca más. Es de esas cosas que en SQL a mano se olvidan y luego se echan de menos.

__str__ no es decorativo. Es el método del módulo 1 de toda la vida, y aquí es lo que se ve en el panel de administración, en los desplegables de los formularios y al imprimir un objeto. Sin él, Django enseña Contacto object (1), que no dice nada. Se pone siempre, en todos los modelos, desde el primer día.

Convertirlo en una tabla#

El modelo por sí solo no crea nada. Son dos órdenes, y tienen su lección entera más adelante:

bash
python manage.py makemigrations
Salida en pantalla
Migrations for 'contactos':
  contactos/migrations/0001_initial.py
    + Create model Contacto
bash
python manage.py migrate
Salida en pantalla
Applying contactos.0001_initial... OK

La primera escribe un fichero que describe el cambio; no toca la base de datos. La segunda lo aplica. Están separadas a propósito, porque ese fichero se guarda en el repositorio y es lo que permite que la base de datos de otra persona, o la del servidor, acabe exactamente igual que la tuya.

Ese SQL tiene tres cosas que no has escrito tú y merecen explicación.

La columna id#

No la has declarado y está ahí, como clave primaria y autoincremental. Django añade una a todo modelo que no declare la suya, porque toda fila necesita algo que la identifique. Se puede usar de dos formas:

python
contacto.id
contacto.pk

pk es un alias de "clave primaria" que apunta a la que haya. Conviene usar pk en el código general, porque si algún día el modelo declara su propia clave primaria con otro nombre, el código que dice pk sigue funcionando.

El nombre de la tabla#

contactos_contacto. Django lo construye juntando el nombre de la aplicación y el de la clase en minúsculas, y así dos aplicaciones pueden tener cada una su Contacto sin chocar.

Se puede cambiar, y es lo que hay que hacer cuando el modelo se apoya en una tabla que ya existe:

python
class Contacto(models.Model):
        ...

    class Meta:
                db_table = 'contactos'

Esa clase Meta de dentro guarda los ajustes que son de la tabla entera y no de un campo. Tiene su lección, la 15, con el orden por defecto, el nombre en plural y las restricciones.

El NOT NULL de notas#

Aquí está el detalle que más despista del modelo. notas tiene blank=True y su columna ha salido NOT NULL. Parece una contradicción y no lo es, porque blank y null son dos cosas distintas y solo una es de la base de datos:

OpciónDe quién esQué significa
null=TrueDe la base de datosLa columna admite NULL
blank=TrueDe los formulariosEl campo se puede dejar vacío al rellenarlo

notas con blank=True y sin null=True significa: en el formulario no es obligatorio, y cuando no se rellena se guarda la cadena vacía. No un NULL, una cadena vacía.

Y eso es lo que se quiere. La regla que evita disgustos:

En los campos de texto, blank=True sí y null=True no.

El motivo es que con los dos puestos habría dos maneras de decir "no hay teléfono": la cadena vacía y el NULL. Entonces telefono = '' y telefono = None son cosas distintas para la base de datos, las consultas tienen que preguntar por las dos, y tarde o temprano una se olvida. En campos que no son de texto —un número, una fecha— no hay cadena vacía que valga, y ahí null=True sí es la forma correcta de decir que no hay valor. La lección siguiente entra en esto campo por campo.

Probarlo#

El intérprete de Django tiene su lección, la 11. Un adelanto para ver que la tabla existe y funciona:

bash
python manage.py shell
consola
>>> Contacto.objects.create(nombre='Ana Ruiz', correo='ana@ejemplo.es', grupo='Trabajo')
<Contacto: Ana Ruiz>
>>> c = Contacto.objects.get(correo='ana@ejemplo.es')
>>> c.pk
1
>>> c.notas
''
>>> c.creado
datetime.datetime(2026, 9, 22, 16, 16, 47, 992908, tzinfo=datetime.timezone.utc)

Cuatro cosas en esas líneas. El <Contacto: Ana Ruiz> sale de tu __str__. El pk lo ha puesto la base de datos al insertar. notas, que no se ha dado, es la cadena vacía y no None, como acaba de explicarse. Y creado viene en UTC, con su tzinfo, porque USE_TZ está activado: Django guarda siempre en UTC y convierte a TIME_ZONE solo al mostrar.