Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 9 de 45

Tipos de campo y sus opciones

Los quince campos que se usan de verdad, las ocho opciones que aceptan casi todos, y las tres decisiones que luego cuesta cambiar.

⏱ 18 min de lectura

Django trae treinta y nueve clases de campo. Se comprueba en dos líneas:

consola
>>> from django.db import models
>>> [n for n in dir(models) if n.endswith('Field')]

De esas treinta y nueve, la mitad son variantes de tamaño de otras y tres son relaciones, que tienen sus propias lecciones. Las que vas a escribir de verdad son unas quince, y lo que de verdad hay que aprender no son los tipos, que se adivinan por el nombre, sino las opciones, que son casi las mismas para todos y son las que deciden cómo se comporta el campo en la base de datos, en los formularios y en el panel de administración.

Esta lección es de referencia, pero no es una lista: está montada alrededor de las tres decisiones que cuestan trabajo deshacer si se toman mal, que son blank frente a null, Decimal frente a float, y cómo se declaran las opciones cerradas.

Los campos y en qué se convierten#

Esta tabla está sacada del SQL que genera sqlmigrate para un modelo con todos ellos, en SQLite. Otros gestores cambian el tipo concreto, no el sentido:

CampoPara quéSQL que genera
CharField(max_length=n)Texto cortovarchar(n)
TextField()Texto largo, sin límitetext
EmailField()Un correovarchar(254)
URLField()Una dirección webvarchar(200)
SlugField()Un identificador para las URLvarchar(50) con índice
IntegerField()Un enterointeger
PositiveIntegerField()Un entero que no baja de cerointeger unsigned CHECK (>= 0)
BooleanField()Sí o nobool
DecimalField(max_digits, decimal_places)Números exactos: dinerodecimal
FloatField()Números aproximados: medidasreal
DateField()Una fechadate
DateTimeField()Fecha y horadatetime
DurationField()Un intervalo de tiempobigint
UUIDField()Un identificador únicochar(32)
JSONField()Datos sin estructura fijatext CHECK (JSON_VALID(...))
FileField() / ImageField()Un fichero subidovarchar(100) con la ruta

Tres detalles que se ven mejor en esa columna de la derecha que en cualquier explicación:

  • Los campos especializados son un CharField con extras. EmailField es un varchar con un validador y un formulario distinto; en la base de datos no hay ningún tipo "correo". Lo mismo URLField y SlugField.
  • SlugField crea un índice él solo. Es el único de la lista que lo hace, porque su razón de ser es aparecer en las URL y por tanto buscarse constantemente.
  • PositiveIntegerField pone una restricción de verdad, no solo un validador de formulario. Un negativo no entra ni forzándolo desde el código.

Las opciones comunes#

Casi todos los campos aceptan estas ocho:

OpciónQué haceDónde se nota
nullLa columna admite NULLBase de datos
blankEl campo se puede dejar vacíoFormularios
defaultValor si no se da ningunoAl crear el objeto
uniqueNo puede repetirseBase de datos
choicesSolo estos valoresFormularios y panel
verbose_nameEl nombre legibleFormularios y panel
help_textUna explicación debajo del campoFormularios y panel
db_indexCrea un índiceBase de datos

La columna de la derecha es la que ordena el asunto: hay opciones que cambian la tabla y opciones que solo cambian lo que ve una persona. Las primeras obligan a una migración; las segundas también generan migración, pero no tocan el SQL.

verbose_name es el primer argumento posicional, así que estas dos líneas son lo mismo:

python
telefono = models.CharField('teléfono', max_length=20, blank=True)
telefono = models.CharField(max_length=20, blank=True, verbose_name='teléfono')

Si no lo pones, Django usa el nombre del atributo cambiando los guiones bajos por espacios. Con nombres en español sin tildes, como telefono, eso significa que el formulario dirá "telefono". Merece la pena ponerlo cuando la tilde importa.

Decisión 1: blank o null#

La lección anterior dejó la regla enunciada; aquí está completa, porque es la que más se equivoca.

null es de la base de datos. blank es de los formularios. Son independientes y se pueden combinar de las cuatro maneras:

CombinaciónSignifica
NingunaObligatorio en todas partes
blank=TrueOpcional en el formulario; se guarda el valor vacío del tipo
null=TrueLa columna admite NULL, pero el formulario lo exige igual
Las dosOpcional, y cuando no se da se guarda NULL

La tercera fila casi nunca es lo que quieres, y es el resultado típico de poner null=True creyendo que era lo que hacía el campo opcional.

La regla práctica, según el tipo:

Texto (CharField, TextField, EmailField...): blank=True sí, null=True no. Todo lo demás (números, fechas, booleanos, relaciones): las dos juntas.

El motivo es el de la lección anterior: en un campo de texto hay ya un valor que significa "vacío", que es la cadena vacía, y añadir NULL deja dos formas distintas de decir lo mismo. A partir de ahí, filter(telefono='') y filter(telefono=None) devuelven cosas distintas, cada consulta tiene que acordarse de las dos, y alguna se olvida. En un DateField no hay "fecha vacía", así que NULL es la única manera de decir que no la hay.

Decisión 2: Decimal o float#

Para el dinero, DecimalField. Siempre. La razón la conoces de la lección 6 del módulo 1:

consola
>>> 0.1 + 0.2
0.30000000000000004
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')

Un float es binario y no puede representar exactamente la mayoría de los decimales. En una medida da igual, porque el error es más pequeño que el instrumento. En una factura con diez mil líneas, esos errores se suman y el total no cuadra con la suma de sus partes, que es la clase de fallo que aparece en una auditoría y no en las pruebas.

python
precio = models.DecimalField(max_digits=8, decimal_places=2)

Los dos argumentos son obligatorios. max_digits son las cifras en total, contando las decimales; decimal_places, cuántas van detrás de la coma. Con 8 y 2 caben hasta 999999.99.

Y un detalle que sorprende al leer de la base de datos:

consola
>>> Demo.objects.create(decimal=Decimal('0.1')).decimal
Decimal('0.1')
>>> Demo.objects.get(pk=1).decimal
Decimal('0.10')

Al volver de la base de datos tiene los dos decimales, porque la columna los tiene. Son el mismo número y == los considera iguales, pero si comparas cadenas o textos representados, no coinciden.

Decisión 3: las opciones cerradas#

El campo grupo de la agenda solo admite unos cuantos valores. En el módulo 3 eso era una tupla suelta:

python
GRUPOS = ("Sin grupo", "Familia", "Amigos", "Trabajo", "Proveedores")

En Django se declara con choices, y la forma moderna es una clase:

contactos/models.py
from django.db import models


class Grupo(models.TextChoices):
        SIN = 'sin', 'Sin grupo'
    FAMILIA = 'familia', 'Familia'
    AMIGOS = 'amigos', 'Amigos'
    TRABAJO = 'trabajo', 'Trabajo'
    PROVEEDORES = 'proveedores', 'Proveedores'


class Contacto(models.Model):
        nombre = models.CharField('nombre', max_length=100)
    correo = models.EmailField('correo', unique=True)
    telefono = models.CharField('teléfono', max_length=20, blank=True)
    grupo = models.CharField('grupo', max_length=15, choices=Grupo, default=Grupo.SIN)
    notas = models.TextField('notas', blank=True)
    creado = models.DateTimeField('creado', auto_now_add=True)

    def __str__(self):
                return self.nombre

Cada línea de Grupo tiene dos partes: lo que se guarda y lo que se enseña. Esa separación es lo importante. En la base de datos va 'trabajo', corto y estable; en pantalla sale Trabajo, que se puede traducir o cambiar sin migrar nada.

TextChoices hereda de Enum, así que en el código escribes Grupo.TRABAJO en vez de la cadena 'trabajo', y una errata deja de ser un dato mal guardado para ser un AttributeError inmediato.

Django añade además un método por cada campo con choices:

consola
>>> c = Contacto.objects.create(nombre='Ana', correo='ana@ejemplo.es', grupo=Grupo.TRABAJO)
>>> c.grupo
'trabajo'
>>> c.get_grupo_display()
'Trabajo'

get_grupo_display() se construye solo, con el nombre del campo en medio. Es lo que se usa en las plantillas, porque el valor crudo no es para enseñarlo.

Las fechas automáticas#

Tres formas de poner una fecha, y se confunden:

OpciónCuándo se pone¿Se puede cambiar?
auto_now_add=TrueSolo al crear la filaNo, el campo se vuelve de solo lectura
auto_now=TrueEn cada save()No
default=timezone.nowAl crear, si no se da otraSí

Las dos primeras son cómodas y tienen un precio: el campo deja de ser editable, no sale en los formularios y no se puede corregir a mano. Para "creado el" y "modificado el" es exactamente lo que se quiere. Para una "fecha de la cita", que alguien tiene que poder elegir y cambiar, hay que usar la tercera.

python
from django.utils import timezone

creado = models.DateTimeField(auto_now_add=True)
modificado = models.DateTimeField(auto_now=True)
fecha_cita = models.DateField(default=timezone.now, null=True, blank=True)

Fíjate en timezone.now sin paréntesis. Se pasa la función, para que Django la llame en el momento de crear cada fila; con paréntesis se pasaría la hora del arranque del servidor, la misma para siempre. Y es django.utils.timezone.now, no datetime.now, por lo de USE_TZ que se explicó en la lección 5: la primera devuelve una fecha con huso y la segunda no.

Lo que no se cambia luego sin dolor#

Para cerrar, lo que conviene pensar dos veces, porque cambiarlo con datos dentro es trabajo:

  • Meter un unique=True en un campo que ya tiene duplicados. La migración falla al aplicarse, no al crearse. Hay que limpiar los datos antes.
  • Bajar el max_length. En SQLite no pasa nada porque no lo comprueba, y en PostgreSQL la migración falla si hay filas más largas. La diferencia entre gestores es de las que se descubren el día del despliegue.
  • Cambiar los valores de choices. Cambiar la etiqueta es gratis; cambiar lo que se guarda obliga a una migración que actualice las filas existentes.
  • Pasar un campo de null=True a obligatorio. makemigrations te preguntará qué poner en las filas que ya tienen NULL, y hay que tener la respuesta.

Ninguna es un callejón sin salida. Todas se resuelven con lo de la lección siguiente, que son precisamente las migraciones.