Flask-Migrate s'appuie sur Alembic pour gérer les migrations de la base de données gérée par l'ORM (SQLAlchemy). Ainsi le modèle de données définit dans l'application Flask peut être amendé/corrigé, c'est l'extension Flask-Migrate qui se chargera de créer les script de migrations capables de modifier le schéma de la base de données préexistante pour que le nouveau modèle de données soit exploitable sans perte de données.
Pour illustrer le fonctionnement de l'extension Flask-Migrate nous allons utiliser une application Flask minimale en configuration monolithique avec une classe de modèle simple : Product.
Création d'un environnement virtuel Python pour le projet Flask et installation des packages via pip :
#création d'un répertoire dédié à l'application mkdir tuto-migrate && cd tuto-migrate # création /activation de l'environnement virtuel Python python3 -m venv .venv source .venv/bin/activate # Installation de Flask et de l'extension Flask-Migrate pip install Flask Flask-SQLAlchemy Flask-Migrate
Création de l'application Flask dans le module Python app.py
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db" db = SQLAlchemy(app) migrate = Migrate(app, db) class Product(db.Model): __tablename__ = "products" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50)) def __repr__(self): return f"<Product id={self.id} name='{self.name}'>" @app.route("/") def index(): return "<h1>Hello, World!</h1>"
Le module Python app.py définit la classe Product avec une clé primaire id et un champ name.
Pour tester rapidement le code :
# Afficher les routes existantes flask routes # Pour tester la création d'une instance de Product flask shell
>>> mandarine = Product(name='mandarine') >>> mandarine <Product id=None name='mandarine'> >>> quit()
Les commandes proposées par l'extension Flask-Migrate sont regroupées sous flask db. On commence par initialiser l’environnement nécessaire à l'extension puis on génère le premier script de migration :
# Création de l'environnement nécessaire à Flask-Migrate flask db init # Création du premier script flask db migrate -m "Initial DB Model Product"
La commande retourne quelques messages de la forme :
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'products'
Generating /home/username/tuto-migrate/migrations/versions/a09681708867_initial_db_model_product.py ... done
L'outil analyse les modifications apportées au modèle de données et crée un script de migration. Ce script est créé dans le sous dossier ./migrations/versions du projet.
./migrations doit être ajouté à votre outil de gestion de révision comme les autres sources de votre projet.
A ce stade, le script existe mais n'a pas été appliqué. Il est intéressant de le relire :
less migrations/versions/a09681708867_initial_db_model_product.py
On peut y voir deux fonctions : upgrade() et downgrade().
Si on affiche le contenu de la base on peut vérifier qu'elle est encore vide pour le moment :
sqlite3 instance/database.db .tables alembic_version # Une seule table existe nommée alembic_version, elle est vide pour le moment sqlite3 instance/database.db 'SELECT * FROM alembic_version;'
Pour appliquer les modifications décrites dans le script de migration à la base de données du projet :
flask db upgrade
La commande retourne quelques messages du type :
INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> a09681708867, Initial DB Model Product
Si on inspecte à nouveau la base, on peut voir que la table products existe à présent et qu'une entrée existe dans la table alembic_version :
# Lister les tables existantes sqlite3 instance/database.db .tables alembic_version products # Schéma des tables sqlite3 instance/database.db '.schema' CREATE TABLE alembic_version ( version_num VARCHAR(32) NOT NULL, CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num) ); CREATE TABLE products ( id INTEGER NOT NULL, name VARCHAR(50), PRIMARY KEY (id) ); # Afficher le contenu de la table alembic_version sqlite3 instance/database.db 'SELECT * FROM alembic_version;' a09681708867
db.create_all() depuis flask shell.
flask db upgrade retourne une erreur car la base existe déjà. Dans ce cas il faut utiliser la commande flask db stamp pour marquer la base comme déjà migrée.
A chaque modification du modèle de données, il faudra répéter les opérations migrate(alias de revision) et upgrade.
Pour illustrer le bon fonctionnement de Flask-Migrate nous allons :
Pour ajouter des données dans la base, on utilise flask shell :
>>> apple = Product(name="Apple") >>> orange = Product(name="Orange") >>> banana = Product(name="Banana") >>> db.session.add_all([apple, orange, banana]) >>> db.session.commit() >>> >>> # Supprime les instances et affiche les valeurs présentes dans la base >>> del apple,orange,banana >>> Product.query.all() [<Product id=1 name='Apple'>, <Product id=2 name='Orange'>, <Product id=3 name='Banana'>] >>> quit()
On modifie la classe Product en y ajoutant un attribut price
On regénère un script de migration :
flask db migrate -m "Ajout attribut price"
La commande retourne des messages indiquant que des modifications ont été identifiées et qu'un nouveau script est produit :
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'products.price'
Generating /home/username/tuto-migrate/migrations/versions/7d38ed81182c_ajout_attribut_price.py ... done
On peut consulter ce nouveau script puis l'exécuter :
less migrations/versions/7d38ed81182c_ajout_attribut_price.py flask db upgrade
Pour vérifier que le nouveau modèle s'applique sans retourner d'erreurs, on utilise flask shell :
>>> Product.query.all() [<Product id=1 name='Apple' price=None>, <Product id=2 name='Orange' price=None>, <Product id=3 name='Banana' price=None>] >>> quit()
Même si aucune valeur n'est renseignée, l'attribut price existe bien.
Version actuelle de la base :
sqlite3 instance/database.db 'SELECT * FROM alembic_version;' 7d38ed81182c sqlite3 instance/database.db '.schema products' CREATE TABLE products ( id INTEGER NOT NULL, name VARCHAR(50), price INTEGER, PRIMARY KEY (id) );
Pour rétrograder la base à la version précédente :
flask db dowgrade
Etat de la base :
sqlite3 instance/database.db 'SELECT * FROM alembic_version;' a09681708867 sqlite3 instance/database.db '.schema products' CREATE TABLE IF NOT EXISTS "products" ( id INTEGER NOT NULL, name VARCHAR(50), PRIMARY KEY (id) );
On peut voir que la version a bien été modifiée et que la table ne comporte plus de colonne price.
Mais notre modèle de données n'est plus en adéquation avec le schéma de la base de données : son utilisation provoquera une erreur :
>>> # Depuis flask shell >>> ananas = Product(name="Ananas") >>> db.session.add(ananas) >>> >>> # La sauvegarde en base provoquera une erreur >>> db.session.commit()
Le commit retournera un ensemble de messages de la forme :
Traceback (most recent call last):
File "/home/username/tuto-migrate/.venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
self.dialect.do_execute(
File "/home/username/tuto-migrate/.venv/lib/python3.12/site-packages/sqlalchemy/engine/default.py", line 951, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: table products has no column named price
...
Il faudra retirer l'attribut price de la classe Product pour que les écritures en base puisse se faire normalement.
Le groupe de commandes par défaut de Flask-Migrate est nommé “db”. Il peut être renommé lors de l'instanciation de l'extension dans l'application Flask s'il ne convient pas au projet :
migrate = Migrate(app, db, command='db-tools')
Au sein du projet, on peut à présent appeler les commandes de Flask-Migrate :
flask db-tools show
flask db-tools --help
On a créé une application Flask minimale et utilisé l'extension Flask-Migrate pour géré les migrations de la base de données après avoir modifier les classes de modèle de données.
En général on peut suivre les étapes suivantes lors du developpement d'une application Flask :
flask db migrate ;flask db upgrade ;La documentation complète de l'extension Flask-Migrate est fournie dans les références.