Saltar a contenido

Modelos

Django básico

Como hemos visto en las características de Django existe un ORM que vincula tablas de la base de datos con objetos de Python.

Un modelo es simplemente una clase de Python que hereda características definidas (en otras clases) del propio framework de Django.

Se trata de una abstracción del modelo de datos que permite trabajar a más alto nivel. En teoría1 podríamos cambiar el sistema gestor de base de datos que hay debajo y todo seguiría funcionando de la misma manera.

Creando modelos

Un modelo no es más que una clase Python que «suele» vivir en el fichero models.py de una aplicación Django.

Veamos un ejemplo en el que creamos un modelo Post de nuestro «blog» dentro de una aplicación llamada posts:

posts/models.py
from django.db import models#(1)!


class Post(models.Model):#(2)!
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()

    def __str__(self):#(3)!
        return self.title

  1. Necesitamos importar el módulo models que nos dará aquellas funcionalidades necesarias para implementar nuestros modelos.
    • La clase debe heredar de models.Model para que se convierta en un modelo válido.
    • En este caso se creará una tabla en la base de datos con el nombre posts_post (app_class).
  2. Es muy recomendable definir el método __str__()

Campos obligatorios

Todos los campos que definimos en un modelo son campos obligatorios (requeridos) por defecto, salvo que se indique lo contrario de forma explícita.

Existe una correspondencia entre el modelo de datos (clase) definido en Django y la tabla creada en la base de datos. Para el caso anterior se creará la tabla posts_post (app_model).

Campos

Los campos de un modelo no son más que atributos de clase.

Dentro del módulo django.db.models disponemos de una gran cantidad de tipos de campos. A continuación se muestra una tabla completa con los tipos de campos existentes en Django:

Campo Descripción Objeto Python Parámetros
AutoField Entero autoincremental int
BigAutoField Entero largo (64bits)
autoincremental
int
BigIntegerField Entero largo (64bits) int
BinaryField Valor binario bytes max_length(1)
BooleanField Valor «booleano» bool
CharField Campo de texto (corto) str max_length (2)
DateField Fecha datetime.date auto_now_add(3)
auto_now(4)
DateTimeField Fecha y hora datetime.datetime auto_now_add(5)
auto_now(6)
DecimalField Valor flotante
(dinero/divisas)
Decimal max_digits (7)
decimal_places (8)
DurationField Período de tiempo datetime.timedelta
EmailField Correo electrónico str max_length(9)
FileField Fichero Storage upload_to(10)
storage(11)
max_length(12)
FilePathField Nombres de ficheros existentes en una ruta str path (13)
match(14)
recursive(15)
allow_files(16)
allow_folders(17)
max_length(18)
FloatField Valor flotante float
GeneratedField Computado en función de otros (a nivel de base de datos) expression(19)
output_field(20)
db_persist(21)
GenericIPAddressField Dirección IPv4 o IPv6 str protocol(22)
unpack_ipv4(23)
ImageField Fichero de imagen Storage upload_to(24)
height_field(25)
width_field(26)
max_length(27)
IntegerField Valor entero int
JSONField Datos JSON dict o list encoder(28)
decoder(29)
PositiveBigIntegerField Entero positivo largo int
PositiveIntegerField Entero positivo int
PositiveSmallIntegerField Entero positivo corto int
SlugField «Slug»2 str max_length(30)
SmallAutoField Entero corto autoincremental int
SmallIntegerField Entero corto int
TextField Campo de texto (largo) str
TimeField Hora datetime.time auto_now_add(31)
auto_now(32)
URLField URL str max_length(33)
UUIDField UUID UUID
  1. Tamaño máximo permitido (en bytes).
    • Número máximo de caracteres que se pueden almacenar en la base de datos.
    • Es obligatorio para todos los sistemas gestores de bases de datos, salvo para PostgreSQL y SQLite.
    • Puesto a True hace que el campo se actualice a la fecha actual cuando se crea el objeto.
    • No es obligatorio. Sólo usarlo en los casos en los que sea necesario.
    • Puesto a True hace que el campo se actualice a la fecha actual cada vez que se guarda el objeto.
    • No es obligatorio. Sólo usarlo en los casos en los que sea necesario.
    • Puesto a True hace que el campo se actualice a la fecha/hora actual cuando se crea el objeto.
    • No es obligatorio. Sólo usarlo en los casos en los que sea necesario.
    • Puesto a True hace que el campo se actualice a la fecha/hora actual cada vez que se guarda el objeto.
    • No es obligatorio. Sólo usarlo en los casos en los que sea necesario.
    • Número máximo de dígitos permitidos en el número.
    • Incluye tanto los dígitos a la izquierda de la «coma» como los dígitos a la derecha de la «coma».
    • Número de cifras decimales para almacenar el número.
    • Por ejemplo para almacenar números hasta 999.99 usaríamos:
      • max_digits=5
      • decimal_places=2
  2. Número máximo de caracteres que se pueden almacenar en la base de datos.
  3. Ruta en la que almacenar el archivo una vez que se suba.
  4. Objeto de almacenamiento.
  5. Número máximo de caracteres que se pueden almacenar en la base de datos.
  6. Ruta de sistema desde la que se extraen las opciones del campo.
  7. Expresión regular para filtrar nombres de fichero.
    • Si es True se listará recursivamente todo el contenido de la ruta indicada.
    • Por defecto es False.
    • Si es True permite el listado de ficheros.
    • Por defecto es True.
    • Si es True permite el listado de directorios.
    • Por defecto es False.
  8. Número máximo de caracteres que se pueden almacenar en la base de datos.
  9. Objeto de tipo Expression para el cálculo del campo.
  10. Instancia de campo de modelo que define el tipo de datos del campo.
    • Si es True la columna en la base de datos será almacenada como real.
    • En otro caso la columna actuará como una columna virtual.
  11. Limita la entrada al protocolo especificado.
  12. Desempaqueta direcciones IPv4.
  13. Ruta en la que almacenar el archivo una vez que se suba.
  14. Nombre de un campo de modelo que contendrá el alto de la imagen.
  15. Nombre de un campo de modelo que contendrá el ancho de la imagen.
  16. Número máximo de caracteres que se pueden almacenar en la base de datos.
  17. Una subclase de JSONEncoder para serializar los tipos de datos no soportados por el serializador JSON.
  18. Una subclase de JSONDecoder para deserializar la entrada.
  19. Número máximo de caracteres que se pueden almacenar en la base de datos.
    • Puesto a True hace que el campo se actualice a la hora actual cuando se crea el objeto.
    • No es obligatorio. Sólo usarlo en los casos en los que sea necesario.
    • Puesto a True hace que el campo se actualice a la hora actual cada vez que se guarda el objeto.
    • No es obligatorio. Sólo usarlo en los casos en los que sea necesario.
  20. Número máximo de caracteres que se pueden almacenar en la base de datos.

Parámetro requerido.

max_length

Aunque es un campo totalmente libre, como estrategia puede ser interesante asignarle valores potencias de 2. Esto normaliza en cierta manera el tamaño que especificamos y tiene su margen de crecimiento: 32, 64, 128, 256, 512, 1024, ...

Tamaño de enteros

Veamos a continuación una tabla resumen del tamaño de los distintos campos enteros existentes en los modelos de Django:

Campo Límite inferior Límite superior
SmallIntegerField -32768 32767
PositiveSmallIntegerField 0 32767
IntegerField -2147483648 2147483647
PositiveIntegerField 0 2147483647
BigIntegerField -9223372036854775808 9223372036854775807
PositiveBigIntegerField 0 9223372036854775807

Migraciones

Una migración es un fichero de código Python que contiene las instrucciones a ejecutar sobre la correspondiente tabla de la base de datos en función de los cambios realizados en el modelo.

Tras cualquier modificación del fichero models.py es necesario:

Para crear las migraciones ejecutamos el comando:

$ ./manage.py makemigrations #(1)!
Migrations for 'posts':
  posts/migrations/0001_initial.py
    + Create model Post

  1. Admite la posibilidad de indicar una aplicación como argumento para crear únicamente las migraciones de dicha aplicación.

$ uv run manage.py makemigrations #(1)!
Migrations for 'posts':
  posts/migrations/0001_initial.py
    + Create model Post

  1. Admite la posibilidad de indicar una aplicación como argumento para crear únicamente las migraciones de dicha aplicación.
justfile

Consulta la receta makemigrations para incluirla en tu justfile.

Para aplicar las migraciones ejecutamos el comando:

$ ./manage.py migrate #(1)!
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0001_initial... OK

  1. Admite la posibilidad de indicar una aplicación como argumento para aplicar únicamente las migraciones de dicha aplicación.

$ uv run manage.py migrate #(1)!
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0001_initial... OK

  1. Admite la posibilidad de indicar una aplicación como argumento para aplicar únicamente las migraciones de dicha aplicación.
justfile

Consulta la receta migrate para incluirla en tu justfile.

Ficheros de migración

Las migraciones se almacenan en la carpeta migrations dentro de la correspondiente aplicación.

flowchart LR
    models[models.py]
    subgraph Migrations
        makemigrations["<tt>./manage.py makemigrations</tt>"]
        makemigrations --> migrate["<tt>./manage.py migrate</tt>"]
    end
    models --> makemigrations
    makemigrations -.-> migrations[migrations/0001_migration.py]
    migrate --> Database

Veamos la migración inicial sobre el modelo Posts definido previamente en nuestro «blog»:

posts/migrations/0001_initial.py
# Generated by Django 5.2.6 on 2025-10-01 09:11

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=256)),
                ('slug', models.SlugField(max_length=256)),
                ('content', models.TextField()),
            ],
        ),
    ]

Como puede observarse en el fragmento de código anterior no aparece ninguna sentencia SQL. Lo que encontramos es código Python con indicaciones de alto nivel que serán «traducidas» a SQL para su aplicación sobre la base de datos.

Registro de migraciones

Django nos ofrece la posibilidad de comprobar el registro de migraciones:

$ ./manage.py showmigrations #(1)!
admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
 [X] 0003_logentry_add_action_flag_choices
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
 [X] 0010_alter_group_name_max_length
 [X] 0011_update_proxy_permissions
 [X] 0012_alter_user_first_name_max_length
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
posts
 [X] 0001_initial
sessions
 [X] 0001_initial

  1. Admite la posibilidad de indicar una aplicación como argumento para mostrar únicamente las migraciones de dicha aplicación.

$ uv run manage.py showmigrations #(1)!
admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
 [X] 0003_logentry_add_action_flag_choices
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
 [X] 0010_alter_group_name_max_length
 [X] 0011_update_proxy_permissions
 [X] 0012_alter_user_first_name_max_length
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
posts
 [X] 0001_initial
sessions
 [X] 0001_initial

  1. Admite la posibilidad de indicar una aplicación como argumento para mostrar únicamente las migraciones de dicha aplicación.
justfile

Consulta la receta showmigrations para incluirla en tu justfile.

Aquellas migraciones marcadas con significa que ya se han aplicado.

Revertir migraciones

Para plantear un escenario inicial, vamos por ejemplo a hacer un pequeño cambio sobre el modelo Post ampliando el tamaño de los campos title y slug:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=300)
    slug = models.SlugField(max_length=300)
    content = models.TextField()

    def __str__(self):
        return self.title

Ahora creamos la migración:

$ ./manage.py makemigrations posts
Migrations for 'posts':
  posts/migrations/0002_alter_post_slug_alter_post_title.py
    ~ Alter field slug on post
    ~ Alter field title on post
$ uv run manage.py makemigrations posts
Migrations for 'posts':
  posts/migrations/0002_alter_post_slug_alter_post_title.py
    ~ Alter field slug on post
    ~ Alter field title on post

Y a continuación aplicamos la migración:

$ ./manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0002_alter_post_slug_alter_post_title... OK    
$ uv run manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0002_alter_post_slug_alter_post_title... OK    

Si visualizamos el registro de migraciones veremos que esta última migración ya se ha aplicado:

$ ./manage.py showmigrations posts
posts
 [X] 0001_initial
 [X] 0002_alter_post_slug_alter_post_title
$ uv run manage.py showmigrations posts
posts
 [X] 0001_initial
 [X] 0002_alter_post_slug_alter_post_title

En este punto podríamos querer revertir («rollback») la última migración. Para ello simplemente migramos al punto de la historia que necesitemos:

$ ./manage.py migrate posts 0001#(1)!
Operations to perform:
  Target specific migration: 0001_initial, from posts
Running migrations:
  Rendering model states... DONE
  Unapplying posts.0002_alter_post_slug_alter_post_title... OK

$ uv run manage.py showmigrations posts #(2)!
posts
 [X] 0001_initial
 [ ] 0002_alter_post_slug_alter_post_title

  1. Indicamos el número de la migración a la que «regresar».
  2. Como era de esperar, la migración ya no aparece aplicada.

$ uv run manage.py migrate posts 0001#(1)!
Operations to perform:
  Target specific migration: 0001_initial, from posts
Running migrations:
  Rendering model states... DONE
  Unapplying posts.0002_alter_post_slug_alter_post_title... OK

$ uv run manage.py showmigrations posts #(2)!
posts
 [X] 0001_initial
 [ ] 0002_alter_post_slug_alter_post_title

  1. Indicamos el número de la migración a la que «regresar».
  2. Como era de esperar, la migración ya no aparece aplicada.

La migración se ha revertido correctamente. Si quisiéramos eliminar del registro la migración 0002 bastaría con eliminar el fichero:

$ rm posts/migrations/0002_alter_post_slug_alter_post_title.py

Ahora si volvemos a comprobar el registro de migraciones, vemos que todo está como esperaríamos:

$ ./manage.py showmigrations posts
posts
 [X] 0001_initial
$ uv run manage.py showmigrations posts
posts
 [X] 0001_initial

Por último, para volver a dejar todo «como estaba», modificamos de nuevo el tamaño de los campos como se había especificado inicialmente:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()

    def __str__(self):
        return self.title

Migraciones manuales

Django avanzado

Hay ocasiones en las que necesitamos crear migraciones manuales (migraciones de datos) para resolver determinados escenarios durante la vida de un proyecto Django.

Vamos a suponer por ejemplo un escenario en el que partimos de un modelo de «post» como el siguiente:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()

    def __str__(self):
        return self.title

Para disponer de datos iniciales en la base de datos, descargamos este fichero posts.json y cargamos las «fixtures»:

$ ./manage.py loaddata posts #(1)!
Installed 5 object(s) from 1 fixture(s)

  1. Aunque no estemos indicando la ruta del fichero, por defecto se van a buscar a la carpeta fixtures/ de cada aplicación del proyecto.

$ uv run manage.py loaddata posts #(1)!
Installed 5 object(s) from 1 fixture(s)

  1. Aunque no estemos indicando la ruta del fichero, por defecto se van a buscar a la carpeta fixtures/ de cada aplicación del proyecto.

Desde «arriba» nos dicen que ahora los «posts» pasarán a ser identificados unívocamente por un campo pid alfanumérico. Por tanto nos vemos obligados a modificar el modelo y añadir dicho atributo:

posts/models.py
from django.db import models


class Post(models.Model):
    pid = models.CharField(unique=True)
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()

    def __str__(self):
        return self.title

Veamos lo que ocurre si intentamos crear la migración:

$ ./manage.py makemigrations posts
It is impossible to add a non-nullable field 'pid' to post without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit and manually define a default value in models.py.
Select an option: 2
$ uv run manage.py makemigrations posts
It is impossible to add a non-nullable field 'pid' to post without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit and manually define a default value in models.py.
Select an option: 2

Lo que está ocurriendo aquí es que ya existen «posts» en la correspondiente tabla de la base de datos y Django no puede añadir la columna pid sin asignar un valor para las filas existentes. Ni siquiera se le podría dar como valor inicial la cadena vacía porque los valores deben ser únicos.

Por tanto aquí vamos a seguir otra estrategia para resolver este problema:

  1. Añadir el campo pid como opcional.
  2. Implementar una migración manual para rellenar los datos de pid.
  3. Modificar el campo pid para que sea requerido y único.

Añadir campo opcional

Modificamos la definición del atributo pid para hacerlo opcional:

posts/models.py
from django.db import models


class Post(models.Model):
    pid = models.CharField(max_length=256, blank=True)
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()

    def __str__(self):
        return self.title

Creamos y aplicamos la migración correspondiente:

$ ./manage.py makemigrations posts
Migrations for 'posts':
  posts/migrations/0002_post_pid.py
    + Add field pid to post

$ ./manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0002_post_pid... OK
$ uv run manage.py makemigrations posts
Migrations for 'posts':
  posts/migrations/0002_post_pid.py
    + Add field pid to post

$ uv run manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0002_post_pid... OK

Rellenar datos

Ahora estamos en disposición de crear una migración manual para rellenar los datos del campo pid. Para ello hacemos lo siguiente:

$ ./manage.py makemigrations --empty posts
Migrations for 'posts':
  posts/migrations/0003_auto_20260411_0901.py
$ uv run manage.py makemigrations --empty posts
Migrations for 'posts':
  posts/migrations/0003_auto_20260411_0901.py

Editamos la migración creada para añadir el código necesario:

posts/migrations/0003_auto_20260411_0901.py
# Generated by Django 5.2.6 on 2026-04-11 09:01

import hashlib

from django.db import migrations


def fill_pid(apps, schema_editor):#(1)!
    Post = apps.get_model('posts', 'Post')#(2)!
    for post in Post.objects.all():#(3)!
        post.pid = hashlib.md5(post.title.encode()).hexdigest()#(4)!
        post.save()#(5)!


def undo_fill_pid(apps, schema_editor):#(6)!
    Post = apps.get_model('posts', 'Post')#(7)!
    for post in Post.objects.all():#(8)!
        post.pid = ''#(9)!
        post.save()#(10)!


class Migration(migrations.Migration):#(11)!
    dependencies = [#(12)!
        ('posts', '0002_post_pid'),
    ]

    operations = [#(13)!
        migrations.RunPython(fill_pid, undo_fill_pid),#(14)!
    ]

  1. Cada función a aplicar en la migración manual recibe dos parámetros:
    • apps que contiene un registro de todas las versiones históricas de los modelos.
    • schema_editor que permite hacer cambios manuales en la base de datos.
  2. Obtenemos el modelo Post de la aplicación posts (en el momento histórico actual).
  3. Recorremos todos los «posts» existentes actualmente.
  4. Aplicamos un hash md5 al título para obtener el nuevo identificador de post.
  5. Guardamos los cambios en la base de datos.
  6. Cada función a aplicar en la migración manual recibe dos parámetros:
    • apps que contiene un registro de todas las versiones históricas de los modelos.
    • schema_editor que permite hacer cambios manuales en la base de datos.
  7. Obtenemos el modelo Post de la aplicación posts (en el momento histórico actual).
  8. Recorremos todos los «posts» existentes actualmente.
  9. Reseteamos el identificador de post como cadena vacía.
  10. Guardamos los cambios en la base de datos.
  11. Esta clase es la migración en sí.
  12. Se establece la dependencia de esta migración justo con la anterior.
  13. Esta variable lleva un registro de las operaciones a realizar en la migración.
  14. Ejecutamos código Python, concretamente dos funciones:
    • La función fill_pid() es el camino «hacia adelante» en la migración.
    • La función undo_fill_pid() es el camino «hacia atrás» en la migración.

Una vez creada esta migración, procedemos a aplicarla:

$ ./manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0003_auto_20260411_0901... OK
$ uv run manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0003_auto_20260411_0901... OK

Si echamos un vistazo al contenido de la base de datos podremos observar que el campo pid de todos los «posts» se ha rellenado correctamente con el valor esperado:

>>> for post in Post.objects.all():
...     print(f'{post.title:20s} | {post.pid}')
...
Small Changes        | 5fba4065cce227573533119c08fd0cfb
Learning Takes Time  | f639832738dd1b495c00ac0323de6577
Thinking in Code     | 853b27a8c1fc995e23b6ffdf0ec16fe6
Useful Mistakes      | 9e92b3e7603272a0e16fe152712a92cd
Curiosity            | 45ac168eaee7da65d269036c7b5b39e1

Modificar campo único

Dado que ahora el campo pid tiene contenido para todos los objetos «post» de la base de datos, podemos modificarlo como valor único sin riesgo a tener ningún tipo de problema:

posts/models.py
from django.db import models


class Post(models.Model):
    pid = models.CharField(max_length=256, unique=True)
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()

    def __str__(self):
        return self.title

Creamos y aplicamos la migración correspondiente:

$ ./manage.py makemigrations posts
Migrations for 'posts':
  posts/migrations/0004_alter_post_pid.py
    ~ Alter field pid on post

$ ./manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0004_alter_post_pid... OK
$ uv run manage.py makemigrations posts
Migrations for 'posts':
  posts/migrations/0004_alter_post_pid.py
    ~ Alter field pid on post

$ uv run manage.py migrate posts
Operations to perform:
  Apply all migrations: posts
Running migrations:
  Applying posts.0004_alter_post_pid... OK

Ahora efectivamente ya hemos completado este proceso de migración manual que conlleva varios pasos pero que asegura una consistencia en la base de datos con respecto al modelo que estamos implementado.

Base de datos

La configuración de la base de datos del proyecto se encuentra en la variable DATABASES del fichero settings.py y (por defecto) tiene este aspecto:

main/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

Podemos ver que se trata de un diccionario con una clave default lo que nos hace pensar que podemos definir configuraciones alternativas para la base de datos.

En esta configuración (por defecto) tenemos un motor de base de datos SQLite que almacenará la información en un fichero db.sqlite3 dentro de la carpeta base3 (raíz) del proyecto.

Control de versiones

El fichero de base de datos debe estar fuera del control de versiones.

Clave primaria

Siempre que creemos un nuevo modelo y no definamos una clave primaria, Django generará automáticamente un campo id de tipo BigAutoField (entero largo autoincremental) que se materializa en la base de datos mediante:

"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT

En el caso de que efectivamente queramos crear una clave primaria propia, basta con indicarlo a la hora de escribir nuestro modelo. Supongamos un ejemplo donde el «slug» de un «post» (blog) fuera la clave primaria:

posts/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, primary_key=True)
    content = models.TextField()

Valores únicos

Hay escenarios en los que necesitamos que los valores de un determinado campo no se repitan, o dicho de otra forma, que sea únicos. En este sentido Django nos permite especificarlo mediante un parámetro en el campo correspondiente.

Siguiendo con el ejemplo anterior de un «post», quizás nos puede interesar que slug sea único. Para ello simplemente hacemos:

posts/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()
Claves candidatas

En terminología de bases de datos relacionales, una clave candidata es aquella que identifica unívocamente a cada fila de una tabla (sin tener en cuenta la clave primaria).

Valores únicos juntos

Django intermedio

Aunque quizás no sea del todo realista, supongamos un ejemplo en el que no pueden haber dos «posts» con el mismo título y «slug».

Para modelar esto haremos uso del atributo unique_together que ofrece Django:

posts/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    class Meta:#(1)!
        unique_together = ['title', 'slug']

    • La clase interior Meta permite indicarle a Django ciertas opciones para un modelo.
    • Por una cuestión de «estilo» se suele escribir justo debajo de los atributos del modelo.
Marca y modelo

Un ejemplo quizás más evidente de valores únicos juntos sea el de marca y modelo en una aplicación de gestión de productos. Obviamente la marca de un producto se puede repetir, y el modelo de un producto se puede repetir, pero lo que no puede pasar es que se repitan ambos. En otras palabras, marca-modelo deben ser únicas:

# Marca Modelo Estado
1 Logitech K120
2 Samsung K120
3 Lenovo Z562
4 Lenovo W208
5 Logitech K120 (colisión #1)

Campos opcionales

Hay ocasiones en las que necesitamos que ciertos campos del modelo puedan quedar vacíos, es lo que conocemos como campos opcionales. La cuestión aquí es que la definición de «vacío» en la base de datos no siempre es tan evidente.

Lo más habitual es entender que NULL sea el valor «vacío» en una base de datos SQL, pero Django distingue el caso de los campos basados en «string», ya que estos podrían admitir dos posibles valores para «vacío»: Uno es NULL y otro es la cadena vacía.

Por otro lado está el tema de las validaciones que hace Django a la hora de introducir datos en nuestros modelos desde la interfaz administrativa. En este sentido se puede establecer que un campo no es requerido, por lo que podría quedar «vacío» a la hora de rellenarlo.

Con todo esto, Django establece dos parámetros que tienen que ver con este escenario de campos «vacíos»:

  • null=True: Indica que un valor «vacío» se almacenará en la base de datos como NULL.
  • blank=True: Indica que la validación Django permitirá la entrada de un valor «vacío».

Combinando estos dos parámetros podemos definir campos opcionales para nuestro modelo. Veamos una tabla resumen:

Campo Opcional con...
CharField
EmailField
SlugField
TextField
blank=True
Resto de campos blank=True, null=True

null=False

Recuerda que null=False es algo que no tiene sentido ya que los campos son obligatorios por defecto.

ORM

Como ya se introdujo en las características de Django el ORM (Object-Relational Mapper) es la herramienta que permite interactuar con bases de datos usando objetos y clases de Python en lugar de escribir directamente consultas SQL.

Shell

Django nos permite abrir una «shell» (intérprete interactivo de Python) con las configuraciones del proyecto ya cargadas:

$ ./manage.py shell
7 objects imported automatically (use -v 2 for details).

Python 3.13.2 (main, Feb 12 2025, 14:59:08) [Clang 19.1.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
$ uv run manage.py shell
7 objects imported automatically (use -v 2 for details).

Python 3.13.2 (main, Feb 12 2025, 14:59:08) [Clang 19.1.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
justfile

Consulta la receta shell para incluirla en tu justfile.

Como puede verse en el fragmento de código anterior, Django importa automáticamente(1) distintos objetos de nuestro proyecto. En el caso concreto de nuestro «blog» se han importado los siguientes:

  1. Para mostrar los «import» automáticos se puede añadir la opción: manage.py shell -v2
from posts.models import Post
from django.contrib.sessions.models import Session
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User, Group, Permission
from django.contrib.admin.models import LogEntry
ipython

Si queremos disponer de una «shell» algo más potente podemos instalar ipython como dependencia de desarrollo:

$ uv add --dev ipython

Creando objetos

Para crear un nuevo objeto (y almacenarlo en la base de datos) mediante el ORM de Django, disponemos de dos aproximaciones:

>>> from posts.models import Post#(1)!

>>> p = Post(#(2)!
...     title='Check out the new Django version',
...     slug='check-out-the-new-django-version',
...     content='Awesome features of the last release of Django',
... )
>>> p.save()#(3)!

  1. No sería necesario si usamos la shell de Django (importación automática de objetos).
  2. Llamada al constructor del modelo. En este momento el objeto sólo se encuentra en memoria.
  3. Momento en el que se consolida el objeto como una fila en la tabla correspondiente.

>>> from posts.models import Post#(1)!

>>> p = Post.objects.create(#(2)!
...     title='Check out the new Django version',
...     slug='check-out-the-new-django-version',
...     content='Awesome features of the last release of Django',
... )

  1. No sería necesario si usamos la shell de Django (importación automática de objetos).
  2. Creación del objeto y consolidación en la base de datos: Todo en la misma llamada.
Creando más objetos

Para disponer de un conjunto más amplio de «posts» en la base de datos, puedes lanzar este fragmento de código:

>>> Post.objects.create(
...     title='Understanding URL routing in Django',
...     slug='understanding-url-routing-in-django',
...     content='Learn how Django URL patterns work and how to organize routes for scalable applications.',
... )

>>> Post.objects.create(
...     title='Working with function-based views in Django',
...     slug='working-with-function-based-views-in-django',
...     content='Learn how to handle requests and responses using function-based views in Django applications.',
... )

>>> Post.objects.create(
...     title='Mastering Django templates',
...     slug='mastering-django-templates',
...     content='Understand template inheritance, context variables, and best practices for clean frontend rendering.',
... )

>>> Post.objects.create(
...     title='Working with forms in Django',
...     slug='working-with-forms-in-django',
...     content='Learn how to create, validate, and process forms using Django forms and ModelForms.',
... )

Shell de base de datos

Django nos permite abrir una «shell» de base de datos (interfaz de comandos del sistema gestor) con las configuraciones del proyecto ya cargadas.

Suponiendo que estamos usando la configuración por defecto de SQLite, primero debemos tener instalado el cliente de SQLite en nuestro sistema:

> winget install -e --id SQLite.SQLite#(1)!

  1. Instalación mediante winstall.

$ brew install sqlite #(1)!

  1. Instalación mediante Homebrew.
$ sudo apt-get install sqlite3

Podemos abrir una «shell» de base de datos con el comando:

$ ./manage.py dbshell
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite>
$ uv run manage.py dbshell
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite>
justfile

Consulta la receta dbshell para incluirla en tu justfile.

Podemos comprobar con una sencilla consulta SQL que el objeto se ha creado correctamente en la base de datos:

sqlite> .mode line
sqlite> SELECT * FROM posts_post;
     id = 1
  title = Check out the new Django version
   slug = check-out-the-new-django-version
content = Awesome features of the last release of Django

Modo caja

Prueba el modificador .mode box en sqlite3 para ver los resultados de las consultas en modo caja.

justfile (para comando)

También dispones de la receta dbcmd que permite ejecutar directamente un comando sobre la base de datos sin necesidad de entrar en el cliente. Por ejemplo:

$ just dbcmd 'SELECT * FROM posts_post'

Guardando objetos

El método save() nos permite guardar los cambios realizados en una instancia de un modelo. Por tanto podemos modificar los valores de sus campos y reflejarlos en la base de datos.

Supongamos por ejemplo que p es un objeto de tipo Post y queremos modificar su título:

>>> p.title = 'This is a better title'
>>> p.save()

Recuperando objetos

Todos los objetos

La primera aproximación a la consulta de datos será obtener todos los objetos de un determinado modelo.

Partiendo del ejemplo con el modelo Post podríamos recuperar todos sus objetos con:

>>> from posts.models import Post

>>> Post.objects.all()#(1)!

    • El atributo objects es el «manager» por defecto.
    • El método all() devuelve un QuerySet (una especie de lista «perezosa» de objetos).

SQL

Django convierte cada llamada al ORM en su correspondiente instrucción SQL. Esto se puede ver fácilmente accediendo al atributo query:

>>> qs = Post.objects.all()

>>> print(qs.query)
SELECT "posts_post"."id",
       "posts_post"."title",
       "posts_post"."slug",
       "posts_post"."content"
FROM "posts_post"

Ciertos objetos

Para recuperar determinados objetos que cumplan ciertas condiciones vamos a utilizar el método filter(). Se trata de un método que recipe como parámetro las condiciones a satisfacer por los objetos del modelo.

Supongamos un ejemplo en el que queremos recuperar todos aquellos «posts» cuyo título empiece por la letra A:

>>> from posts.models import Post

>>> Post.objects.filter(title__startswith='A')#(1)!

    • El atributo objects es el «manager» por defecto.
    • El método filter() devuelve un QuerySet (una especie de lista «perezosa» de objetos).
    • startswith es un «field lookup». Existen muchos otros.

Un único objeto

La forma más «obvia» de recuperar un objeto de modelo es mediante su clave primaria.

En el siguiente ejemplo vamos a recuperar un «post» cuya clave primaria es 7:

>>> from posts.models import Post

>>> p = Post.objects.get(pk=7)#(1)!

  1. Podríamos haber utilizado Post.objects.get(id=7) pero el hecho de usar pk es una buena práctica y nos «abstrae» del nombre concreto que tenga el campo de clave primaria en la tabla de la base de datos.

DoesNotExist

Cuando usamos el método get() y Django no encuentra ningún objeto que satisfaga la condición, lanzará una excepción de tipo DoesNotExist. Todos los modelos heredan esta excepción como atributo de clase, por lo tanto es posible capturarla de la siguiente manera:

try:
    p = Post.objects.get(pk=-1)
except Post.DoesNotExist as err:
    print('Sorry the post you need does not exist')

Es posible que en cierta documentación de Django encuentres la siguiente «fórmula» para obtener un único objeto:

post = Post.objects.filter(pk=7).first()#(1)!

  1. Utilizamos la función first().

Hay que diferenciar dos casos:

  1. Si el «post» que buscamos existe, lo obtendremos en la variable post.
  2. Si el «post» que buscamos no existe, obtendremos None (a diferencia de get() donde se lanza una excepción).

Excluyendo objetos

Hay escenarios en los que se necesita excluir ciertos objetos del total o de determinado consulta previa. Para estos casos Django proporciona el método exclude().

Supongamos un ejemplo en el que queremos obtener todos los «posts» cuyo título empiece por la letra A pero que no terminen por la letra z:

>>> from posts.models import Post

>>> Post.objects.filter(title__startswith='A').exclude(title__endswith='z')#(1)!

    • El atributo objects es el «manager» por defecto.
    • El método filter() devuelve un QuerySet (una especie de lista «perezosa» de objetos).
    • startswith es un «field lookup». Existen muchos otros.
    • El método exclude() devuelve un QuerySet (una especie de lista «perezosa» de objetos).
    • endswith es un «field lookup». Existen muchos otros.

Encadenados

Es muy importante hacer notar que las consultas en Django están diseñadas para que puedan encadenarse unas con otras:

>>> Post.objects.filter(<selector>).exclude(<selector>).filter(<selector>) # ...

Selectores de consulta

Para construir consultas podemos hacer uso de los «field lookups» (selectores) de Django. Lo podemos ver como el contenido que aparecerá posteriormente en la cláusula WHERE de la sentencia SQL correspondiente.

La sintaxis para usar estos selectores es la siguiente:

<Model>.objects.filter(<field>__<lookup>)#(1)!

  1. También funciona para get() y para exclude().

A continuación se muestran todos los selectores de consulta disponibles en Django:

Selector Descripción
exact Busca el término exacto.
iexact Busca el término exacto (ignorando mayúsculas/minúsculas).
contains Busca si contiene el término(1).
icontains Busca si contiene el término (ignorando mayúsculas/minúsculas).
startswith Busca si empieza por un término(2).
istartswith Busca si empieza por un término (ignorando mayúsculas/minúsculas).
endswith Busca si termina por un término(3).
iendswith Busca si termina por un término (ignorando mayúsculas/minúsculas).
regex Busca si casa con una expresión regular.
iregex Busca si casa con una expresión regular (ignorando mayúsculas/minúsculas).
Selector Descripción
date Busca si la fecha coincide.
year Busca si el año coincide.
iso_year Busca si el año coincide en formato ISO 8601.
month Busca si el mes coincide.
day Busca si el día coincide.
week Busca si la semana coincide.
week_day Busca si el día de la semana coincide.
iso_week_day Busca si el día de la semana coincide en formato ISO 8601.
quarter Busca si el trimestre del año coincide.
time Busca si el «tiempo» coincide.
hour Busca si la hora coincide.
minute Busca si el minuto coincide.
second Busca si el segundo coincide.
Selector Descripción
in Busca si aparece en un iterable de valores.
gt Busca si es mayor que un valor.
gte Busca si es mayor o igual que un valor.
lt Busca si es menor que un valor.
lte Busca si es menor o igual que un valor.
range Busca si está en un rango \((min, max)\)
isnull Busca si el valor es nulo.

Borrando objetos

Una vez que tenemos localizado el objeto que queremos borrar, es muy sencillo ya que simplemente tendremos que invocar al método delete():

>>> post.delete()#(1)!
(1, {'posts.Post': 1})

  1. Devuelve una tupla con:
    1⃣ Número total de objetos borrados.
    2⃣ Diccionario con el tipo de objeto y el número de objetos borrados de cada tipo.

Este método también funciona para borrados en lote. Por ejemplo:

>>> Post.objects.all().delete()
(10, {'posts.Post': 10})

Contando objetos

Hay muchas ocasiones en las que resulta necesario obtener el número de objetos que tiene una determinada consulta. Para ello Django nos ofrece el método count().

Si queremos por ejemplo sacar el número total de «posts» que hay en nuestro «blog» podríamos escribir lo siguiente:

>>> from posts.models import Post

>>> Post.objects.count()#(1)!
10

  1. También se puede aplicar sobre un filtro Post.objects.filter(title__contains='Test').count()
Contando con len

Se podría tener la tentación de contar los objetos de la siguiente manera:

>>> len(Post.objects.all())
10

Aunque el resultado es el mismo que utilizando .count(), esta consulta es mucho más costosa ya que se recuperan todos los objetos de la tabla (SELECT * FROM posts_post) y luego se cuentan.

Comprobando existencia

No siempre buscamos contar el número de resultados sino que únicamente necesitamos saber si existen o no objetos para una determinada consulta. Es por ello que Django ofrece el método exists() que devuelve True o False.

Por ejemplo si queremos saber si existen «posts» que comienzan por la letra «A»:

>>> from posts.models import Post

>>> if Post.objects.filter(title__startswith='A').exists():
...     print('Hay posts que empiezan por la letra A')

Ordenando resultados

Es muy habitual querer ordenar el resultado de una consulta por uno o varios campos. Para ello Django nos ofrece la función order_by().

Supongamos por ejemplo que queremos ordenar el listado de «posts» por su título:

>>> from posts.models import Post

>>> Post.objects.order_by('title')#(1)!

    • Podemos añadir más campos de ordenación simplemente añadiendo más argumentos.
    • Por ejemplo order_by('title', 'content') significa que si dos «posts» tienen el mismo título se ordenarán por su contenido.
Ordenación descendente

Por defecto el método order_by() ordena de forma ascendente por los campos indicados. Si queremos aplicar una ordenación descendente basta con añadir un guión medio - delante del campo.

Por ejemplo Post.objects.order_by('-title') ordenaría los «posts» por su título de forma descendente (es decir de la Z a la A).

Primeros y últimos

Django ofrece varias funciones para acceder a los primeros y últimos objetos de una consulta que cumplan ciertas condiciones:

first devuelve el primer objeto del QuerySet correspondiente. Por ejemplo para obtener el primer «post» por orden de título haríamos:

>>> Post.objects.order_by('title').first()

last devuelve el último objeto del QuerySet correspondiente. Por ejemplo para obtener el último «post» por orden de título haríamos:

>>> Post.objects.order_by('title').last()

earliest devuelve el objeto del QuerySet con el menor valor del campo indicado. Por ejemplo para obtener el «post» con menor clave primaria haríamos:

>>> Post.objects.earliest('pk')

latest devuelve el objeto del QuerySet con el mayor valor del campo indicado. Por ejemplo para obtener el «post» con mayor clave primaria haríamos:

>>> Post.objects.latest('pk')

Actualizando objetos

Django proporciona el método update() para actualizar múltiples objetos a la vez.

Supongamos por ejemplo que queremos borrar el contenido de todos los «posts» de nuestro «blog». Para ello podemos utilizar esta aproximación:

>>> Post.objects.update(content='')#(1)!
10

    • El método devuelve el número de objetos afectados.
    • Es posible indicar varios atributos a actualizar simultáneamente.
    • Obviamente también se puede utilizar sobre una operación de filtrado.

Refrescando objetos

Hay ocasiones en las que los valores de un objeto (de modelo) no están sincronizados con sus correspondientes en la base de datos. Para actualizar dichos atributos, Django ofrece el método refresh_from_db().

Por ejemplo un «post» que se actualiza en la base de datos pero no en memoria:

>>> from posts.models import Post

>>> post = Post.objects.get(slug='first-post')#(1)!

>>> post.content#(2)!
'First post'

>>> Post.objects.filter(slug='first-post').update(content='Updated content')#(3)!
1

>>> post.content#(4)!
'First post'

>>> post.refresh_from_db()#(5)!

>>> post.content#(6)!
'Updated content'

  1. Recuperamos un determinado «post» de nuestro «blog».
  2. Comprobamos su contenido.
  3. Actualizamos su contenido (en la base de datos).
  4. Comprobamos su contenido (en memoria) que no está sincronizado con la base de datos.
  5. Refrescamos el objeto desde la base de datos.
  6. Comprobamos que qhora su contenido (en memoria) sí coincide con el que tiene la base de datos.

En realidad la operación refresh_from_db() es la opuesta a save():

flowchart TD
    model@{ shape: div-rect, label: "Model instance" } --->|"<tt>save</tt>"| database@{ shape: cyl, label: "Database" }
    database --->|"<tt>refresh_from_db</tt>"| model

Tipos enumerados

Django avanzado

Django nos permite definir tipos enumerados que establecen un conjunto (normalmente pequeño) de posibles valores.

Un tipo enumerado se define por dos componentes: una etiqueta (nombre largo) y un valor (código corto). Lo que hace Django es almacenar en la base de datos únicamente el valor, mientras que la «etiqueta» se establece en la definición del campo.

Django ofrece dos variantes:

1⃣ Tipos enumerados basados en cadenas de textoEnumerados textuales.
2⃣ Tipos enumerados basados en números enterosEnumerados enteros.

Enumerados textuales

En este escenario se utiliza un campo CharField y se definen los posibles valores mediante el parámetro choices a través de una subclase de models.TextChoices.

Supongamos un ejemplo en el que queremos clasificar los «posts» del «blog» por categorías:

posts/models.py
from django.db import models


class Post(models.Model):
    class Category(models.TextChoices):#(1)!
        SOCIETY = 'SOC', 'Society'#(2)!
        EDUCATION = 'EDU', 'Education'
        HEALTH = 'HLT', 'Health'
        CULTURE = 'CUL', 'Culture'
        TECH = 'TEC', 'Technology'

    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()
    category = models.CharField(#(3)!
        max_length=3,#(4)!
        choices=Category,#(5)!
        default=Category.SOCIETY,#(6)!
    )

    • Esta clase interior nos permite definir las distintas opciones.
    • Suele ser buena práctica darle el mismo nombre que al atributo que va a definir.
    • Cada atributo de clase indica una opción en formato tupla: <COD_CORTO>, <NOMBRE_LARGO>
    • El código corto debería ser un string de pocos caracteres (longitud 1, 2, 3, ...)
    • Si no se especifica el nombre largo, Django lo infiere del propio código corto:

      SCIENCE_FICTION = 'SCI'#(1)!
      

      1. El nombre largo será 'Science Fiction' inferido desde SCIENCE_FICTION.
  1. El campo se define como un CharField().

  2. El tamaño máximo debe coincidir con la longitud del código corto.
  3. Se definen las opciones con referencia a la clase interior.
  4. No es obligatorio aunque sí recomendable definir un valor por defecto.

La clase interior Category es de tipo TextChoices y permite ciertas operaciones:

>>> Post.Category.choices
[('SOC', 'Society'), ('EDU', 'Education'), ('HLT', 'Health'), ('CUL', 'Culture'), ('TEC', 'Technology')]

>>> Post.Category.labels
['Society', 'Education', 'Health', 'Culture', 'Technology']

>>> Post.Category.values
['SOC', 'EDU', 'HLT', 'CUL', 'TEC']

>>> Post.Category.names
['SOCIETY', 'EDUCATION', 'HEALTH', 'CULTURE', 'TECH']

Veamos la forma de acceder a la categoría de un determinado «post»:

>>> from posts.models import Post
>>> post = Post.objects.first()

>>> post.category
'SOC'

>>> from posts.models import Post
>>> post = Post.objects.first()

>>> post.get_category_display()#(1)!
'Society'

    • Si el atributo enumerado es foo siempre exisitirá un método obj.get_foo_display().

Para comprobar el valor de un tipo enumerado debemos hacer uso de la clase interior. Veamos un ejemplo en el que queremos verificar si un determinado «post» es educativo:

if post.category == 'EDU':#(1)!
    # ...

  1. Si en un futuro modificamos el valor (código corto) de la categoría, nos veremos obligados a reemplazar el literal 'EDU' en todo nuestro código.
from posts.models import Post

if post.category == Post.Category.EDUCATION:
    # ...

Para comprobar si el valor está dentro de un enumerado también debemos hacer uso de la clase interior. Veamos un ejemplo en el que queremos verificar si un determinado valor es una categoría de «post»:

if 'EDU' in ['SOC', 'EDU', 'HLT', 'CUL', 'TEC']:#(1)!
    # ...

  1. Si en un futuro modificamos el valor (código corto) de la categoría, nos veremos obligados a reemplazar el literal 'EDU' en todo nuestro código.
from posts.models import Post

if 'EDU' in Post.Category:
    # ...

Enumerados enteros

En este escenario se utiliza un campo IntegerField y se definen los posibles valores mediante el parámetro choices a través de una subclase de models.IntegerChoices.

Supongamos un ejemplo en el que queremos valorar la calidad de los «posts» del «blog» en base a una escala predeterminada:

posts/models.py
from django.db import models


class Post(models.Model):
    class Rating(models.IntegerChoices):#(1)!
        VERY_BAD = 1#(2)!
        BAD = 2
        AVERAGE = 3
        GOOD = 4
        EXCELLENT = 5

    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()
    rating = models.IntegerField(#(3)!
        choices=Rating,#(4)!
        default=Rating.AVERAGE,#(5)!
    )

    • Esta clase interior nos permite definir las distintas opciones.
    • Suele ser buena práctica darle el mismo nombre que al atributo que va a definir.
    • Cada atributo de clase indica una opción en formato tupla: <COD_CORTO>, <NOMBRE_LARGO>
    • El código corto debería ser un entero.
    • Si no se especifica el nombre largo, Django lo infiere del propio código corto.
    • En este caso, la etiqueta sería 'Very Bad'
    • No es obligatorio que los valores sean correlativos.
    • El campo se define como un IntegerField().
    • Aunque dependiendo del contexto se podría usar un PositiveSmallIntegerField().
  1. Se definen las opciones con referencia a la clase interior.
  2. No es obligatorio aunque sí recomendable definir un valor por defecto.

Graduación

Cuando el campo toma una serie de valores concretos que «semánticamente» tienen un orden (de menor a mayor) es posible que un tipo enumerado entero sea una buena solución:

>>> from posts.models import Post

>>> post1 = Post.objects.first()
>>> post2 = Post.objects.last()

>>> post1.rating
3
>>> post2.rating
1

>>> post1.get_rating_display()
'Average'
>>> post2.get_rating_display()
'Very Bad'

>>> post1.rating > post2.rating
True

Claves ajenas

Django intermedio

Una de las mayores fortalezas de los Sistemas Gestores de Bases de Datos Relacionales RDBMS es la de poder «relacionar» entidades (modelos) mediante el uso de claves ajenas.

Django nos ofrece muchas funcionalidades en este sentido, que podemos agrupar en tres escenarios:

1⃣ Relaciones uno a muchos → \(1:N\)
2⃣ Relaciones uno a uno → \(1:1\)
3⃣ Relaciones muchos a muchos → \(N:N\)

Relaciones uno a muchos

Veamos un ejemplo en el que permitimos que un «post» de un «blog» admita comentarios:

erDiagram
    POST ||--o{ COMMENT : has

Un «post» tiene cero o muchos comentarios, pero un comentario está relacionado con un único «post».

Partimos de un modelo de «post» habitual:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

Para relacionar ambos modelos usamos un campo de tipo ForeignKey:

comments/models.py
from django.db import models


class Comment(models.Model):
    alias = models.CharField(max_length=128)
    content = models.TextField()
    post = models.ForeignKey(
        'posts.Post',              # Modelo relacionado
        related_name='comments',   # Nombre relacionado
        on_delete=models.CASCADE,  # Acción de borrado
    )

Analicemos cada parámetro de ForeignKey por separado:

1⃣ Modelo relacionado
2⃣ Nombre relacionado
3⃣ Acción de borrado

El primer parámetro que recibe ForeignKey es el modelo que vamos a relacionar.

Hay dos formas de indicarlo:

  1. Si se indica en formato «string» hay que especificarlo como: '<app>.<Model>'. (1)
  2. También podemos importar el modelo y hacer referencia directa.
  1. Esto permite evitar los llamados «circular imports».

El parámetro related_name establece el nombre que podremos usar en el «otro lado de la relación» para recuperar todos los objetos vinculados.

Es una buena práctica que este parámetro se llame como la clase en la que está incluido pero en minúsculas y en plural:

Dark image Light image

A través del «related name» es posible obtener todos los objetos relacionados. Imaginemos que en el ejemplo del «blog» necesitamos obtener todos los comentarios de un determinado «post»:

>>> from posts.models import Post

>>> post = Post.objects.get(slug='django-is-awesome')
>>> post.comments.all()#(1)!
<QuerySet [<Comment: This is cool>, <Comment: I don't understand it>, <Comment: Please explain it again>]>

  1. La forma anti natural podría sería: Comment.objects.filter(post=post)

related_name no es un parámetro requerido, pero es altamente recomendable incluirlo.

Hay ocasiones en las que se puede producir una colisión si tenemos dos claves ajenas apuntando al mismo modelo y con el mismo related_name.

Pensemos en un ejemplo donde reflejamos el escritor y el editor de un «post»:

posts/models.py
class Post(models.Model):
    writer = models.ForeignKey('members.Member', related_name='posts')
    editor = models.ForeignKey('members.Member', related_name='posts')

Obtendríamos un error similar a este:

Reverse accessor 'Member.posts' for 'posts.Post.editor'
clashes with reverse accessor for 'posts.Post.writer'.

En estos casos debemos «romper» la regla y establecer nombres diferentes para related_name:

posts/models.py
class Post(models.Model):
    writer = models.ForeignKey('members.Member', related_name='writer_posts')
    editor = models.ForeignKey('members.Member', related_name='editor_posts')

Acción de borrado

El parámetro on_delete especifica qué acción debe tomar Django cuando se borra el objeto relacionado.

En el ejemplo anterior, vendría a significar, qué hacemos con los comentarios de un «post» que acabamos de «borrar».

Los posibles valores de este parámetro se muestran en la siguiente tabla:

Valor Acción
models.CASCADE Borrado en cascada de todos los objetos relacionados.
models.PROTECT Impide el borrado si existen objetos relacionados.
models.RESTRICT Impide el borrado si existen objetos relacionados (con matices).
models.SET_NULL Pone a NULL la clave ajena (sólo si admite nulos).
models.SET_DEFAULT Pone un valor por defecto en la clave ajena (necesario definir default).
models.SET Pone un valor dado en la clave ajena.
models.DO_NOTHING No hace nada. Deja que la base de datos gestione el error de integridad.

Claves ajenas nulas

Si en el ejemplo anterior, pudieran existir comentarios sin «post» (huérfanos), tendríamos que modificar ligeramente el modelo para admitir valores nulos:

comments/models.py
from django.db import models


class Comment(models.Model):
    alias = models.CharField(max_length=128)
    content = models.TextField(max_length=256)
    post = models.ForeignKey(
        'posts.Post',
        related_name='comments',
        on_delete=models.CASCADE,
        blank=True,
        null=True,
    )

Claves ajenas con usuario

Es muy habitual relacionar modelos con la clase User predefinida en Django. No es distinto de lo que hemos visto hasta ahora, pero sí vale la pena indicar cómo se referencia.

Siguiendo con el ejemplo del «blog», supongamos que queremos modelar la siguiente relación:

erDiagram
    USER ||--o{ COMMENT : writes

Un usuario escribe cero o muchos comentarios, pero un comentario lo escribe un único usuario.

Por tanto, tendremos que añadir una clave ajena al usuario en el modelo de comentario:

comments/models.py
from django.conf import settings
from django.db import models


class Comment(models.Model):
    content = models.TextField()
    post = models.ForeignKey(
        'posts.Post',
        related_name='comments',
        on_delete=models.CASCADE,
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,#(1)!
        related_name='comments',
        on_delete=models.CASCADE,#(2)!
    )

  1. Consulta acceso al modelo de usuario.
  2. Si borramos un usuario se borrarán todos sus comentarios.

Operaciones con claves ajenas

Veamos a continuación diferentes operaciones que podemos realizar con claves ajenas sobre el ejemplo concreto de los comentarios de un «post» dentro de un «blog»:

Creamos un «post» y a continuación creamos un comentario vinculado a dicho «post»:

>>> from posts.models import Post
>>> from comments.models import Comment

>>> post = Post.create(
...     title='Django makes it very simple',
...     slug='django-makes-it-very-simple',
...     content='You can save related objects quite fast',
... )

>>> comment = Comment.create(
...     alias='sdelquin',
...     content='You are absolutely right!',
...     post=post,#(1)!
... )

  1. Al final no deja de ser un atributo más al que asignamos un valor (objeto de clase Post).

Dado un «post» y un «comentario», vinculamos (asignamos) el comentario al «post»:

>>> from posts.models import Post
>>> from comments.models import Comment

>>> post = Post.objects.get(slug='django-is-awesome')
>>> comment = Comments.objects.get(content='Yes indeed!')

>>> comment.post = post#(1)!
>>> comment.save()

  1. Al final no deja de ser un atributo más al que asignamos un valor (objeto de clase Post).

Consultamos todos los comentarios de aquellos «posts» que empiecen por la palabra «Future»:

>>> from comments.models import Comment

>>> Comment.objects.filter(post__title__startswith('Future'))#(1)!

  1. Para acceder a un campo de un objeto relacionado (clave ajena) hay que utilizar doble subguión. Véase post__title.

1⃣ Borramos todos los comentarios de un determinado «post»:

>>> from posts.models import Post

>>> post = Post.objects.get(slug='django-is-awesome')
>>> post.comments.delete()#(1)!

  1. Utilizamos related-name para acceder a la relación inversa.

2⃣ Borramos (desvinculamos) un determinado comentario de un «post»:

>>> comment.post = None#(1)!
>>> comment.save()

  1. Esto sólo se podrá hacer si pueden existir comentarios "húerfanos" (consultar claves ajenas nulas).

Relaciones uno a uno

Django ofrece la posibilidad de definir relaciones uno a uno utilizando la clase OneToOneField().

Un caso de uso muy habitual de este tipo de relaciones es cuando queremos extender la información del usuario predefinido en Django.

Extendiendo el modelo de usuario

Dado que no tenemos acceso «directo» a modificar el modelo User predefinido en Django hay que buscar otras alternativas para extender dicho modelo.

La opción más «sencilla» es crear un modelo alternativo que disponga de una clave ajena al usuario de tipo uno a uno.

Veamos un ejemplo en el que queremos añadir la profesión y el teléfono a la información de usuario. Crearemos un nuevo modelo denominado Profile (perfil de usuario) que se relaciona con User y que contendrá los atributos «extendidos» de usuario:

erDiagram
    USER ||--|| PROFILE : has

Un usuario tiene un único perfil y cada perfil sólo pertenece a un usuario.

Como se puede observar, se trata de una relación 1:1 por lo que vamos a utilizar la clase OneToOneField() que proporciona Django. La implementación sería la siguiente:

users/models.py
from django.db import models
from django.conf import settings


class Profile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,#(1)!
        related_name='profile',#(2)!
        on_delete=models.CASCADE,
    )
    occupation = models.CharField(max_length=256, blank=True)
    phone = models.CharField(max_length=16, blank=True)

  1. Se podría poner directamente 'auth.User' aunque este acceso está más desacoplado.
  2. Al ser un OneToOneField() el related_name debería ser el nombre de la clase a la que pertenece en minúsculas singular.

Acceso al perfil

Dado un objeto user el acceso a su perfil sería user.profile gracias a la relación inversa definida en el modelo.

Relaciones muchos a muchos

Django avanzado

Planteamos el siguiente ejemplo en el que asignamos etiquetas a los «posts» de nuestro «blog»:

erDiagram
    POST }o--o{ LABEL : has

Un «post» tiene cero o muchas etiquetas y una etiqueta puede estar en cero o muchos «posts».

Lo primero será crear las etiquetas a través de un sencillo modelo Label:

labels/models.py
from django.db import models


class Label(models.Model):
    name = models.CharField(max_length=128)
    slug = models.SlugField(max_length=128, unique=True)

Aplicación independiente

El hecho de que una etiqueta pueda tener sentido fuera de los «posts» (por ejemplo aplicarse también a otros elementos) indica que podríamos crear una aplicación labels para almacenar los modelos.

Para relacionar los «posts» con las etiquetas usaremos un campo de tipo ManyToManyField:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()
    labels = models.ManyToManyField(#(1)!
        'labels.Label',#(2)!
        related_name='posts',#(3)!
        blank=True,#(4)!
    )

  1. El atributo labels hace referencia a múltiples etiquetas que puede tener un «post».
  2. El modelo vinculado es Label dentro de la aplicación labels.
  3. El parámetro related_name funciona igual que en casos anteriores.
  4. Específico para este caso en el que pueden haber «posts» que no tengan etiquetas.

No aplican aquí

Cuando no sea obligatorio que existan valores del campo ManyToManyField, esto se indicará únicamente usando blank=True. En este escenario null no tiene efecto ya que no hay forma de requerir una relación a nivel de la base de datos.

El argumento on_delete define cómo manejar la eliminación de un objeto relacionado. En un campo ManyToManyField, las relaciones se gestionan a través de una tabla intermedia automática. En este escenario on_delete no tiene efecto ya que no hay un «objeto relacionado» único que se deba eliminar directamente como en un ForeignKey.

Lo primero será crear etiquetas y «posts»:

>>> from labels.models import Label
>>> from posts.models import Post

>>> label_tech = Label.objects.create(name='Technology', slug='tech')
>>> label_ai = Label.objects.create(name='Artificial Intelligence', slug='ai')

>>> post_python = Post.objects.create(
...     title='Python',
...     slug='python',
...     content='Now is better than never',
... )
>>> post_midjourney = Post.objects.create(
...     title='Midjourney',
...     slug='midjourney',
...     content='Awesome images',
... )

Ahora podemos realizar distintas operaciones sobre el campo labels de tipo «muchos a muchos»:

Utilizamos el método add() para añadir objetos relacionados:

Añadir etiquetas a un «post»
>>> post_python.labels.add(label_tech)#(1)!

  1. También se pueden añadir varias a la etiquetas a la vez:

    >>> # Por separado
    >>> post_python.labels.add(label_tech, label_ai)
    
    >>> # Desde un iterable (queryset)
    >>> post_python.labels.add(*labels)
    

Añadir «posts» a una etiqueta
>>> label_ai.posts.add(post_midjourney)#(1)!

  1. También se pueden añadir varios «posts» a la vez:

    >>> # Por separado
    >>> label_ai.posts.add(post_python, post_midjourney)
    
    >>> # Desde un iterable (queryset)
    >>> label_ai.posts.add(*posts)
    

Utilizamos el método create() para crear y añadir objetos relacionados:

Crear etiqueta y añadirla a un «post»
>>> post_python.labels.create(name='Technology', slug='tech')#(1)!
<Label: Technology>

  1. Hay que darle valor a todos los atributos obligatorios de la etiqueta.

Crear «post» y añadirlo a una etiqueta
>>> label_ai.posts.create(title='Midjourney', content='Awesome images')#(1)!
<Post: Midjourney>

  1. Hay que darle valor a todos los atributos obligatorios del «post».

Utilizamos el método set() para reemplazar objetos relacionados:

Reemplazar las etiquetas de un «post»
>>> post_python.labels.set([label_tech, label_ai])#(1)!

  1. Pasamos un iterable de objetos.

Reemplazar los «posts» de una etiqueta
>>> label_ai.posts.set([post_python, post_midjourney])#(1)!

  1. Pasamos un iterable de objetos.

Utilizamos el método remove() para eliminar objetos relacionados:

Eliminar etiquetas de un «post»
>>> post_python.labels.remove(label_ai)#(1)!

  1. Para eliminar todas las etiquetas de un «post» utilizamos el método clear():

    >>> post_python.labels.clear()
    

Eliminar «posts» de una etiqueta
>>> label_tech.posts.remove(post_midjourney)#(1)!

  1. Para eliminar todos los «posts» de una etiqueta utilizamos el método clear():

    >>> label_ai.posts.clear()
    

Consultar las etiquetas de un «post»
>>> post_python.labels.all()#(1)!
<QuerySet [<Label: Technology>, <Label: Artificial Intelligence>]>

  1. También se puede aplicar filter(), get() o exclude().

Consultar los «posts» de una etiqueta
>>> label_ai.posts.all()#(1)!
<QuerySet [<Post: Midjourney>, <Post: Python>]>

  1. También se puede aplicar filter(), get() o exclude().

Interfaz administrativa

Para habilitar modelos «muchos a muchos» en la interfaz administrativa consulta esta documentación.

Relaciones muchos a muchos con modelo intermedio

Hay ocasiones en las que la relación «muchos a muchos» debe incluir atributos adicionales. Para ello Django nos ofrece la posibilidad de añadir un modelo intermedio en el campo ManyToManyField().

Si continuamos con el ejemplo anterior, supongamos que ahora queremos registrar los detalles del etiquetado de un determinado «post». Veamos cómo proceder.

El modelo para las etiquetas no sufre cambios:

labels/models.py
from django.db import models


class Label(models.Model):
    name = models.CharField(max_length=128)
    slug = models.SlugField(max_length=128, unique=True)

    def __str__(self):
        return self.name

Pero ahora se define un nuevo modelo que representa el «detalle de etiquetado del post» y que tendrá ese rol «intermedio»:

posts/models.py
from django.db import models


class PostLabelingDetail(models.Model):
    post = models.ForeignKey(#(1)!
        'posts.Post',
        related_name='post_labeling_details',
        on_delete=models.CASCADE,
    )
    label = models.ForeignKey(#(2)!
        'labels.Label',
        related_name='post_labeling_details',
        on_delete=models.CASCADE,
    )
    reason = models.CharField(max_length=256)#(3)!
    labeled_at = models.DateTimeField(auto_now_add=True)#(4)!

    class Meta:
        unique_together = ('post', 'label')#(5)!

    def __str__(self):
        return f'{self.reason} ({self.labeled_at.strftime("%d-%m-%Y")})'

  1. Esta clave ajena proviene del modelo Post.
  2. Esta clave ajena proviene del modelo Label.
  3. Este atributo adicional nos permite registrar la razón del etiquetado.
  4. Este atributo adicional nos permite registrar la fecha del etiquetado.
    • De esta forma sólo permitimos una única razón de etiquetado (por «post» y etiqueta).
    • Consulta valores únicos juntos.

Modelo dentro de aplicación

El hecho de que el modelo PostLabelingDetail viva en la aplicación posts se explica porque es un caso de etiquetado específico para «posts» que no tendría sentido para otro tipo de objetos y, por lo tanto, no conlleva la creación de una nueva aplicación.

El modelo para los «posts» incorpora ahora el atributo through con el modelo intermedio que se va a utilizar:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()
    published = models.BooleanField(default=False)
    labels = models.ManyToManyField(
        'labels.Label',
        related_name='posts',
        through='posts.PostLabelingDetail',
        blank=True,
    )

Lo primero será crear etiquetas y «posts»:

>>> from labels.models import Label
>>> from posts.models import Post

>>> label_tech = Label.objects.create(name='Technology', slug='tech')
>>> label_ai = Label.objects.create(name='Artificial Intelligence', slug='ai')

>>> post_python = Post.objects.create(
...     title='Python',
...     slug='python',
...     content='Now is better than never',
... )
>>> post_midjourney = Post.objects.create(
...     title='Midjourney',
...     slug='midjourney',
...     content='Awesome images',
... )

Ahora podemos realizar distintas operaciones sobre el campo labels de tipo «muchos a muchos con modelo intermedio»:

Al crear objetos en la relación intermedia PostLabelingDetail estaremos añadiendo etiquetas a «posts» (y viceversa):

>>> from posts.models import PostLabelingDetail

>>> PostLabelingDetail.objects.create(
...     post=post_python,
...     label=label_tech,
...     reason='Python is cool tech',#(1)!
... )
<PostLabelingDetail: Python is cool tech (23-11-2025)>

>>> PostLabelingDetail.objects.create(
...     post=post_python,
...     label=label_ai,
...     reason='Python is the language for AI',#(2)!
... )
<PostLabelingDetail: Python is the language for AI (23-11-2025)>

>>> PostLabelingDetail.objects.create(
...     post=post_midjourney,
...     label=label_tech,
...     reason='Midjourney is high tech',#(3)!
... )
<PostLabelingDetail: Midjourney is high tech (23-11-2025)>

>>> PostLabelingDetail.objects.create(
...     post=post_midjourney,
...     label=label_ai,
...     reason='Midjourney is generative AI',#(4)!
... )
<PostLabelingDetail: Midjourney is generative AI (23-11-2025)>

  1. No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.
  2. No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.
  3. No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.
  4. No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.

Añadir etiquetas a un «post»
>>> post_python.labels.add(#(1)!
...     label_tech,#(2)!
...     through_defaults={'reason': 'Python is cool tech'},#(3)!
... )

  1. Utilizamos el método add() para añadir una etiqueta.
  2. Indicamos la etiqueta a añadir.
    • Especificamos el/los campo(s) de la relación intermedia.
    • No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.

Añadir «posts» a una etiqueta
>>> label_ai.posts.add(#(1)!
...     post_python,#(2)!
...     through_defaults={'reason': 'Python is the language for AI'},#(3)!
... )     

  1. Utilizamos el método add() para añadir un «post».
  2. Indicamos el «post» a añadir.
    • Especificamos el/los campo(s) de la relación intermedia.
    • No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.

Crear una etiqueta y añadirla a un «post»
>>> post_python.labels.create(#(1)!
...     name='Technology', slug='tech',#(2)!
...     through_defaults={'reason': 'Python is cool tech'},#(3)!
... )
<Label: Technology>

  1. Utilizamos el método create() que devuelve el objeto creado.
  2. Hay que darle valor a todos los atributos obligatorios de la etiqueta.
    • Especificamos el/los campo(s) de la relación intermedia.
    • No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.

Crear un «post» y añadirlo a una etiqueta
>>> label_ai.posts.create(#(1)!
...     title='Midjourney', content='Awesome images',#(2)!
...     through_defaults={'reason': 'Midjourney is generative AI'},#(3)!
... )
<Post: Midjourney>

  1. Utilizamos el método create() que devuelve el objeto creado.
  2. Hay que darle valor a todos los atributos obligatorios del «post».
    • Especificamos el/los campo(s) de la relación intermedia.
    • No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.

Reemplazar las etiquetas de un «post»
>>> post_python.labels.set(#(1)!
...     [label_tech, label_ai],#(2)!
...     through_defaults={'reason': 'Python is cool tech'},#(3)!
... )

  1. Utilizamos el método set() que reemplaza objetos relacionados.
  2. Pasamos un iterable de objetos.
    • Especificamos el/los campo(s) de la relación intermedia.
    • No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.

Reemplazar los «posts» de una etiqueta
>>> label_ai.posts.set(#(1)!
...     [post_python, post_midjourney],#(2)!
...     through_defaults={'reason': 'Python is cool tech'},#(3)!
... )

  1. Utilizamos el método set() que reemplaza objetos relacionados.
  2. Pasamos un iterable de objetos.
    • Especificamos el/los campo(s) de la relación intermedia.
    • No es necesario añadir labeled_at ya que se almacena automáticamente gracias a auto_now_add.

Eliminar etiquetas de un «post»
>>> post_python.labels.remove(label_ai)#(1)!

  1. Para eliminar todas las etiquetas de un «post»:

    >>> post_python.labels.clear()
    

Eliminar «posts» de una etiqueta
>>> label_tech.posts.remove(post_midjourney)#(1)!

  1. Para eliminar todos los «posts» de una etiqueta:

    >>> label_ai.posts.clear()
    

Consultar las etiquetas de un «post»
>>> post_python.labels.all()#(1)!
<QuerySet [<Label: Technology>, <Label: Artificial Intelligence>]>

  1. También se puede aplicar filter(), get() o exclude().

Consultar los «posts» de una etiqueta
>>> label_ai.posts.all()#(1)!
<QuerySet [<Post: Midjourney>, <Post: Python>]>

  1. También se puede aplicar filter(), get() o exclude().

Consultar los detalles de etiquetado de un «post»
>>> post_python.post_labeling_details.all()#(1)!
<QuerySet [<PostLabelingDetail: Python is cool tech (23-11-2025)>,
           <PostLabelingDetail: Python is the language for AI (23-11-2025)>]>

  1. Ejemplo en plantilla:

    posts/templates/posts/post/detail.html
    <h1>{{ post }}</h1><!--(1)!-->
    <h2>Labels</h2>
    <ul class="labels">
        {% for detail in post.post_labeling_details.all %}
            <li>
                <b>{{ detail.label }}</b> <!--(2)!-->
                <span class="post-labeling-detail">
                    {{ detail }}<!--(3)!-->
                </span>
            </li>
        {% endfor %}
    </ul>
    

    1. Nos valemos de Post.__str__()
    2. Nos valemos de Label.__str__()
    3. Nos valemos de PostLabelingDetail.__str__()
Consultar los detalles de etiquetado de un «post» (desde la etiqueta)
>>> label_ai.post_labeling_details.all()
<QuerySet [<PostLabelingDetail: Python is the language for AI (23-11-2025)>,
           <PostLabelingDetail: Midjourney is generative AI (23-11-2025)>]> 

Interfaz administrativa

Para habilitar modelos «muchos a muchos» en la interfaz administrativa consulta esta documentación.

Campos de fichero

Django intermedio

Entre los distintos campos que podemos utilizar en un modelo Django están los campos de fichero que permiten almacenar (vincular) un fichero a un objeto.

Las dos opciones de las que disponemos son FileField e ImageField.

Vamos a partir de un ejemplo en el que modelamos un «post» con una imagen de portada («cover»). Para ello añadimos un nuevo campo cover de tipo ImageField:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()
    cover = models.ImageField(
        upload_to='covers',
        default='covers/nocover.png',
    )

Analicemos cada parámetro de ImageField por separado:

El atributo upload_to de un campo ImageField o FileField nos indica la carpeta a la que se van a subir los ficheros como ruta relativa a settings.MEDIA_ROOT que, por defecto, es la raíz de nuestro proyecto.

Es decir que si tenemos upload_to='covers' esto crearía una carpeta covers en el raíz de nuestro proyecto con las imágenes (portadas) de los «posts» que vayamos creando.

Carpetas organizadas por fecha

Con el parámetro upload_to es posible indicar la creación de carpetas (con fechas) a la hora de subir ficheros:

cover = models.ImageField(
    upload_to='covers/%Y/%m/%d/',
    default='covers/nocover.png',
)

En este caso las imágenes se subirán a MEDIA_ROOT/covers/2025/10/29/.
Las carpetas se pueden personalizar con el formato de strftime.

Suele ser una buena práctica (dependiendo del contexto) definir un valor por defecto para la imagen.

En el ejemplo anterior hemos establecido default='covers/nocover.png'. Esto quiere decir que cuando se guarde un nuevo objeto «post» sin especificar una portada, se asignará dicho valor, que será una ruta relativa a settings.MEDIA_ROOT.

Dependencias si trabajamos con imágenes

Cuando trabajamos con campos de tipo ImageField debemos instalar el paquete pillow:

$ pip install pillow
$ uv add pillow

Ruta del fichero

Django ofrece la posibilidad de modificar la configuración settings.MEDIA_ROOT para indicar la ruta «base» donde se van a almacenar este tipo de recursos en el sistema de ficheros.

Suele ser una buena práctica establecer /media como la carpeta para la subida de ficheros. Para ello modificamos la siguiente variable de la configuración del proyecto:

main/settings.py
MEDIA_ROOT = BASE_DIR / 'media'#(1)!

  1. BASE_DIR es una variable definida al comienzo de settings.py y que contiene la ruta absoluta a la raíz de nuestro proyecto Django.

Por tanto, si subimos un fichero 'tech.jpg' como portada de un «post», la ruta en el sistema de ficheros donde se guardará (partiendo de la raíz del proyecto) será:

blog
media
└── covers
    └── tech.jpg

Ruta en disco

Dado un objeto post de tipo Post podemos acceder a la ruta (absoluta) en disco de su imagen de portada mediante post.cover.path.

URL del fichero

Supongamos un ejemplo en el que pasamos un objeto post de tipo Post a una plantilla para renderizar su contenido. El acceso a la URL de la portada del «post» es muy sencillo:

posts/templates/posts/post/detail.html
<div class="post">
    <h1>{{ post }}</h1>
    <img src="{{ post.cover.url }}"/><!--(1)!-->
    <p>{{ post.content }}</p>
</div>

  1. El campo cover (ImageField) dispone de un atributo url que devuelve la URL de la imagen.

Suele ser una buena práctica establecer /media como la URL de acceso a los ficheros subidos. Para ello modificamos la siguiente variable de la configuración del proyecto:

main/settings.py
MEDIA_URL = 'media/'

URL de acceso

Si subimos un fichero 'tech.jpg' como portada de un «post», la URL de acceso será: http://localhost:8000/media/covers/tech.jpg.

Servidor de desarrollo

Es posible que después de todas estas configuraciones aún no logres ver correctamente la imagen de portada de este «post». Esto puede deberse a que el servidor de desarrollo no está configurado para ello.

Si es tu caso, debes agregar la siguiente configuración en las URLs de primer nivel:

main/urls.py
from django.conf import settings
from django.conf.urls.static import static

urlpattners = [
    # ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)#(1)!

  1. Estamos vinculando MEDIA_ROOT con MEDIA_URL para que Django sepa cómo servir ficheros subidos correctamente.

Producción

Esto sólo es válido para un entorno de desarrollo, cuando pasamos a producción habrá que configurar el servidor web correspondiente para gestionar los ficheros subidos.

Subida de ficheros

Una de las operaciones más habituales cuando manejamos FileField o ImageField es la subida de ficheros para poder actualizar el campo correspondiente.

Gestión del formulario

En este caso vamos a poner el ejemplo de añadir un «post» que tiene imagen de portada («cover») utilizando un formulario de modelo para ello:

posts/forms.py
from django import forms

from .models import Post


class AddPostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'content', 'cover')

Gestión de la plantilla

Cuando definimos un formulario en una plantilla que va a contener algún campo de fichero, es fundamental definir el atributo enctype del formulario para que todo funcione correctamente.

A continuación se muestra un ejemplo de un formulario para añadir un «post» a nuestra aplicación «blog»:

posts/templates/posts/post/add.html
<form method="post" enctype="multipart/form-data" novalidate>
    {% csrf_token %}
    {{ form }}
    <input type="submit" value="Add post">
</form>

Gestión de la vista

A la hora de procesar la subida de un campo de fichero en una vista, debemos tener en cuenta que la información correspondiente se encuentra en el diccionario request.FILES.

A continuación se implementa un ejemplo de vista para procesar el formulario anterior de creación de un nuevo «post»:

posts/views.py
from django.shortcuts import redirect, render

from .forms import AddPostForm


def add_post(request):
    if request.method == 'POST':
        if (form := AddPostForm(request.POST, request.FILES)).is_valid():#(1)!
            form.save()
            return redirect('posts:post-list')
    else:
        form = AddPostForm()
    return render(request, 'posts/post/add.html', {'form': form})

    • request.FILES se pasará como segundo argumento posicional o como files=request.FILES en formato nominal.
    • El resto de parámetros al constructor de formulario se pasarán a continuación.

Guardar de forma personalizada

Django intermedio

Hay ocasiones en las que nos interesa personalizar el guardado de un modelo para modificar determinados atributos o realizar otras acciones. Esto se consigue sobreescribiendo el método save() del modelo.

Vamos a retomar el ejemplo del «post». Supongamos que queremos, siempre que se guarde un objeto de tipo Post generar su «slug» a partir del título y que se almacene en la base de datos:

posts/models.py
from django.db import models
from django.utils.text import slugify


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def save(self, *args, **kwargs):#(1)!
        self.slug = slugify(self.title)#(2)!
        super().save(*args, **kwargs)#(3)!

  1. Esta es la forma de sobreescribir el método save().
  2. Convertimos el título a «slug».
  3. Llamamos al constructor de la clase base models.Model para almacenar definitivamente el objeto en disco.

Pensemos ahora en un ejemplo en el que sólo queremos crear el «slug» de un «post» la primera vez que se crea el objeto. Para ello hay que buscar una manera de identificar si estamos en ese momento.

Django proporciona un atributo _state que está disponible en todas las instancias de modelo y que dispone de un atributo adding que indica si el objeto aún no se ha guardado en la base de datos:

posts/models.py
from django.db import models
from django.utils.text import slugify


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def save(self, *args, **kwargs):
        if self._state.adding:#(1)!
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    • Si _state.adding es True El objeto se está creando.
    • Si _state.adding es False El objeto se está actualizando.
    • Históricamente se ha usado la condición if self.pk is None: para comprobar que el objeto aún no está en la base de datos.

Editable

Todos los campos de un modelo son editables desde la interfaz administrativa salvo que se indique lo contrario.

Supongamos por ejemplo que no queremos que el «slug» de un «post» se pueda modificar directamente desde la interfaz administrativa. Para ello debemos añadir el parámetro editable en la definición del campo:

posts/models.py
from django.db import models
from django.utils.text import slugify


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True, editable=False)
    content = models.TextField()

    def save(self, *args, **kwargs):
        if self._state.adding:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

URL canónica

Django intermedio

Django nos ofrece la posibilidad de asignar a cada instancia de modelo una URL canónica4. Para ello debemos implementar el método get_absolute_url().

Continuando con el ejemplo del «post», podríamos definir su URL canónica de la siguiente manera:

posts/models.py
from django.db import models
from django.urls import reverse
from django.utils.text import slugify


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def get_absolute_url(self):
        return reverse('posts:post-detail', args=[self.slug])#(1)!

  1. La función reverse ya nos devuelve la URL correspondiente.

URL canónica en vistas

Además de las redirecciones ya vistas, Django nos permite hacer una redirección sobre una instancia de un modelo. En ese caso se usará la URL canónica del objeto como URL de destino.

Supongamos el típico ejemplo en el que, después de dar de alta un «post» de un «blog» redirigimos al detalle de dicho «post»:

posts/views.py
from django.shortcuts import redirect    

from .forms import AddPostForm
from .models import Post


def add_post(request):
    if request.method == 'POST':
        if (form := AddPostForm(request.POST)).is_valid():
            post = form.save()
            return redirect(post)#(1)!
    else:
        form = AddPostForm()
    return render(request, 'posts/post/add.html', {'form': form})

    • Django obtiene la URL llamando a post.get_absolute_url() y hace la redirección.
    • En este caso se haría una redirección a /posts/this-is-a-test-post/.

URL canónica en plantillas

Podemos reaprovechar el método get_absolute_url() para utilizarlo en plantillas. En el siguiente ejemplo creamos un enlace a cada «post»:

posts/templates/posts/post/list.html
{% for posts in posts %}
    <div class="post">
        <a href="{{ post.get_absolute_url }}">{{ post }}</a>
    </div>
{% endfor %}

Ordenación por defecto

Django intermedio

Hemos visto ya cómo ordenar resultados de consultas pero Django también ofrece la posibilidad de definir una ordenación por defecto para los modelos.

Supongamos por ejemplo que queremos que todos los «posts» de nuestro «blog» se ordenen siempre por su título de forma ascendente. Tendríamos que hacer lo siguiente:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    class Meta:#(1)!
        ordering = ['title']#(2)!

  1. La clase interior Meta permite indicarle a Django ciertas opciones para un modelo.
    • Es posible añadir más de un campo: ['title', 'slug']
    • Es posible indicar ordenación descendente: '-title'
    • Es posible también usar una tupla.

Por supuesto esta ordenación por defecto siempre puede ser "invalidada" por un order_by() explícito.

Señales

Django avanzado

Las señales en Django permiten realizar acciones cuando suceden determinados eventos. Los más habituales están relacionados con modificación de modelos.

Señales de modelo

Veamos a continuación las señales de modelo incluidas en Django:

Esta señal se lanza al comienzo del método __init__()

from django.db.models.signals import pre_init
from .models import MyModel


@receiver(pre_init, sender=MyModel)
def my_signal_dispatcher(sender, *args, **kwargs):
    # your code here
Parámetros
  • sender: Clase que se está instanciando.
  • args: Argumentos posicionales pasados a __init__()
  • kwargs: Argumentos nominales pasados a __init__()

Esta señal se lanza al final del método __init__()

from django.db.models.signals import post_init
from .models import MyModel


@receiver(post_init, sender=MyModel)
def my_signal_dispatcher(sender, instance):
    # your code here
Parámetros
  • sender: Clase que se está instanciando.
  • instance: Instancia actual que está siendo creada.

Esta señal se lanza al comienzo del método save()

from django.db.models.signals import pre_save
from .models import MyModel


@receiver(pre_save, sender=MyModel)
def my_signal_dispatcher(sender, instance, raw, using, update_fields):
    # your code here
Parámetros
  • sender: Clase del modelo.
  • instance: Instancia actual que está siendo guardada.
  • raw: True cuando se usa ./manage.py loaddata, False en otro caso.
  • using: Alias de la base de datos que se está usando.
  • update_fields: Conjunto de campos que se van a actualizar.

Esta señal se lanza al final del método save()

from django.db.models.signals import post_save
from .models import MyModel


@receiver(post_save, sender=MyModel)
def my_signal_dispatcher(sender, instance, crated, raw, using, update_fields):
    # your code here
Parámetros
  • sender: Clase del modelo.
  • instance: Instancia actual que está siendo guardada.
  • created: True si se está creando un nuevo registro, False en otro caso.
  • raw: True cuando se usa ./manage.py loaddata, False en otro caso.
  • using: Alias de la base de datos que se está usando.
  • update_fields: Conjunto de campos que se van a actualizar.

Esta señal se lanza al comienzo del método delete()

from django.db.models.signals import pre_delete
from .models import MyModel


@receiver(pre_delete, sender=MyModel)
def my_signal_dispatcher(sender, instance, using, origin):
    # your code here
Parámetros
  • sender: Clase del modelo.
  • instance: Instancia actual que está siendo borrada.
  • using: Alias de la base de datos que se está usando.
  • origin: Origen del borrado (modelo o «queryset»).

Esta señal se lanza al final del método delete()

from django.db.models.signals import post_delete
from .models import MyModel


@receiver(post_delete, sender=MyModel)
def my_signal_dispatcher(sender, instance, using, origin):#(1)!
    # your code here
Parámetros
  • sender: Clase del modelo.
  • instance: Instancia actual que está siendo borrada (ya no está en la base de datos).
  • using: Alias de la base de datos que se está usando.
  • origin: Origen del borrado (modelo o «queryset»).

Esta señal se lanza al modificar un campo ManyToManyField de una instancia de modelo.

from django.db.models.signals import m2m_changed
from .models import MyModel


@receiver(m2m_changed, sender=MyModel)
def my_signal_dispatcher(sender, instance, action, reverse, model, pk_set, using):#(1)!
    # your code here
Parámetros
  • sender: Clase de modelo intermedio.
  • instance: Instancia cuya relación «muchos a muchos» se va a actualizar.
  • action: Tipo de actualización (str)
    • 'pre_add'
    • 'post_add'
    • 'pre_remove'
    • 'post_remove'
    • 'pre_clear'
    • 'post_clear'
  • reverse: Indica qué lado de la relación se está actualizando.
  • model: Clase de los objetos que se están añadiendo o borrando de la relación.
  • pk_set: Claves primarias de los objetos involucrados en la operación.
  • using: Alias de la base de datos que se está usando.

Registrar una señal

Vamos a ilustrar mediante un ejemplo la forma de trabajar con señales en Django. Partiendo de una aplicación de «blog», queremos implementar un artefacto software que, cada vez que se cree un nuevo «post», enviemos un correo al administrador notificando este hecho.

Lo primero será registrar la señal que en este caso se trata de post_save:

posts/signals.py
from django.db.models.signals import post_save#(1)!
from django.dispatch import receiver#(2)!

from .models import Post#(3)!
from .tasks import send_message#(4)!


@receiver(post_save, sender=Post)#(5)!
def notify_administrator_with_new_post(sender, instance, created, **kwargs):#(6)!
    if created:#(7)!
        msg = f'Post #{instance.pk} has been created!'#(8)!
        send_message('admin', msg)#(9)!

  1. Importamos la señal post_save.
  2. Para registrar la señal necesitamos el decorador receiver.
  3. Importamos el modelo sobre el que vamos a trabajar.
  4. Importamos la función que envía correos (ficticio, sólo a efectos de demostración).
  5. Registramos la señal post_save sobre la clase Post.
  6. Definimos la función que se va a ejecutar, teniendo en cuenta los parámetros necesarios.
  7. En este caso, sólo nos interesa aplicar la lógica cuando se crea una nueva instancia de «post».
  8. Construimos un mensaje usando la clave primaria del «post» (instance).
  9. Enviamos el mensaje al usuario admin.

Conectar una señal

Es necesario conectar la señal (o señales) con la aplicación en cuestión. Esto lo hacemos desde el fichero de configuración de la propia aplicación:

posts/apps.py
from django.apps import AppConfig


class PostsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'posts'

    def ready(self):#(1)!
        from . import signals#(2)!

  1. El método ready() nos indica el momento en el que la aplicación posts está disponible.
    • Importamos las señales previamente definidas.
    • Si el «linter» se queja de esta línea (import) podemos añadir comentario y solucionarlo de la siguiente manera:
      from . import signals  # noqa
      

Validación

Django avanzado

Cada campo de un modelo Django ya incorpora (por defecto) una serie de validaciones: por ejemplo un campo PositiveIntegerField() debe ser un número entero mayor que cero.

Pero existen ocasiones en las que queremos añadir nuevas validaciones a los campos existentes. Hay dos enfoques para ello:

1⃣ Validación individual
2⃣ Validación cruzada

Validación individual

La validación individual nos sirve para validar el valor de cada campo por separado haciendo uso de validadores.

Validadores predefinidos

Django proporciona una serie de validadores predefinidos. A continuación se muestran algunos de ellos:

Validador Permite...
RegexValidator Validar mediante una expresión regular.
MinValueValidator Establecer un valor mínimo.
MaxValueValidator Establecer un valor máximo.

Por ejemplo supongamos que los «posts» de un «blog» pueden ser valorados de 1 a 5. Podríamos definir el modelo de la siguiente manera:

posts/models.py
from django.core.validators import MaxValueValidator, MinValueValidator#(1)!
from django.db import models


class Post(models.Model):
    DEFAULT_RANK = 3

    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()
    rank = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],#(2)!
        default=DEFAULT_RANK,
    )

    def __str__(self):
        return self.title

  1. Importamos los validadores.
  2. Establecemos los validadores en la definición del campo.

Validadores personalizados

También es posible definir validadores personalizados. Se trata únicamente de definir una función que recibe el valor del campo y realiza las comprobaciones correspondiente.

Continuando con ejemplo del «blog» supongamos que nos interesa validar que el título de un «post» siempre se escriba en formato título:

posts/validators.py
from django.core.exceptions import ValidationError#(1)!


def validate_title(value: str):#(2)!
    if not value.istitle():#(3)!
        raise ValidationError(f'{value} is not in title format')#(4)!

  1. Los errores de validación hay que indicarlos mediante objetos de tipo ValidationError.
  2. El validador es simplemente una función que recibe value (valor a validar).
  3. Comprobamos si el valor (título del «post») está en formato título.
  4. Lanzamos una excepción con el mensaje de error correspondiente.

posts/models.py
from django.db import models

from .validators import validate_title#(1)!


class Post(models.Model):
    title = models.CharField(max_length=256, validators=[validate_title])#(2)!
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

  1. Importamos el validador personalizado que hemos creado.
  2. Asignamos el validador personalizado al campo del título del «post».

Comportamiento de los validadores

Los validadores no se ejecutan cuando guardamos «directamente» un modelo, sólo se ejecutan cuando creamos formularios de modelo y tratamos de guardar una instancia del mismo.

Validación cruzada

La validación cruzada nos sirve para validar el valor de un campo en relación a otros haciendo uso del método clean().

Supongamos por ejemplo que queremos validar en un «post» que siempre coincida su «slug» con la transformación correcta de su título:

posts/models.py
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.text import slugify

from .validators import validate_title


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

    def clean(self):
        if self.slug != slugify(self.title):
            raise ValidationError("Slug does not match with title's slugify")

Fixtures

Django avanzado

Una «fixture» es una colección de ficheros que contienen el contenido serializado5 de la base de datos. Django ofrece herramientas para poder gestionar fixtures.

Partiremos del ejemplo del «post» en un «blog»:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

Antes de nada, vamos a cargar algo de información en la base de datos para tener contenido sobre el que trabajar:

>>> Post.objects.create(
...     title="Small Changes",
...     slug="small-changes",
...     content="Small daily changes can lead to big results.",
... )
<Post: Small Changes>

>>> Post.objects.create(
...     title="Learning Takes Time",
...     slug="learning-takes-time",
...     content="Technology moves fast, but real learning takes time.",
... )
<Post: Learning Takes Time>

>>> Post.objects.create(
...     title="Thinking in Code",
...     slug="thinking-in-code",
...     content="Writing code is also a way of thinking.",
... )
<Post: Thinking in Code>

>>> Post.objects.create(
...     title="Useful Mistakes",
...     slug="useful-mistakes",
...     content="Not every error is a failure.",
... )
<Post: Useful Mistakes>

>>> Post.objects.create(
...     title="Curiosity",
...     slug="curiosity",
...     content="Great ideas are born from curiosity.",
... )
<Post: Curiosity>

Generar fixtures

Para generar (volcar) las fixtures debemos hacer uso del comando manage.py dumpdata. El formato de salida por defecto es JSON:

$ ./manage.py dumpdata --indent 2 posts
[
{
"model": "posts.post",
"pk": 1,
"fields": {
    "title": "Small Changes",
    "slug": "small-changes",
    "content": "Small daily changes can lead to big results."
}
},
{
"model": "posts.post",
"pk": 2,
"fields": {
    "title": "Learning Takes Time",
    "slug": "learning-takes-time",
    "content": "Technology moves fast, but real learning takes time."
}
},
{
"model": "posts.post",
"pk": 3,
"fields": {
    "title": "Thinking in Code",
    "slug": "thinking-in-code",
    "content": "Writing code is also a way of thinking."
}
},
{
"model": "posts.post",
"pk": 4,
"fields": {
    "title": "Useful Mistakes",
    "slug": "useful-mistakes",
    "content": "Not every error is a failure."
}
},
{
"model": "posts.post",
"pk": 5,
"fields": {
    "title": "Curiosity",
    "slug": "curiosity",
    "content": "Great ideas are born from curiosity."
}
}
]
$ uv run manage.py dumpdata --indent 2 posts
[
{
"model": "posts.post",
"pk": 1,
"fields": {
    "title": "Small Changes",
    "slug": "small-changes",
    "content": "Small daily changes can lead to big results."
}
},
{
"model": "posts.post",
"pk": 2,
"fields": {
    "title": "Learning Takes Time",
    "slug": "learning-takes-time",
    "content": "Technology moves fast, but real learning takes time."
}
},
{
"model": "posts.post",
"pk": 3,
"fields": {
    "title": "Thinking in Code",
    "slug": "thinking-in-code",
    "content": "Writing code is also a way of thinking."
}
},
{
"model": "posts.post",
"pk": 4,
"fields": {
    "title": "Useful Mistakes",
    "slug": "useful-mistakes",
    "content": "Not every error is a failure."
}
},
{
"model": "posts.post",
"pk": 5,
"fields": {
    "title": "Curiosity",
    "slug": "curiosity",
    "content": "Great ideas are born from curiosity."
}
}
]

Argumentos

Los argumentos utilizados en manage.py dumpdata son los siguientes:

  • --indent 2: la salida se formatea más «bonita» con una indentación de 2 espacios.
  • posts: nombre de aplicación desde la que se volcarán las fixtures

También se puede indicar app_label.ModelName para volcar las fixtures de un modelo en concreto.

Si en vez de mostrar el resultado «por pantalla» queremos volcarlo a un fichero podemos hacerlo con:

$ ./manage.py dumpdata --indent 2 posts -o posts.json
[...........................................................................]
$ uv run manage.py dumpdata --indent 2 posts -o posts.json
[...........................................................................]

Efectivamente el fichero generado posts.json es un JSON:

$ file posts.json
posts.json: JSON data

Cargar fixtures

Para cargar las fixtures debemos hacer uso del comando manage.py loaddata. Simplemente hay que indicar el archivo JSON donde se encuentra la información:

$ ./manage.py loaddata posts.json
Installed 5 object(s) from 1 fixture(s)
$ uv run manage.py loaddata posts.json
Installed 5 object(s) from 1 fixture(s)

Ubicación de «fixtures»

Si no se indica una ruta al fichero de «fixtures», Django lo buscará primero en la carpeta actual de trabajo y, si no lo encuentra, en la carpeta fixtures de cada una de las aplicaciones instaladas en el proyecto.

Managers

Django avanzado

Un «manager» de un modelo Django es una especie de manejador que nos permite interactuar con el ORM sobre el modelo en cuestión. El manager por defecto que incorpora Django a todos sus modelos es objects.

Así, por ejemplo, podríamos obtener todos los «posts» de nuestra base de datos mediante una consulta en el ORM con Post.objects.all().

Pero este escenario se puede ampliar de dos formas distintas:

  1. Añadiendo métodos adicionales al manager por defecto.
  2. Añadiendo managers adicionales al manager por defecto.

Vamos a partir del modelo base de Post para ejemplificar cada aproximación:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

Para disponer de datos iniciales en la base de datos, descargamos este fichero posts.json y cargamos las «fixtures»:

$ ./manage.py loaddata posts #(1)!
Installed 5 object(s) from 1 fixture(s)

  1. Aunque no estemos indicando la ruta del fichero, por defecto se van a buscar a la carpeta fixtures/ de cada aplicación del proyecto.

$ uv run manage.py loaddata posts #(1)!
Installed 5 object(s) from 1 fixture(s)

  1. Aunque no estemos indicando la ruta del fichero, por defecto se van a buscar a la carpeta fixtures/ de cada aplicación del proyecto.

Añadiendo métodos

Supongamos que queremos añadir al manager por defecto objects un método with_length() que nos devuelve los «posts» de la base de datos pero incluyendo una anotación con el tamaño de cada «post»:

posts/models.py
from django.db import models
from django.db.models.functions import Length#(1)!


class PostManager(models.Manager):#(2)!
    def with_length(self):#(3)!
        return self.annotate(length=Length('content'))


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

    objects = PostManager()#(4)!

  1. Importamos este recurso únicamente para aplicar la anotación en la consulta.
  2. Necesitamos implementar una clase (heredando de manager).
  3. Este sería el método que añadimos a los existentes.
  4. Asignamos esta nueva clase al manager por defecto objects.

Podemos comprobar el funcionamiento de este método de una manera muy sencilla:

>>> for post in Post.objects.with_length():
...     print(f'{post.title:20s} | {post.length}')
...
Small Changes        | 44
Learning Takes Time  | 52
Thinking in Code     | 39
Useful Mistakes      | 29
Curiosity            | 36

Añadiendo managers

Supongamos que queremos añadir algunos managers que devuelvan los «posts» pero filtrando su contenido por ciertas palabras clave, con lo que facilitar el acceso a los mismos:

main/posts.py
from django.db import models


class TechPostManager(models.Manager):#(1)!
    def get_queryset(self):#(2)!
        return super().get_queryset().filter(content__icontains='technology')#(3)!


class CodePostManager(models.Manager):#(4)!
    def get_queryset(self):#(5)!
        return super().get_queryset().filter(content__icontains='code')#(6)!


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

    objects = models.Manager()#(7)!
    tech = TechPostManager()#(8)!
    code = CodePostManager()#(9)!

  1. Implementamos un nuevo manager.
  2. Debemos sobreescribir el método get_queryset().
  3. Accedemos a la consulta desde la clase base para luego aplicar el filtro correspondiente.
  4. Implementamos un nuevo manager.
  5. Debemos sobreescribir el método get_queryset().
  6. Accedemos a la consulta desde la clase base para luego aplicar el filtro correspondiente.
  7. No queremos «perder» el manager por defecto objects.
  8. Creamos un nuevo manager tech desde la clase previamente implementada.
  9. Creamos un nuevo manager code desde la clase previamente implementada.

Podemos comprobar el funcionamiento de estos managers de una manera muy sencilla:

>>> for post in Post.objects.all():#(1)!
...     print(f'{post.title:20s} | {post.content}')
...
Small Changes        | Small daily changes can lead to big results.
Learning Takes Time  | Technology moves fast, but real learning takes time.
Thinking in Code     | Writing code is also a way of thinking.
Useful Mistakes      | Not every error is a failure.
Curiosity            | Great ideas are born from curiosity.

>>> for post in Post.tech.all():#(2)!
...     print(f'{post.title:20s} | {post.content}')
...
Learning Takes Time  | Technology moves fast, but real learning takes time.

>>> for post in Post.code.all():#(3)!
...     print(f'{post.title:20s} | {post.content}')
...
Thinking in Code     | Writing code is also a way of thinking.

  1. Seguimos disponiendo del manager por defecto objects.
  2. El nuevo manager tech nos devuelve solo aquellos «posts» que tengan que ver con tecnología.
  3. El nuevo manager code nos devuelve solo aquellos «posts» que tengan que ver con código.

Funciones

Hay ocasiones en las que nos interesa aplicar ciertas funciones predefinidas sobre una determinada consulta de la base de datos.

En este apartado veremos los dos posibles enfoques que nos proporciona Django para ello:

  1. Agregación.
  2. Anotación.

Vamos a partir del modelo base de Post para ejemplificar cada aproximación:

posts/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

Para disponer de datos iniciales en la base de datos, descargamos este fichero posts.json y cargamos las «fixtures»:

$ ./manage.py loaddata posts #(1)!
Installed 5 object(s) from 1 fixture(s)

  1. Aunque no estemos indicando la ruta del fichero, por defecto se van a buscar a la carpeta fixtures/ de cada aplicación del proyecto.

$ uv run manage.py loaddata posts #(1)!
Installed 5 object(s) from 1 fixture(s)

  1. Aunque no estemos indicando la ruta del fichero, por defecto se van a buscar a la carpeta fixtures/ de cada aplicación del proyecto.

Agregación

La agregación permite aplicar una función «resumen» sobre un conjunto de datos («queryset») obteniendo así un «nuevo» atributo con el valor calculado.

Supongamos por ejemplo que queremos obtener la longitud media de todos los «posts» de nuestra base de datos.

La primera aproximación que se nos viene a la cabeza podría ser la siguiente:

>>> from posts.models import Post

>>> posts = Post.objects.all()
>>> sum(len(post.content) for post in posts) / len(posts)
40.0

Pero aplicando agregación podemos obtener el mismo resultado por otro camino:

>>> from posts.models import Post
>>> from django.db.models import Avg#(1)!
>>> from django.db.models.functions import Length#(2)!

>>> Post.objects.aggregate(avg_length=Avg(Length('content')))#(3)!
{'avg_length': 40.0}

  1. Función de agregación que calcula la media.
  2. Función de base de datos que calcula la longitud de una cadena de caracteres.
  3. Se devuelve un diccionario con el nuevo atributo agregado.

La gran ventaja de este enfoque frente al enfoque «tradicional» (Python) es que realmente se está ejecutando una sentencia SQL más eficiente a nivel de base de datos.

Django nos ofrece una gran cantidad de recursos que podemos aplicar en este contexto:

Anotación

La anotación permite aplicar una función sobre cada objeto de un conjunto de datos («queryset») obteniendo así un nuevo atributo con el valor calculado.

Supongamos por ejemplo que queremos añadir («anotar») la longitud del contenido de cada «post» de nuestra base de datos.

La primera aproximación que se nos viene a la cabeza podría ser la siguiente:

>>> from posts.models import Post

>>> for post in Post.objects.all():
...     print(post, len(post.content), sep='|')
...
Small Changes|44
Learning Takes Time|52
Thinking in Code|39
Useful Mistakes|29
Curiosity|36

Pero aplicando anotación podemos obtener el mismo resultado por otro camino:

>>> from posts.models import Post
>>> from django.db.models.functions import Length#(1)!

>>> for post in Post.objects.annotate(length=Length('content')):
...     print(post, post.length, sep='|')#(2)!
...
Small Changes|44
Learning Takes Time|52
Thinking in Code|39
Useful Mistakes|29
Curiosity|36

  1. Función de base de datos que calcula la longitud de una cadena de caracteres.
  2. Aparece un nuevo atributo length que podemos utilizar con normalidad.

  1. En la práctica hay ciertos aspectos a tener en cuenta cuando usamos distintos sistemas gestores de bases de datos. 

  2. El slug es la parte que identifica a una página en concreto dentro de una URL amigable 

  3. Es importante manejar el módulo pathlib de la librería estándar para manejar rutas en el sitema de ficheros. 

  4. Una URL canónica es la URL que mejor representa un determinado recurso web. 

  5. La información se convierte en una secuencia estructurada de datos (por ejemplo, texto o bytes) para poder guardarse, transmitirse o reconstruirse después.