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:
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
- Necesitamos importar el módulo
modelsque nos dará aquellas funcionalidades necesarias para implementar nuestros modelos. -
- La clase debe heredar de
models.Modelpara 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).
- La clase debe heredar de
- 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 |
- 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
Truehace 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
-
- Puesto a
Truehace 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
-
- Puesto a
Truehace 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
-
- Puesto a
Truehace 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.
- Puesto a
-
- 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=5decimal_places=2
- Número máximo de caracteres que se pueden almacenar en la base de datos.
- Ruta en la que almacenar el archivo una vez que se suba.
- Objeto de almacenamiento.
- Número máximo de caracteres que se pueden almacenar en la base de datos.
- Ruta de sistema desde la que se extraen las opciones del campo.
- Expresión regular para filtrar nombres de fichero.
-
- Si es
Truese listará recursivamente todo el contenido de la ruta indicada. - Por defecto es
False.
- Si es
-
- Si es
Truepermite el listado de ficheros. - Por defecto es
True.
- Si es
-
- Si es
Truepermite el listado de directorios. - Por defecto es
False.
- Si es
- Número máximo de caracteres que se pueden almacenar en la base de datos.
- Objeto de tipo
Expressionpara el cálculo del campo. - Instancia de campo de modelo que define el tipo de datos del campo.
-
- Si es
Truela columna en la base de datos será almacenada como real. - En otro caso la columna actuará como una columna virtual.
- Si es
- Limita la entrada al protocolo especificado.
- Desempaqueta direcciones IPv4.
- Ruta en la que almacenar el archivo una vez que se suba.
- Nombre de un campo de modelo que contendrá el alto de la imagen.
- Nombre de un campo de modelo que contendrá el ancho de la imagen.
- Número máximo de caracteres que se pueden almacenar en la base de datos.
- Una subclase de
JSONEncoderpara serializar los tipos de datos no soportados por el serializador JSON. - Una subclase de
JSONDecoderpara deserializar la entrada. - Número máximo de caracteres que se pueden almacenar en la base de datos.
-
- Puesto a
Truehace 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
-
- Puesto a
Truehace 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.
- Puesto a
- 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
- 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
- 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
- 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
- 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»:
# 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
- 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
- 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:
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:
Y a continuación aplicamos la migración:
Si visualizamos el registro de migraciones veremos que esta última migración ya se ha aplicado:
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
- Indicamos el número de la migración a la que «regresar».
- 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
- Indicamos el número de la migración a la que «regresar».
- 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:
Ahora si volvemos a comprobar el registro de migraciones, vemos que todo está como esperaríamos:
Por último, para volver a dejar todo «como estaba», modificamos de nuevo el tamaño de los campos como se había especificado inicialmente:
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:
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»:
- 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:
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:
- Añadir el campo
pidcomo opcional. - Implementar una migración manual para rellenar los datos de
pid. - Modificar el campo
pidpara que sea requerido y único.
Añadir campo opcional¶
Modificamos la definición del atributo pid para hacerlo opcional:
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:
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:
Editamos la migración creada para añadir el código necesario:
# 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)!
]
- Cada función a aplicar en la migración manual recibe dos parámetros:
appsque contiene un registro de todas las versiones históricas de los modelos.schema_editorque permite hacer cambios manuales en la base de datos.
- Obtenemos el modelo
Postde la aplicaciónposts(en el momento histórico actual). - Recorremos todos los «posts» existentes actualmente.
- Aplicamos un hash md5 al título para obtener el nuevo identificador de post.
- Guardamos los cambios en la base de datos.
- Cada función a aplicar en la migración manual recibe dos parámetros:
appsque contiene un registro de todas las versiones históricas de los modelos.schema_editorque permite hacer cambios manuales en la base de datos.
- Obtenemos el modelo
Postde la aplicaciónposts(en el momento histórico actual). - Recorremos todos los «posts» existentes actualmente.
- Reseteamos el identificador de post como cadena vacía.
- Guardamos los cambios en la base de datos.
- Esta clase es la migración en sí.
- Se establece la dependencia de esta migración justo con la anterior.
- Esta variable lleva un registro de las operaciones a realizar en la migración.
- 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.
- La función
Una vez creada esta migración, procedemos a aplicarla:
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:
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:
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:
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:
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:
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:
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:
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
Metapermite indicarle a Django ciertas opciones para un modelo. - Por una cuestión de «estilo» se suele escribir justo debajo de los atributos del modelo.
- La clase interior
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 comoNULL. -
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... |
|---|---|
CharFieldEmailFieldSlugFieldTextField |
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:
$ 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:
- 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:
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)!
- No sería necesario si usamos la shell de Django (importación automática de objetos).
- Llamada al constructor del modelo. En este momento el objeto sólo se encuentra en memoria.
- 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',
... )
- No sería necesario si usamos la shell de Django (importación automática de objetos).
- 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:
Podemos abrir una «shell» de base de datos con el comando:
$ 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:
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:
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:
SQL
Django convierte cada llamada al ORM en su correspondiente instrucción SQL. Esto se puede ver fácilmente accediendo al atributo query:
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:
-
- El atributo
objectses el «manager» por defecto. - El método
filter()devuelve un QuerySet (una especie de lista «perezosa» de objetos). startswithes un «field lookup». Existen muchos otros.
- El atributo
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:
- Podríamos haber utilizado
Post.objects.get(id=7)pero el hecho de usarpkes 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:
Es posible que en cierta documentación de Django encuentres la siguiente «fórmula» para obtener un único objeto:
- Utilizamos la función
first().
Hay que diferenciar dos casos:
- Si el «post» que buscamos existe, lo obtendremos en la variable
post. - Si el «post» que buscamos no existe, obtendremos
None(a diferencia deget()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
objectses el «manager» por defecto. - El método
filter()devuelve un QuerySet (una especie de lista «perezosa» de objetos). startswithes un «field lookup». Existen muchos otros.- El método
exclude()devuelve un QuerySet (una especie de lista «perezosa» de objetos). endswithes un «field lookup». Existen muchos otros.
- El atributo
Encadenados
Es muy importante hacer notar que las consultas en Django están diseñadas para que puedan encadenarse unas con otras:
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:
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. |
-
- En SQLite ignora mayúsculas/minúsculas.
- En PostgreSQL respeta mayúsculas/minúsculas.
-
- En SQLite ignora mayúsculas/minúsculas.
- En PostgreSQL respeta mayúsculas/minúsculas.
-
- En SQLite ignora mayúsculas/minúsculas.
- En PostgreSQL respeta mayúsculas/minúsculas.
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():
- Devuelve una tupla con:
Número total de objetos borrados.
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:
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:
- 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:
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:
-
- 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:
last devuelve el último objeto del QuerySet correspondiente. Por ejemplo para obtener el último «post» por orden de título haríamos:
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:
-
- 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'
- Recuperamos un determinado «post» de nuestro «blog».
- Comprobamos su contenido.
- Actualizamos su contenido (en la base de datos).
- Comprobamos su contenido (en memoria) que no está sincronizado con la base de datos.
- Refrescamos el objeto desde la base de datos.
- 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:
Tipos enumerados basados en cadenas de texto → Enumerados textuales.
Tipos enumerados basados en números enteros → Enumerados 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:
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:
- El nombre largo será
'Science Fiction'inferido desdeSCIENCE_FICTION.
- El nombre largo será
- Cada atributo de clase indica una opción en formato tupla:
-
El campo se define como un
CharField(). - El tamaño máximo debe coincidir con la longitud del código corto.
- Se definen las opciones con referencia a la clase interior.
- 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»:
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:
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»:
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:
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.
- Cada atributo de clase indica una opción en formato tupla:
-
- El campo se define como un
IntegerField(). - Aunque dependiendo del contexto se podría usar un
PositiveSmallIntegerField().
- El campo se define como un
- Se definen las opciones con referencia a la clase interior.
- 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:
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:
Relaciones uno a muchos → \(1:N\)
Relaciones uno a uno → \(1:1\)
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:
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:
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:
Modelo relacionado
Nombre relacionado
Acción de borrado
Modelo relacionado¶
El primer parámetro que recibe ForeignKey es el modelo que vamos a relacionar.
Hay dos formas de indicarlo:
- Si se indica en formato «string» hay que especificarlo como:
'<app>.<Model>'. (1) - También podemos importar el modelo y hacer referencia directa.
- Esto permite evitar los llamados «circular imports».
Nombre relacionado¶
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:
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>]>
- La forma
antinatural podría sería:Comment.objects.filter(post=post)
related_name no es un parámetro requerido, pero es altamente recomendable incluirlo.
Colisión¶
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»:
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:
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:
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:
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)!
)
- Consulta acceso al modelo de usuario.
- 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)!
... )
- 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()
- 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)!
- Para acceder a un campo de un objeto relacionado (clave ajena) hay que utilizar doble subguión. Véase
post__title.
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)!
- Utilizamos
related-namepara acceder a la relación inversa.
Borramos (desvinculamos) un determinado comentario de un «post»:
- 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:
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)
- Se podría poner directamente
'auth.User'aunque este acceso está más desacoplado. - Al ser un
OneToOneField()elrelated_namedeberí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:
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:
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)!
)
- El atributo
labelshace referencia a múltiples etiquetas que puede tener un «post». - El modelo vinculado es
Labeldentro de la aplicaciónlabels. - El parámetro
related_namefunciona igual que en casos anteriores. - 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:
-
También se pueden añadir varias a la etiquetas a la vez:
-
También se pueden añadir varios «posts» a la vez:
Utilizamos el método create() para crear y añadir objetos relacionados:
>>> post_python.labels.create(name='Technology', slug='tech')#(1)!
<Label: Technology>
- Hay que darle valor a todos los atributos obligatorios de la etiqueta.
>>> label_ai.posts.create(title='Midjourney', content='Awesome images')#(1)!
<Post: Midjourney>
- Hay que darle valor a todos los atributos obligatorios del «post».
Utilizamos el método set() para reemplazar objetos relacionados:
- Pasamos un iterable de objetos.
- Pasamos un iterable de objetos.
Utilizamos el método remove() para eliminar objetos relacionados:
-
Para eliminar todas las etiquetas de un «post» utilizamos el método
clear():
-
Para eliminar todos los «posts» de una etiqueta utilizamos el método
clear():
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:
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»:
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")})'
- Esta clave ajena proviene del modelo
Post. - Esta clave ajena proviene del modelo
Label. - Este atributo adicional nos permite registrar la razón del etiquetado.
- 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:
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)>
- No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add. - No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add. - No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add. - No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add.
>>> post_python.labels.add(#(1)!
... label_tech,#(2)!
... through_defaults={'reason': 'Python is cool tech'},#(3)!
... )
- Utilizamos el método
add()para añadir una etiqueta. - Indicamos la etiqueta a añadir.
-
- Especificamos el/los campo(s) de la relación intermedia.
- No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add.
>>> label_ai.posts.add(#(1)!
... post_python,#(2)!
... through_defaults={'reason': 'Python is the language for AI'},#(3)!
... )
- Utilizamos el método
add()para añadir un «post». - Indicamos el «post» a añadir.
-
- Especificamos el/los campo(s) de la relación intermedia.
- No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add.
>>> post_python.labels.create(#(1)!
... name='Technology', slug='tech',#(2)!
... through_defaults={'reason': 'Python is cool tech'},#(3)!
... )
<Label: Technology>
- Utilizamos el método
create()que devuelve el objeto creado. - 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_atya que se almacena automáticamente gracias aauto_now_add.
>>> label_ai.posts.create(#(1)!
... title='Midjourney', content='Awesome images',#(2)!
... through_defaults={'reason': 'Midjourney is generative AI'},#(3)!
... )
<Post: Midjourney>
- Utilizamos el método
create()que devuelve el objeto creado. - 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_atya que se almacena automáticamente gracias aauto_now_add.
>>> post_python.labels.set(#(1)!
... [label_tech, label_ai],#(2)!
... through_defaults={'reason': 'Python is cool tech'},#(3)!
... )
- Utilizamos el método
set()que reemplaza objetos relacionados. - Pasamos un iterable de objetos.
-
- Especificamos el/los campo(s) de la relación intermedia.
- No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add.
>>> label_ai.posts.set(#(1)!
... [post_python, post_midjourney],#(2)!
... through_defaults={'reason': 'Python is cool tech'},#(3)!
... )
- Utilizamos el método
set()que reemplaza objetos relacionados. - Pasamos un iterable de objetos.
-
- Especificamos el/los campo(s) de la relación intermedia.
- No es necesario añadir
labeled_atya que se almacena automáticamente gracias aauto_now_add.
-
Para eliminar todas las etiquetas de un «post»:
-
Para eliminar todos los «posts» de una etiqueta:
>>> post_python.labels.all()#(1)!
<QuerySet [<Label: Technology>, <Label: Artificial Intelligence>]>
>>> label_ai.posts.all()#(1)!
<QuerySet [<Post: Midjourney>, <Post: Python>]>
>>> 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)>]>
-
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>- Nos valemos de
Post.__str__() - Nos valemos de
Label.__str__() - Nos valemos de
PostLabelingDetail.__str__()
- Nos valemos de
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:
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:
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:
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:
BASE_DIRes una variable definida al comienzo desettings.pyy 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á:
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:
<div class="post">
<h1>{{ post }}</h1>
<img src="{{ post.cover.url }}"/><!--(1)!-->
<p>{{ post.content }}</p>
</div>
- El campo
cover(ImageField) dispone de un atributourlque 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:
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:
from django.conf import settings
from django.conf.urls.static import static
urlpattners = [
# ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)#(1)!
- Estamos vinculando
MEDIA_ROOTconMEDIA_URLpara 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:
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»:
<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»:
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.FILESse pasará como segundo argumento posicional o comofiles=request.FILESen 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:
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)!
- Esta es la forma de sobreescribir el método
save(). - Convertimos el título a «slug».
- Llamamos al constructor de la clase base
models.Modelpara 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:
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.addingesTrueEl objeto se está creando. - Si
_state.addingesFalseEl 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.
- Si
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:
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:
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)!
- 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»:
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/.
- Django obtiene la URL llamando a
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»:
{% 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:
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)!
- La clase interior
Metapermite 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.
- Es posible añadir más de un campo:
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:Truecuando se usa./manage.py loaddata,Falseen 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:Truesi se está creando un nuevo registro,Falseen otro caso.raw:Truecuando se usa./manage.py loaddata,Falseen 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:
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)!
- Importamos la señal
post_save. - Para registrar la señal necesitamos el decorador
receiver. - Importamos el modelo sobre el que vamos a trabajar.
- Importamos la función que envía correos (ficticio, sólo a efectos de demostración).
- Registramos la señal
post_savesobre la clasePost. - Definimos la función que se va a ejecutar, teniendo en cuenta los parámetros necesarios.
- En este caso, sólo nos interesa aplicar la lógica cuando se crea una nueva instancia de «post».
- Construimos un mensaje usando la clave primaria del «post» (
instance). - 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:
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)!
- El método
ready()nos indica el momento en el que la aplicaciónpostsestá 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:
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:
Validación individual
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:
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
- Importamos los validadores.
- 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:
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)!
- Los errores de validación hay que indicarlos mediante objetos de tipo
ValidationError. - El validador es simplemente una función que recibe
value(valor a validar). - Comprobamos si el valor (título del «post») está en formato título.
- Lanzamos una excepción con el mensaje de error correspondiente.
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
- Importamos el validador personalizado que hemos creado.
- 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:
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»:
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:
Efectivamente el fichero generado posts.json es un JSON:
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:
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:
- Añadiendo métodos adicionales al manager por defecto.
- Añadiendo managers adicionales al manager por defecto.
Vamos a partir del modelo base de Post para ejemplificar cada aproximación:
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»:
- 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»:
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)!
- Importamos este recurso únicamente para aplicar la anotación en la consulta.
- Necesitamos implementar una clase (heredando de manager).
- Este sería el método que añadimos a los existentes.
- 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:
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)!
- Implementamos un nuevo manager.
- Debemos sobreescribir el método
get_queryset(). - Accedemos a la consulta desde la clase base para luego aplicar el filtro correspondiente.
- Implementamos un nuevo manager.
- Debemos sobreescribir el método
get_queryset(). - Accedemos a la consulta desde la clase base para luego aplicar el filtro correspondiente.
- No queremos «perder» el manager por defecto
objects. - Creamos un nuevo manager
techdesde la clase previamente implementada. - Creamos un nuevo manager
codedesde 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.
- Seguimos disponiendo del manager por defecto
objects. - El nuevo manager
technos devuelve solo aquellos «posts» que tengan que ver con tecnología. - El nuevo manager
codenos 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:
- Agregación.
- Anotación.
Vamos a partir del modelo base de Post para ejemplificar cada aproximación:
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»:
- 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}
- Función de agregación que calcula la media.
- Función de base de datos que calcula la longitud de una cadena de caracteres.
- 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
- Función de base de datos que calcula la longitud de una cadena de caracteres.
- Aparece un nuevo atributo
lengthque podemos utilizar con normalidad.
-
En la práctica hay ciertos aspectos a tener en cuenta cuando usamos distintos sistemas gestores de bases de datos. ↩
-
El slug es la parte que identifica a una página en concreto dentro de una URL amigable ↩
-
Es importante manejar el módulo
pathlibde la librería estándar para manejar rutas en el sitema de ficheros. ↩ -
Una URL canónica es la URL que mejor representa un determinado recurso web. ↩
-
La información se convierte en una secuencia estructurada de datos (por ejemplo, texto o bytes) para poder guardarse, transmitirse o reconstruirse después. ↩