16 KiB
Bases de Datos SQL (Relacionales)
FastAPI no requiere que uses una base de datos SQL (relacional). Pero puedes utilizar cualquier base de datos que desees.
Aquí veremos un ejemplo usando SQLModel.
SQLModel está construido sobre SQLAlchemy y Pydantic. Fue creado por el mismo autor de FastAPI para ser la combinación perfecta para aplicaciones de FastAPI que necesiten usar bases de datos SQL.
/// tip | Consejo
Puedes usar cualquier otro paquete de bases de datos SQL o NoSQL que quieras (en algunos casos llamadas "ORMs"), FastAPI no te obliga a usar nada. 😎
///
Como SQLModel se basa en SQLAlchemy, puedes usar fácilmente cualquier base de datos soportada por SQLAlchemy (lo que las hace también soportadas por SQLModel), como:
- PostgreSQL
- MySQL
- SQLite
- Oracle
- Microsoft SQL Server, etc.
En este ejemplo, usaremos SQLite, porque utiliza un solo archivo y Python tiene soporte integrado. Así que puedes copiar este ejemplo y ejecutarlo tal cual.
Más adelante, para tu aplicación en producción, es posible que desees usar un servidor de base de datos como PostgreSQL.
/// tip | Consejo
Hay un generador de proyectos oficial con FastAPI y PostgreSQL que incluye un frontend y más herramientas: https://github.com/fastapi/full-stack-fastapi-template
///
Este es un tutorial muy simple y corto, si deseas aprender sobre bases de datos en general, sobre SQL o más funcionalidades avanzadas, ve a la documentación de SQLModel.
Instalar SQLModel
Primero, asegúrate de crear tu entorno virtual{.internal-link target=_blank}, actívalo, y luego instala sqlmodel
:
$ pip install sqlmodel
---> 100%
Crear la App con un Solo Modelo
Primero crearemos la versión más simple de la aplicación con un solo modelo de SQLModel.
Más adelante la mejoraremos aumentando la seguridad y versatilidad con múltiples modelos a continuación. 🤓
Crear Modelos
Importa SQLModel
y crea un modelo de base de datos:
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *}
La clase Hero
es muy similar a un modelo de Pydantic (de hecho, en el fondo, realmente es un modelo de Pydantic).
Hay algunas diferencias:
-
table=True
le dice a SQLModel que este es un modelo de tabla, que debe representar una tabla en la base de datos SQL, no es solo un modelo de datos (como lo sería cualquier otra clase regular de Pydantic). -
Field(primary_key=True)
le dice a SQLModel queid
es la clave primaria en la base de datos SQL (puedes aprender más sobre claves primarias de SQL en la documentación de SQLModel).Al tener el tipo como
int | None
, SQLModel sabrá que esta columna debe ser unINTEGER
en la base de datos SQL y que debe serNULLABLE
. -
Field(index=True)
le dice a SQLModel que debe crear un índice SQL para esta columna, lo que permitirá búsquedas más rápidas en la base de datos cuando se lean datos filtrados por esta columna.SQLModel sabrá que algo declarado como
str
será una columna SQL de tipoTEXT
(oVARCHAR
, dependiendo de la base de datos).
Crear un Engine
Un engine
de SQLModel (en el fondo, realmente es un engine
de SQLAlchemy) es lo que mantiene las conexiones a la base de datos.
Tendrías un solo objeto engine
para todo tu código para conectar a la misma base de datos.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *}
Usar check_same_thread=False
permite a FastAPI usar la misma base de datos SQLite en diferentes hilos. Esto es necesario ya que una sola request podría usar más de un hilo (por ejemplo, en dependencias).
No te preocupes, con la forma en que está estructurado el código, nos aseguraremos de usar una sola session de SQLModel por request más adelante, esto es realmente lo que intenta lograr el check_same_thread
.
Crear las Tablas
Luego añadimos una función que usa SQLModel.metadata.create_all(engine)
para crear las tablas para todos los modelos de tabla.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *}
Crear una Dependencia de Session
Una Session
es lo que almacena los objetos en memoria y lleva un seguimiento de cualquier cambio necesario en los datos, luego usa el engine
para comunicarse con la base de datos.
Crearemos una dependencia de FastAPI con yield
que proporcionará una nueva Session
para cada request. Esto es lo que asegura que usemos una sola session por request. 🤓
Luego creamos una dependencia Annotated
SessionDep
para simplificar el resto del código que usará esta dependencia.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *}
Crear Tablas de Base de Datos al Arrancar
Crearemos las tablas de la base de datos cuando arranque la aplicación.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *}
Aquí creamos las tablas en un evento de inicio de la aplicación.
Para producción probablemente usarías un script de migración que se ejecuta antes de iniciar tu aplicación. 🤓
/// tip | Consejo
SQLModel tendrá utilidades de migración envolviendo Alembic, pero por ahora, puedes usar Alembic directamente.
///
Crear un Hero
Debido a que cada modelo de SQLModel también es un modelo de Pydantic, puedes usarlo en las mismas anotaciones de tipos que podrías usar en modelos de Pydantic.
Por ejemplo, si declaras un parámetro de tipo Hero
, será leído desde el JSON body.
De la misma manera, puedes declararlo como el tipo de retorno de la función, y luego la forma de los datos aparecerá en la interfaz automática de documentación de la API.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *}
Aquí usamos la dependencia SessionDep
(una Session
) para añadir el nuevo Hero
a la instance Session
, comiteamos los cambios a la base de datos, refrescamos los datos en el hero
y luego lo devolvemos.
Leer Heroes
Podemos leer Hero
s de la base de datos usando un select()
. Podemos incluir un limit
y offset
para paginar los resultados.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *}
Leer Un Hero
Podemos leer un único Hero
.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *}
Eliminar un Hero
También podemos eliminar un Hero
.
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *}
Ejecutar la App
Puedes ejecutar la aplicación:
$ fastapi dev main.py
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Luego dirígete a la interfaz de /docs
, verás que FastAPI está usando estos modelos para documentar la API, y los usará para serializar y validar los datos también.

Actualizar la App con Múltiples Modelos
Ahora vamos a refactorizar un poco esta aplicación para aumentar la seguridad y la versatilidad.
Si revisas la aplicación anterior, en la interfaz verás que, hasta ahora, permite al cliente decidir el id
del Hero
a crear. 😱
No deberíamos permitir que eso suceda, podrían sobrescribir un id
que ya tenemos asignado en la base de datos. Decidir el id
debería ser tarea del backend o la base de datos, no del cliente.
Además, creamos un secret_name
para el héroe, pero hasta ahora, lo estamos devolviendo en todas partes, eso no es muy secreto... 😅
Arreglaremos estas cosas añadiendo unos modelos extra. Aquí es donde SQLModel brillará. ✨
Crear Múltiples Modelos
En SQLModel, cualquier clase de modelo que tenga table=True
es un modelo de tabla.
Y cualquier clase de modelo que no tenga table=True
es un modelo de datos, estos son en realidad solo modelos de Pydantic (con un par de características extra pequeñas). 🤓
Con SQLModel, podemos usar herencia para evitar duplicar todos los campos en todos los casos.
HeroBase
- la clase base
Comencemos con un modelo HeroBase
que tiene todos los campos que son compartidos por todos los modelos:
name
age
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *}
Hero
- el modelo de tabla
Luego, crearemos Hero
, el modelo de tabla real, con los campos extra que no siempre están en los otros modelos:
id
secret_name
Debido a que Hero
hereda de HeroBase
, también tiene los campos declarados en HeroBase
, por lo que todos los campos para Hero
son:
id
name
age
secret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *}
HeroPublic
- el modelo de datos público
A continuación, creamos un modelo HeroPublic
, este es el que será devuelto a los clientes de la API.
Tiene los mismos campos que HeroBase
, por lo que no incluirá secret_name
.
Por fin, la identidad de nuestros héroes está protegida! 🥷
También vuelve a declarar id: int
. Al hacer esto, estamos haciendo un contrato con los clientes de la API, para que siempre puedan esperar que el id
esté allí y sea un int
(nunca será None
).
/// tip | Consejo
Tener el modelo de retorno asegurando que un valor siempre esté disponible y siempre sea int
(no None
) es muy útil para los clientes de la API, pueden escribir código mucho más simple teniendo esta certeza.
Además, los clientes generados automáticamente tendrán interfaces más simples, para que los desarrolladores que se comuniquen con tu API puedan tener una experiencia mucho mejor trabajando con tu API. 😎
///
Todos los campos en HeroPublic
son los mismos que en HeroBase
, con id
declarado como int
(no None
):
id
name
age
secret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *}
HeroCreate
- el modelo de datos para crear un héroe
Ahora creamos un modelo HeroCreate
, este es el que validará los datos de los clientes.
Tiene los mismos campos que HeroBase
, y también tiene secret_name
.
Ahora, cuando los clientes crean un nuevo héroe, enviarán el secret_name
, se almacenará en la base de datos, pero esos nombres secretos no se devolverán en la API a los clientes.
/// tip | Consejo
Esta es la forma en la que manejarías contraseñas. Recíbelas, pero no las devuelvas en la API.
También hashea los valores de las contraseñas antes de almacenarlos, nunca los almacenes en texto plano.
///
Los campos de HeroCreate
son:
name
age
secret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *}
HeroUpdate
- el modelo de datos para actualizar un héroe
No teníamos una forma de actualizar un héroe en la versión anterior de la aplicación, pero ahora con múltiples modelos, podemos hacerlo. 🎉
El modelo de datos HeroUpdate
es algo especial, tiene todos los mismos campos que serían necesarios para crear un nuevo héroe, pero todos los campos son opcionales (todos tienen un valor por defecto). De esta forma, cuando actualices un héroe, puedes enviar solo los campos que deseas actualizar.
Debido a que todos los campos realmente cambian (el tipo ahora incluye None
y ahora tienen un valor por defecto de None
), necesitamos volver a declararlos.
Realmente no necesitamos heredar de HeroBase
porque estamos volviendo a declarar todos los campos. Lo dejaré heredando solo por consistencia, pero esto no es necesario. Es más una cuestión de gusto personal. 🤷
Los campos de HeroUpdate
son:
name
age
secret_name
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *}
Crear con HeroCreate
y devolver un HeroPublic
Ahora que tenemos múltiples modelos, podemos actualizar las partes de la aplicación que los usan.
Recibimos en la request un modelo de datos HeroCreate
, y a partir de él, creamos un modelo de tabla Hero
.
Este nuevo modelo de tabla Hero
tendrá los campos enviados por el cliente, y también tendrá un id
generado por la base de datos.
Luego devolvemos el mismo modelo de tabla Hero
tal cual desde la función. Pero como declaramos el response_model
con el modelo de datos HeroPublic
, FastAPI usará HeroPublic
para validar y serializar los datos.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *}
/// tip | Consejo
Ahora usamos response_model=HeroPublic
en lugar de la anotación de tipo de retorno -> HeroPublic
porque el valor que estamos devolviendo en realidad no es un HeroPublic
.
Si hubiéramos declarado -> HeroPublic
, tu editor y linter se quejarían (con razón) de que estás devolviendo un Hero
en lugar de un HeroPublic
.
Al declararlo en response_model
le estamos diciendo a FastAPI que haga lo suyo, sin interferir con las anotaciones de tipo y la ayuda de tu editor y otras herramientas.
///
Leer Heroes con HeroPublic
Podemos hacer lo mismo que antes para leer Hero
s, nuevamente, usamos response_model=list[HeroPublic]
para asegurar que los datos se validen y serialicen correctamente.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *}
Leer Un Hero con HeroPublic
Podemos leer un único héroe:
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *}
Actualizar un Hero con HeroUpdate
Podemos actualizar un héroe. Para esto usamos una operación HTTP PATCH
.
Y en el código, obtenemos un dict
con todos los datos enviados por el cliente, solo los datos enviados por el cliente, excluyendo cualquier valor que estaría allí solo por ser valores por defecto. Para hacerlo usamos exclude_unset=True
. Este es el truco principal. 🪄
Luego usamos hero_db.sqlmodel_update(hero_data)
para actualizar el hero_db
con los datos de hero_data
.
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *}
Eliminar un Hero de Nuevo
Eliminar un héroe se mantiene prácticamente igual.
No satisfaremos el deseo de refactorizar todo en este punto. 😅
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *}
Ejecutar la App de Nuevo
Puedes ejecutar la aplicación de nuevo:
$ fastapi dev main.py
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Si vas a la interfaz de /docs
de la API, verás que ahora está actualizada, y no esperará recibir el id
del cliente al crear un héroe, etc.

Resumen
Puedes usar SQLModel para interactuar con una base de datos SQL y simplificar el código con modelos de datos y modelos de tablas.
Puedes aprender mucho más en la documentación de SQLModel, hay un mini tutorial sobre el uso de SQLModel con FastAPI. 🚀