Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 16 min de lectura

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:

contactos/models.py
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)
consola
>>> 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#

python
def __str__(self):
        return self.nombre

Ya 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.

python
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.

python
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:

contactos/models.py
class Contacto(models.Model):
        ...

    class Meta:
                verbose_name = 'contacto'
        verbose_name_plural = 'contactos'
        ordering = ['nombre']
OpciónPara qué
orderingOrden por defecto de todas las consultas
verbose_name y verbose_name_pluralCómo se llama el modelo en el panel y en los mensajes
db_tableCambiar el nombre de la tabla
indexesÍndices propios
constraintsRestricciones que cumple la base de datos
unique_togetherCombinación de campos que no se repite
abstractQue la clase sea solo para heredar de ella, sin tabla
permissionsPermisos 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:

consola
>>> 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:

python
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:

python
class Meta:
        constraints = [
        models.CheckConstraint(
            condition=models.Q(nombre__gt=''),
            name='nombre_no_vacio',
        ),
    ]
consola
>>> Contacto.objects.create(nombre='', correo='vacio@e.es')
IntegrityError: CHECK constraint failed: nombre_no_vacio

Ahora sí. Ni create(), ni update(), ni un INSERT a mano pueden meter un contacto sin nombre.

Hay dos tipos principales:

python
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.