Una guida completa alla gestione delle sessioni SQLAlchemy in Python, focalizzata su tecniche robuste di gestione delle transazioni per garantire l'integrità e la coerenza dei dati nelle tue applicazioni.
Gestione delle Sessioni SQLAlchemy in Python: Padroneggiare la Gestione delle Transazioni per l'Integrità dei Dati
SQLAlchemy è una libreria Python potente e flessibile che fornisce un kit di strumenti completo per interagire con i database. Al centro di SQLAlchemy c'è il concetto di sessione, che funge da zona di staging per tutte le operazioni che esegui sul tuo database. Una corretta gestione delle sessioni e delle transazioni è fondamentale per mantenere l'integrità dei dati e garantire un comportamento coerente del database, soprattutto in applicazioni complesse che gestiscono richieste simultanee.
Comprendere le Sessioni SQLAlchemy
Una Sessione SQLAlchemy rappresenta un'unità di lavoro, una conversazione con il database. Tiene traccia delle modifiche apportate agli oggetti, consentendoti di persisterle nel database come singola operazione atomica. Pensala come un'area di lavoro in cui apporti modifiche ai dati prima di salvarli ufficialmente. Senza una sessione ben gestita, rischi incoerenze dei dati e potenziale corruzione.
Creazione di una Sessione
Prima di poter iniziare a interagire con il tuo database, devi creare una sessione. Ciò implica innanzitutto stabilire una connessione al database utilizzando il motore di SQLAlchemy.
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
# Stringa di connessione al database
db_url = 'sqlite:///:memory:' # Sostituisci con l'URL del tuo database (es. PostgreSQL, MySQL)
# Crea un motore
engine = create_engine(db_url, echo=False) # echo=True per vedere l'SQL generato
# Definisci una base per i modelli dichiarativi
Base = declarative_base()
# Definisci un modello semplice
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
def __repr__(self):
return f""
# Crea la tabella nel database
Base.metadata.create_all(engine)
# Crea una classe di sessione
Session = sessionmaker(bind=engine)
# Istanzia una sessione
session = Session()
In questo esempio:
- Importiamo i moduli SQLAlchemy necessari.
- Definiamo una stringa di connessione al database (`db_url`). Questo esempio utilizza un database SQLite in-memory per semplicità, ma dovresti sostituirlo con una stringa di connessione appropriata per il tuo sistema di database (ad esempio, PostgreSQL, MySQL). Il formato specifico varia in base al motore del database e al driver che stai utilizzando. Consulta la documentazione di SQLAlchemy e la documentazione del tuo fornitore di database per il formato corretto della stringa di connessione.
- Creiamo un `engine` usando `create_engine()`. Il motore è responsabile della gestione del pool di connessioni e della comunicazione con il database. Il parametro `echo=True` può essere utile per il debug, poiché stamperà le istruzioni SQL generate sulla console.
- Definiamo una classe base (`Base`) usando `declarative_base()`. Questa viene utilizzata come classe base per tutti i nostri modelli SQLAlchemy.
- Definiamo un modello `User`, mappandolo a una tabella del database denominata `users`.
- Creiamo la tabella nel database usando `Base.metadata.create_all(engine)`.
- Creiamo una classe di sessione usando `sessionmaker(bind=engine)`. Questo configura la classe di sessione per utilizzare il motore specificato.
- Infine, istanziamo una sessione usando `Session()`.
Comprendere le Transazioni
Una transazione è una sequenza di operazioni del database trattate come una singola unità logica di lavoro. Le transazioni aderiscono alle proprietà ACID:
- Atomicità: Tutte le operazioni nella transazione hanno successo completamente o falliscono completamente. Se una parte della transazione fallisce, l'intera transazione viene ripristinata.
- Consistenza: La transazione deve mantenere il database in uno stato valido. Non può violare alcun vincolo o regola del database.
- Isolamento: Le transazioni simultanee sono isolate l'una dall'altra. Le modifiche apportate da una transazione non sono visibili ad altre transazioni fino a quando la prima transazione non viene eseguita.
- Durabilità: Una volta che una transazione è stata eseguita, le sue modifiche sono permanenti e sopravviveranno anche ai guasti del sistema.
SQLAlchemy fornisce meccanismi per gestire le transazioni, garantendo il mantenimento di queste proprietà ACID.
Gestione delle Transazioni di Base
Le operazioni di transazione più comuni sono commit e rollback.
Esecuzione delle Transazioni
Quando tutte le operazioni all'interno di una transazione sono state completate con successo, si esegue la transazione. Questo persiste le modifiche nel database.
try:
# Aggiungi un nuovo utente
new_user = User(name='Alice Smith', email='alice.smith@example.com')
session.add(new_user)
# Esegui la transazione
session.commit()
print("Transazione eseguita con successo!")
except Exception as e:
# Gestisci le eccezioni
print(f"Si è verificato un errore: {e}")
session.rollback()
print("Transazione ripristinata.")
finally:
session.close()
In questo esempio:
- Aggiungiamo un nuovo oggetto `User` alla sessione.
- Chiamiamo `session.commit()` per persistere le modifiche nel database.
- Incapsuliamo il codice in un blocco `try...except...finally` per gestire le potenziali eccezioni.
- Se si verifica un'eccezione, chiamiamo `session.rollback()` per annullare le modifiche apportate durante la transazione.
- Chiamiamo sempre `session.close()` nel blocco `finally` per rilasciare la sessione e restituire la connessione al pool di connessioni. Questo è fondamentale per evitare perdite di risorse. La mancata chiusura delle sessioni può portare all'esaurimento delle connessioni e all'instabilità dell'applicazione.
Ripristino delle Transazioni
Se si verifica un errore durante una transazione o se si decide che le modifiche non devono essere persistite, si ripristina la transazione. Questo ripristina il database allo stato precedente all'inizio della transazione.
try:
# Aggiungi un utente con un'e-mail non valida (esempio per forzare un rollback)
invalid_user = User(name='Bob Johnson', email='invalid-email')
session.add(invalid_user)
# Il commit fallirà se l'e-mail non viene convalidata a livello di database
session.commit()
print("Transazione eseguita.")
except Exception as e:
print(f"Si è verificato un errore: {e}")
session.rollback()
print("Transazione ripristinata con successo.")
finally:
session.close()
In questo esempio, se l'aggiunta di `invalid_user` genera un'eccezione (ad esempio, a causa di una violazione del vincolo del database), la chiamata a `session.rollback()` annullerà l'inserimento tentato, lasciando il database invariato.
Gestione Avanzata delle Transazioni
Utilizzo dell'istruzione `with` per l'ambito delle transazioni
Un modo più Pythonico e robusto per gestire le transazioni è utilizzare l'istruzione `with`. Ciò garantisce che la sessione venga chiusa correttamente, anche se si verificano eccezioni.
from contextlib import contextmanager
@contextmanager
def session_scope():
"""Fornisci un ambito transazionale attorno a una serie di operazioni."""
session = Session()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
# Utilizzo:
with session_scope() as session:
new_user = User(name='Charlie Brown', email='charlie.brown@example.com')
session.add(new_user)
# Operazioni all'interno del blocco 'with'
# Se non si verificano eccezioni, la transazione viene eseguita automaticamente.
# Se si verifica un'eccezione, la transazione viene ripristinata automaticamente.
print("Utente aggiunto.")
print("Transazione completata (eseguita o ripristinata).")
La funzione `session_scope` è un context manager. Quando si entra nel blocco `with`, viene creata una nuova sessione. Quando si esce dal blocco `with`, la sessione viene eseguita (se non si sono verificate eccezioni) o ripristinata (se si è verificata un'eccezione). La sessione viene sempre chiusa nel blocco `finally`.
Transazioni annidate (punti di salvataggio)
SQLAlchemy supporta le transazioni annidate usando i punti di salvataggio. Un punto di salvataggio ti consente di ripristinare un punto specifico all'interno di una transazione più ampia, senza influire sull'intera transazione.
try:
with session_scope() as session:
user1 = User(name='David Lee', email='david.lee@example.com')
session.add(user1)
session.flush() # Invia le modifiche al database ma non eseguire ancora
# Crea un punto di salvataggio
savepoint = session.begin_nested()
try:
user2 = User(name='Eve Wilson', email='eve.wilson@example.com')
session.add(user2)
session.flush()
# Simula un errore
raise ValueError("Errore simulato durante la transazione annidata")
except Exception as e:
print(f"Errore di transazione annidata: {e}")
savepoint.rollback()
print("Transazione annidata ripristinata al punto di salvataggio.")
# Continua con la transazione esterna, user1 verrà comunque aggiunto
user3 = User(name='Frank Miller', email='frank.miller@example.com')
session.add(user3)
except Exception as e:
print(f"Errore di transazione esterna: {e}")
#Commit eseguirà user1 e user3, ma non user2 a causa del rollback annidato
try:
with session_scope() as session:
#Verifica che esistano solo user1 e user3
users = session.query(User).all()
for user in users:
print(user)
except Exception as e:
print(f"Eccezione inaspettata: {e}") #Non dovrebbe succedere
In questo esempio:
- Iniziamo una transazione esterna usando `session_scope()`.
- Aggiungiamo `user1` alla sessione e svuotiamo le modifiche nel database. `flush()` invia le modifiche al server del database ma *non* le esegue. Ti consente di vedere se le modifiche sono valide (ad esempio, nessuna violazione dei vincoli) prima di eseguire l'intera transazione.
- Creiamo un punto di salvataggio usando `session.begin_nested()`.
- All'interno della transazione annidata, aggiungiamo `user2` e simuliamo un errore.
- Ripristiniamo la transazione annidata al punto di salvataggio usando `savepoint.rollback()`. Questo annulla solo le modifiche apportate all'interno della transazione annidata (cioè, l'aggiunta di `user2`).
- Continuiamo con la transazione esterna e aggiungiamo `user3`.
- La transazione esterna viene eseguita, persistendo `user1` e `user3` nel database, mentre `user2` viene scartato a causa del rollback del punto di salvataggio.
Controllo dei livelli di isolamento
I livelli di isolamento definiscono il grado in cui le transazioni simultanee sono isolate l'una dall'altra. Livelli di isolamento più alti forniscono maggiore coerenza dei dati, ma possono ridurre la concorrenza e le prestazioni. SQLAlchemy ti consente di controllare il livello di isolamento delle tue transazioni.
I livelli di isolamento comuni includono:
- Lettura non confermata: Il livello di isolamento più basso. Le transazioni possono vedere le modifiche non confermate apportate da altre transazioni. Questo può portare a letture sporche.
- Lettura confermata: Le transazioni possono vedere solo le modifiche confermate apportate da altre transazioni. Questo impedisce letture sporche, ma può portare a letture non ripetibili e letture fantasma.
- Lettura ripetibile: Le transazioni possono vedere gli stessi dati durante la transazione, anche se altre transazioni li modificano. Questo impedisce letture sporche e letture non ripetibili, ma può portare a letture fantasma.
- Serializzabile: Il livello di isolamento più alto. Le transazioni sono completamente isolate l'una dall'altra. Questo impedisce letture sporche, letture non ripetibili e letture fantasma, ma può ridurre significativamente la concorrenza.
Il livello di isolamento predefinito dipende dal sistema di database. È possibile impostare il livello di isolamento quando si crea il motore o quando si inizia una transazione.
Esempio (PostgreSQL):
from sqlalchemy.dialects.postgresql import dialect
# Imposta il livello di isolamento quando si crea il motore
engine = create_engine('postgresql://user:password@host:port/database',
connect_args={'options': '-c statement_timeout=1000'} #Esempio di timeout
)
# Imposta il livello di isolamento quando si inizia una transazione (specifico del database)
# Per postgresql, si consiglia di impostarlo sulla connessione, non sul motore.
from sqlalchemy import event
from sqlalchemy.pool import Pool
@event.listens_for(Pool, "connect")
def set_isolation_level(dbapi_connection, connection_record):
existing_autocommit = dbapi_connection.autocommit
dbapi_connection.autocommit = True
cursor = dbapi_connection.cursor()
cursor.execute("SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE")
dbapi_connection.autocommit = existing_autocommit
cursor.close()
# Quindi le transazioni create tramite SQLAlchemy utilizzeranno il livello di isolamento configurato.
Importante: Il metodo per l'impostazione dei livelli di isolamento è specifico del database. Fare riferimento alla documentazione del database per la sintassi corretta. L'impostazione errata dei livelli di isolamento può portare a comportamenti o errori imprevisti.
Gestione della Concorrenza
Quando più utenti o processi accedono agli stessi dati contemporaneamente, è fondamentale gestire correttamente la concorrenza per prevenire la corruzione dei dati e garantire la coerenza dei dati. SQLAlchemy fornisce diversi meccanismi per la gestione della concorrenza, tra cui il blocco ottimistico e il blocco pessimistico.
Blocco ottimistico
Il blocco ottimistico presuppone che i conflitti siano rari. Controlla le modifiche apportate da altre transazioni prima di eseguire una transazione. Se viene rilevato un conflitto, la transazione viene ripristinata.
Per implementare il blocco ottimistico, in genere aggiungi una colonna versione alla tua tabella. Questa colonna viene incrementata automaticamente ogni volta che la riga viene aggiornata.
from sqlalchemy import Column, Integer, String, Integer
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
content = Column(String)
version = Column(Integer, nullable=False, default=1)
def __repr__(self):
return f""
#All'interno del blocco try catch
def update_article(session, article_id, new_content):
article = session.query(Article).filter_by(id=article_id).first()
if article is None:
raise ValueError("Articolo non trovato")
original_version = article.version
# Aggiorna il contenuto e incrementa la versione
article.content = new_content
article.version += 1
# Tentativo di aggiornamento, controllando la colonna versione nella clausola WHERE
rows_affected = session.query(Article).filter(
Article.id == article_id,
Article.version == original_version
).update({
Article.content: new_content,
Article.version: article.version
}, synchronize_session=False)
if rows_affected == 0:
session.rollback()
raise ValueError("Conflitto: l'articolo è stato aggiornato da un'altra transazione.")
session.commit()
In questo esempio:
- Aggiungiamo una colonna `version` al modello `Article`.
- Prima di aggiornare l'articolo, memorizziamo il numero di versione corrente.
- Nell'istruzione `UPDATE`, includiamo una clausola `WHERE` che controlla se la colonna versione è ancora uguale al numero di versione memorizzato. `synchronize_session=False` impedisce a SQLAlchemy di caricare di nuovo l'oggetto aggiornato; stiamo gestendo esplicitamente il versioning.
- Se la colonna versione è stata modificata da un'altra transazione, l'istruzione `UPDATE` non influirà su alcuna riga (rows_affected sarà 0) e genereremo un'eccezione.
- Ripristiniamo la transazione e informiamo l'utente che si è verificato un conflitto.
Blocco pessimistico
Il blocco pessimistico presuppone che i conflitti siano probabili. Acquisisce un blocco su una riga o una tabella prima di modificarla. Questo impedisce ad altre transazioni di modificare i dati fino a quando il blocco non viene rilasciato.
SQLAlchemy fornisce diverse funzioni per l'acquisizione di blocchi, come `with_for_update()`.
# Esempio usando PostgreSQL
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
# Impostazione del database (sostituire con l'URL del tuo database effettivo)
db_url = 'postgresql://user:password@host:port/database'
engine = create_engine(db_url, echo=False) #Imposta echo su true se desideri vedere l'SQL generato
Base = declarative_base()
class Item(Base):
__tablename__ = 'items'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(Integer)
def __repr__(self):
return f"- "
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
#Funzione per aggiornare l'elemento (all'interno di un try/except)
def update_item_value(session, item_id, new_value):
# Acquisisci un blocco pessimistico sull'elemento
item = session.query(Item).filter(Item.id == item_id).with_for_update().first()
if item is None:
raise ValueError("Elemento non trovato")
# Aggiorna il valore dell'elemento
item.value = new_value
session.commit()
return True
In questo esempio:
- Usiamo `with_for_update()` per acquisire un blocco sulla riga `Item` prima di aggiornarla. Questo impedisce ad altre transazioni di modificare la riga fino a quando la transazione corrente non viene eseguita o ripristinata. La funzione `with_for_update()` è specifica del database; consulta la documentazione del database per i dettagli. Alcuni database potrebbero avere diversi meccanismi di blocco o sintassi.
Importante: Il blocco pessimistico può ridurre la concorrenza e le prestazioni, quindi usalo solo quando necessario.
Best practice per la gestione delle eccezioni
Una corretta gestione delle eccezioni è fondamentale per garantire l'integrità dei dati ed evitare arresti anomali dell'applicazione. Incapsula sempre le operazioni del database nei blocchi `try...except` e gestisci le eccezioni in modo appropriato.
Ecco alcune best practice per la gestione delle eccezioni:
- Intercetta eccezioni specifiche: Evita di intercettare eccezioni generiche come `Exception`. Intercetta eccezioni specifiche come `sqlalchemy.exc.IntegrityError` o `sqlalchemy.exc.OperationalError` per gestire i diversi tipi di errori in modo diverso.
- Ripristina le transazioni: Ripristina sempre la transazione se si verifica un'eccezione.
- Registra le eccezioni: Registra le eccezioni per aiutare a diagnosticare e risolvere i problemi. Includi quanto più contesto possibile nei tuoi registri (ad esempio, l'ID utente, i dati di input, il timestamp).
- Rilancia le eccezioni quando appropriato: Se non puoi gestire un'eccezione, rilanciala per consentire a un gestore di livello superiore di gestirla.
- Pulisci le risorse: Chiudi sempre la sessione e rilascia tutte le altre risorse in un blocco `finally`.
import logging
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.exc import IntegrityError, OperationalError
# Configura la registrazione
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Impostazione del database (sostituire con l'URL del tuo database effettivo)
db_url = 'postgresql://user:password@host:port/database'
engine = create_engine(db_url, echo=False)
Base = declarative_base()
class Product(Base):
__tablename__ = 'products'
id = Column(Integer, primary_key=True)
name = Column(String)
price = Column(Integer)
def __repr__(self):
return f""
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
# Funzione per aggiungere un prodotto
def add_product(session, name, price):
try:
new_product = Product(name=name, price=price)
session.add(new_product)
session.commit()
logging.info(f"Prodotto '{name}' aggiunto con successo.")
return True
except IntegrityError as e:
session.rollback()
logging.error(f"IntegrityError: {e}")
#Gestisci le violazioni dei vincoli del database (ad esempio, nome duplicato)
return False
except OperationalError as e:
session.rollback()
logging.error(f"OperationalError: {e}")
#Gestisci gli errori di connessione o altri problemi operativi
return False
except Exception as e:
session.rollback()
logging.exception(f"Si è verificato un errore imprevisto: {e}")
# Gestisci eventuali altri errori imprevisti
return False
finally:
session.close()
In questo esempio:
- Configuriamo la registrazione per registrare gli eventi durante il processo.
- Intercettiamo eccezioni specifiche come `IntegrityError` (per violazioni dei vincoli) e `OperationalError` (per errori di connessione).
- Ripristiniamo la transazione nei blocchi `except`.
- Registriamo le eccezioni usando il modulo `logging`. Il metodo `logging.exception()` include automaticamente lo stack trace nel messaggio di registro.
- Rilanciamo l'eccezione se non possiamo gestirla.
- Chiudiamo la sessione nel blocco `finally`.
Pooling delle Connessioni al Database
SQLAlchemy utilizza il pooling delle connessioni per gestire in modo efficiente le connessioni al database. Un pool di connessioni mantiene un insieme di connessioni aperte al database, consentendo alle applicazioni di riutilizzare le connessioni esistenti invece di crearne di nuove per ogni richiesta. Ciò può migliorare significativamente le prestazioni, in particolare nelle applicazioni che gestiscono un gran numero di richieste simultanee.
La funzione `create_engine()` di SQLAlchemy crea automaticamente un pool di connessioni. Puoi configurare il pool di connessioni passando argomenti a `create_engine()`.
I parametri comuni del pool di connessioni includono:
- pool_size: Il numero massimo di connessioni nel pool.
- max_overflow: Il numero di connessioni che possono essere create oltre pool_size.
- pool_recycle: Il numero di secondi dopo i quali una connessione viene riciclata.
- pool_timeout: Il numero di secondi da attendere affinché una connessione diventi disponibile.
engine = create_engine('postgresql://user:password@host:port/database',
pool_size=5, #Dimensione massima del pool
max_overflow=10, #Overflow massimo
pool_recycle=3600, #Ricicla le connessioni dopo 1 ora
pool_timeout=30
)
Importante: Scegli le impostazioni appropriate del pool di connessioni in base alle esigenze della tua applicazione e alle capacità del tuo server di database. Un pool di connessioni configurato in modo errato può causare problemi di prestazioni o l'esaurimento delle connessioni.
Transazioni asincrone (Async SQLAlchemy)
Per le applicazioni moderne che richiedono un'elevata concorrenza, in particolare quelle create con framework asincroni come FastAPI o AsyncIO, SQLAlchemy offre una versione asincrona chiamata Async SQLAlchemy.
Async SQLAlchemy fornisce versioni asincrone dei componenti principali di SQLAlchemy, consentendoti di eseguire operazioni sul database senza bloccare il ciclo degli eventi. Ciò può migliorare significativamente le prestazioni e la scalabilità delle tue applicazioni.
Ecco un esempio di base di utilizzo di Async SQLAlchemy:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, Integer, String
import asyncio
# Impostazione del database (sostituire con l'URL del tuo database effettivo)
db_url = 'postgresql+asyncpg://user:password@host:port/database'
engine = create_async_engine(db_url, echo=False)
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
def __repr__(self):
return f""
async def create_db_and_tables():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def add_user(name, email):
async with AsyncSession(engine) as session:
new_user = User(name=name, email=email)
session.add(new_user)
await session.commit()
async def main():
await create_db_and_tables()
await add_user("Async User", "async.user@example.com")
if __name__ == "__main__":
asyncio.run(main())
Differenze chiave da SQLAlchemy sincrono:
- `create_async_engine` viene utilizzato al posto di `create_engine`.
- `AsyncSession` viene utilizzato al posto di `Session`.
- Tutte le operazioni del database sono asincrone e devono essere attese usando `await`.
- È necessario utilizzare driver di database asincroni (ad esempio, `asyncpg` per PostgreSQL).
Importante: Async SQLAlchemy richiede un driver di database che supporti le operazioni asincrone. Assicurati di avere il driver corretto installato e configurato.
Conclusione
Padroneggiare la gestione delle sessioni e delle transazioni SQLAlchemy è essenziale per creare applicazioni Python robuste e affidabili che interagiscono con i database. Comprendendo i concetti di sessioni, transazioni, livelli di isolamento e concorrenza e seguendo le best practice per la gestione delle eccezioni e il pooling delle connessioni, puoi garantire l'integrità dei dati e ottimizzare le prestazioni delle tue applicazioni.
Che tu stia creando una piccola applicazione Web o un sistema aziendale su larga scala, SQLAlchemy fornisce gli strumenti necessari per gestire le tue interazioni con il database in modo efficace. Ricorda di dare sempre la priorità all'integrità dei dati e di gestire con grazia i potenziali errori per garantire l'affidabilità delle tue applicazioni.
Considera di esplorare argomenti avanzati come:
- Two-Phase Commit (2PC): Per le transazioni che si estendono su più database.
- Sharding: Per la distribuzione dei dati su più server di database.
- Migrazioni del database: Utilizzo di strumenti come Alembic per gestire le modifiche dello schema del database.