La guida completa (tutorial) all’utilizzo dei database relazionali con FastAPI
Introduzione
Lo scopo di questo articolo è creare una semplice guida su come utilizzare FastAPI con i database relazionali e utilizzare Alembic per le migrazioni. Un’implementazione che può essere utilizzata in produzione.
Installazione
Useremo pipenv
per gestire sia i miei pacchetti che l’ambiente virtuale. Sentiti libero di gestire i tuoi pacchetti come preferisci.
Pacchetti Utilizzati
- python ≥ 3.5
- fastapi
- pydantic
- fastapi-sqlalchemy
- alembic
- psycopg2
- uvicorn
Creiamo una nuova directory (puoi chiamarla come vuoi).
Ad esempio possiamo chiamarla fastapi_sqlalchemy_alembic
Apri il terminale e scrivi
cd fastapi_sqlalchemy_alembic
pipenv install --three fastapi fastapi-sqlalchemy pydantic alembic psycopg2 uvicorn
Utilizzerò docker compose per gestire il database, potresti ricevere alcuni errori relativi all’installazione di psycopg2 se stai utilizzando Mac-OS, ma dal momento che utilizzeremo Docker, questo non è molto importante.
Main.py
Iniziamo con un semplice file principale per Fastapi
# main.py
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.post("/user/", response_model=User)
def create_user(user: User):
return user
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Configurazione di Docker
# Dockerfile
# Pull base image
FROM python:3.7
# Set environment varibles
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
WORKDIR /code/
# Install dependencies
RUN pip install pipenv
COPY Pipfile Pipfile.lock /code/
RUN pipenv install --system --dev
COPY . /code/
EXPOSE 8000
# docker-compose.yml
version: "3"
services:
db:
image: postgres:11
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=test_db
web:
build: .
command: bash -c "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- db
pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
environment:
- [email protected]
- PGADMIN_DEFAULT_PASSWORD=admin
ports:
- "5050:80"
depends_on:
- db
La configurazione precedente creerà un cluster con 3 contenitori:
- contenitore web — dove verrà eseguito il codice effettivo
- contenitore db
- contenitore pgadmin
Nella tua directory corrente dovresti vedere 5 file:
- file pip
- Pipfile.lock
- Dockerfile
- docker-compose.yml
- main.py
Quindi costruiamo il cluster docker, eseguendo il seguente comando nel terminale:
docker-compose build
Alembic
Si inizializza Alembic eseguendo il seguente cmd nel terminale della stessa directory:
alembic init alembic
In questo modo si crea una directory chiamata alembic
e un file di configurazione alembic.ini
Il prossimo passo è aprire il file alembic.ini
con il tuo editor e modificare la riga 38 da:
sqlalchemy.url = driver://user:pass@localhost/dbname
a:
sqlalchemy.url =
e quindi aggiungere l’url db postgres nel file alembic/env.py.
Dato che stiamo creando una configurazione che dovrebbe funzionare in produzione, non possiamo scrivere in chiaro il nome utente e password del database all’interno del file alembic.ini
. Dobbiamo invece leggerlo dalle variabili d’ambiente tramite lo script alembic/env.py.
Installiamo python-dotenv
pipenv install python-dotenv
Dal momento che abbiamo aggiunto un nuovo pacchetto, ricostruiamo il docker per includerlo:
docker-compose build
Creiamo un .envfile
e aggiungiamo quanto segue:
DATABASE_URL = postgresql+psycopg2://postgres:postgres@db:5432
Come abbiamo scoperto l’URL del database?
DATABASE_URL = postgresql+psycopg2://{utente}:{password}@{host}:{porta}
se controlliamo la configurazione docker-compose.yml per il database:
...
db:
image: postgres:11
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=test_db
...
Vediamo come user=postgres, password=postgres e poiché siamo nel mondo docker, l’host del database non sarà localhost ma il nome del contenitore, nel nostro caso lo abbiamo chiamato db.
Quindi aggiungiamo questa riga al nostro .env:
DATABASE_URL = postgresql+psycopg2://postgres:postgres@db:5432
Apriamo alembic\env.py
, che dovrebbe apparire come segue:
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
Dobbiamo quindi apportare le seguenti modifiche:
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import poolfrom alembic import context
# ---------------- added code here -------------------------#
import os, sys
from dotenv import load_dotenv
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
load_dotenv(os.path.join(BASE_DIR, ".env"))
sys.path.append(BASE_DIR)
#------------------------------------------------------------#
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# ---------------- added code here -------------------------#
# this will overwrite the ini-file sqlalchemy.url path
# with the path given in the config of the main code
config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])
#------------------------------------------------------------#
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# ---------------- added code here -------------------------#
import models
#------------------------------------------------------------#
# ---------------- changed code here -------------------------#
# here target_metadata was equal to None
target_metadata = models.Base.metadata
#------------------------------------------------------------#
Modelli
Ora creiamo i nostri modelli da migrare a PostgreSQL:
# models.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
first_name = Column(String,)
last_name = Column(String)
age = Column(Integer)
Quindi è necessario generare la revisione per la nostra prima migrazione:
docker-compose run web alembic revision --autogenerate -m "First migration"
Se il comando è stato eseguito correttamente dovresti vedere un nuovo file generato nella directory “versions”:
Infine possiamo eseguire la migrazione:
docker-compose run web alembic upgrade head
Pgadmin
Per controllare le migrazioni create è sufficiente eseguire nel terminale il seguente comando:
docker-compose up
ed aspettare un po’, ci vuole un po’ di tempo per caricare.
A caricamento concluso, si apre un browser all’indirizzo localhost:5050
e si può accedere con [email protected]
e password=admin
, come da impostazione definita nel nostro docker-compose.yml
...
pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
environment:
- [email protected]
- PGADMIN_DEFAULT_PASSWORD=admin
ports:
- "5050:80"
depends_on:
- db
...
Una volta entrati, è necessario creare una nuova connessione. Alla voce “General” si deve scegliere un nome per la connessione.
Alla voce “Connection” bisogna inserire i riferimenti e le credenziali per connettersi al database (la password è postgres
)
Navigamio fino a trovare la nostra tabella User.
Servers > {your-connection-name} > Databases > postgres > Schemas > public > Tables > users
Ora possiamo sicuramente dire che la nostra migrazione si è conclusa con successo.
Finore abbiamo implementato completamente l’ORM con le migrazioni Alembic. Il prossimo passo è collegarlo allo schema Pydantic.
Schema — Modello Pydantic
# schema.py
from pydantic import BaseModel
class User(BaseModel):
first_name: str
last_name: str = None
age: int
class Config:
orm_mode = True
Si noti che abbiamo una classe Config
in cui impostiamo orm_mode=True
ed è tutto ciò di cui abbiamo bisogno per i modelli Pydantic, senza i quali gli oggetti del modello Sqlalchemy non verranno serializzati su JSON.
Connettiamo tutto all’interno di main.py
import uvicorn
from fastapi import FastAPI
#--------------- added code ------------------------#
import os
from fastapi_sqlalchemy import DBSessionMiddleware
from fastapi_sqlalchemy import db
from models import User as ModelUser
from schema import User as SchemaUser
from dotenv import load_dotenv
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(BASE_DIR, ".env"))
#---------------------------------------------------#
app = FastAPI()
#--------------- added code ------------------------#
app.add_middleware(DBSessionMiddleware,
db_url=os.environ["DATABASE_URL"])
#---------------------------------------------------#
#--------------- modified code ---------------------#
@app.post("/user/", response_model=SchemaUser)
def create_user(user: SchemaUser):
db_user = ModelUser(first_name=user.first_name,
last_name=user.last_name,
age=user.age
)
db.session.add(db_user)
db.session.commit()
return db_user
#---------------------------------------------------#
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
Fantastico, eseguiamo di nuovo docker-compose up
Ora andiamo a \docs
e invochiamo l’endpoint Create User
Possiamo quindi controllare su pgadmin
se ha funzionato correttamente.
Colleghiamoci a localhost:5050
No comment yet, add your voice below!