Módulo 4 · Lección 15 de 45
Métodos del modelo, Meta y __str__
Cómo dejar de repartir la lógica de un contacto por todas las vistas, y los ajustes que valen para la tabla entera.
Un modelo no es solo una descripción de columnas. Es una clase de Python, y puede tener métodos, propiedades y validación como cualquier otra. Esta lección cierra el bloque con lo que va dentro de la clase pero no es un campo: los métodos que dan lógica, la clase Meta con los ajustes de la tabla entera, y las restricciones que por fin obligan a la base de datos a cumplir lo que los campos solo sugerían.
Hay una razón de fondo para tomárselo en serio, y es la misma de la lección 41 del módulo 3, cuando se separó la lógica de la interfaz de Tkinter: si el modelo no sabe hacer nada, todo lo que sabe hacer un contacto acaba repartido entre las vistas, y la tercera vez que hay que calcular lo mismo, alguien lo calcula distinto.
Métodos#
Son métodos normales. Se les pasa self, que es la fila, y pueden usar sus campos y sus relaciones:
class Contacto(models.Model):
...
def __str__(self):
return self.nombre
@property
def iniciales(self):
return ''.join(parte[0].upper() for parte in self.nombre.split())
def tiene_contacto_directo(self):
return bool(self.telefono or self.correo)>>> Contacto.objects.first().iniciales
'AR'La diferencia entre un método y una @property es que la propiedad se usa sin paréntesis. Eso importa más de lo que parece, porque las plantillas de Django no permiten llamar a funciones con argumentos: {{ contacto.iniciales }} funciona con las dos formas, pero cualquier cosa que necesite un argumento hay que resolverla con una etiqueta de plantilla, que es más trabajo. La costumbre es que lo que se vaya a enseñar sea una propiedad o un método sin argumentos.
Los tres métodos que Django conoce por su nombre#
def __str__(self):
return self.nombreYa está dicho desde la lección 8 y se repite porque es el que más rendimiento da por línea escrita: es lo que se ve en el panel de administración, en los desplegables de los formularios y al depurar.
def get_absolute_url(self):
from django.urls import reverse
return reverse('contactos:detalle', args=[self.pk])Contesta a "¿cuál es la dirección de este objeto?", y hay media docena de sitios en Django que la llaman sola: el panel de administración pone un botón de "ver en el sitio", las vistas de crear y editar redirigen ahí después de guardar, y en las plantillas basta con {{ contacto.get_absolute_url }}. Necesita rutas con nombre, que es lo de la lección 21, y se añade en cuanto existan.
def clean(self):
...Es el sitio de la validación que afecta a varios campos a la vez, y lo llama full_clean(). Se ve en el bloque de formularios.
La clase Meta#
Dentro del modelo, una clase llamada Meta con los ajustes que son de la tabla entera:
class Contacto(models.Model):
...
class Meta:
verbose_name = 'contacto'
verbose_name_plural = 'contactos'
ordering = ['nombre']| Opción | Para qué |
|---|---|
ordering | Orden por defecto de todas las consultas |
verbose_name y verbose_name_plural | Cómo se llama el modelo en el panel y en los mensajes |
db_table | Cambiar el nombre de la tabla |
indexes | Índices propios |
constraints | Restricciones que cumple la base de datos |
unique_together | Combinación de campos que no se repite |
abstract | Que la clase sea solo para heredar de ella, sin tabla |
permissions | Permisos propios, para el bloque E |
verbose_name_plural existe porque el plural en inglés no siempre es añadir una ese, y Django, que lo hace así, se equivoca con casi todo en español: "contactos" le saldría bien por casualidad, pero "direccións" y "mess" no. Con el idioma en es-es, ponerlo es la diferencia entre un panel de administración cuidado y uno que da vergüenza enseñar.
ordering tiene un efecto que hay que medir antes de ponerlo:
>>> Contacto.objects.values_list('nombre', flat=True)
['Ana Ruiz', 'Luis Pardo', 'Por id', 'Sin grupo', 'Zoe Alvarez']Salen ordenados sin que nadie lo haya pedido. Eso es cómodo y tiene dos contrapartidas. La primera es que cada consulta lleva ahora un ORDER BY, incluidas las que no lo necesitan, y ordenar cuesta. La segunda es que ese orden es el de la base de datos, con los tres problemas del español de la lección 12.
Aun así conviene ponerlo, por un motivo concreto: paginar sin un orden definido devuelve resultados incoherentes. Sin ORDER BY, el gestor no garantiza que la página dos sea la continuación de la página uno, y una fila puede salir en las dos o en ninguna. Django avisa de esto con un UnorderedObjectListWarning en cuanto pagines, que es la lección 36.
Índices#
Un índice hace rápidas las búsquedas por una columna a cambio de ocupar espacio y ralentizar un poco las escrituras. Se declaran en Meta:
class Meta:
indexes = [
models.Index(fields=['grupo', 'nombre'], name='idx_grupo_nombre'),
]Ese es un índice compuesto, y el orden de los campos importa: sirve para filtrar por grupo, y para filtrar por grupo y ordenar por nombre, pero no para filtrar solo por nombre. Es como un listín ordenado por provincia y luego por apellido: para buscar por apellido sin saber la provincia no ayuda.
Los índices no se ponen por si acaso. Django ya crea los que casi siempre hacen falta, que son los de las claves primarias, los de las claves ajenas y los de los campos con unique. Se añade uno cuando una consulta concreta va lenta y se ha comprobado que es por ahí.
Restricciones#
Aquí se cierra algo que ha quedado pendiente en dos lecciones. En la 8 se vio que save() no valida; en la 9, que choices no genera ninguna restricción. La conclusión era que quien protege los datos es el formulario. Eso vale para lo que escribe una persona y no vale para lo que escribe tu código, una migración o una carga masiva.
Lo único que se cumple siempre es lo que vive en la base de datos:
class Meta:
constraints = [
models.CheckConstraint(
condition=models.Q(nombre__gt=''),
name='nombre_no_vacio',
),
]>>> Contacto.objects.create(nombre='', correo='vacio@e.es')
IntegrityError: CHECK constraint failed: nombre_no_vacioAhora sí. Ni create(), ni update(), ni un INSERT a mano pueden meter un contacto sin nombre.
Hay dos tipos principales:
constraints = [
models.CheckConstraint(condition=models.Q(nombre__gt=''), name='nombre_no_vacio'),
models.UniqueConstraint(fields=['nombre', 'grupo'], name='nombre_unico_por_grupo'),
]UniqueConstraint es la forma moderna de unique_together, y admite cosas que aquella no: una condición para que la unicidad solo se aplique a algunas filas, o unicidad sobre una expresión.
Qué va en el modelo y qué no#
Para cerrar el bloque, el criterio, que es el mismo de la lección 41 del módulo 3:
Va en el modelo todo lo que sea verdad sobre un contacto sin importar quién pregunte: las iniciales, si está completo, cómo se muestra, cuál es su dirección. Si dos vistas necesitan lo mismo, va en el modelo.
No va en el modelo nada que sepa de HTTP. Un modelo que lee request, que devuelve HTML o que sabe qué página estás mirando ha cruzado la frontera, y a partir de ahí no se puede probar sin montar una petición ni reutilizar desde un guion de línea de comandos.
La prueba práctica es esta: un modelo tiene que poder usarse entero desde manage.py shell. Todo lo que has hecho en este bloque ha sido desde ahí, sin una sola página de por medio, y esa es exactamente la propiedad que hay que conservar cuando en el bloque siguiente empiecen a aparecer vistas.