Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Flask est un framework web Python léger qui vous donne un contrôle total sur la structure de l’application. Combiné à mssql-python, vous pouvez créer des applications web et des API REST soutenues par Microsoft SQL et Azure SQL Database avec un minimum de surcharge.
Logiciels requis
- Python 3.10 ou version ultérieure.
- Packages
mssql-pythonetflask. Installez les deux avecpip install flask mssql-python. - Installez les prérequis ponctuels spécifiques au système d'exploitation. Les utilisateurs Windows peuvent sauter cette étape. Pour tous les détails sur la plateforme, voir Installer mssql-python.
Créer une base de données SQL
Créer ou connecter une base de données SQL sur l’une des plateformes suivantes :
Les exemples de cet article utilisent la base de données d’exemples AdventureWorksLT , en particulier la SalesLT.Product table. Si vous n’avez pas AdventureWorksLT installé, consultez les bases de données d’exemple AdventureWorks.
Configuration du projet
Installer des dépendances
Installez les packages requis avec pip :
pip install flask mssql-python
Structure du projet
Organisez votre projet avec des modules séparés pour la configuration, la gestion des connexions, les itinéraires et les tests :
my_app/
├── app.py # Flask app and routes
├── config.py # database settings
├── database.py # connection lifecycle
├── test_app.py # pytest tests
└── blueprints/ # optional: routes grouped into modules
├── __init__.py
└── products.py
Gestion des connexions à la base de données
Flask n’inclut pas de couche de base de données intégrée, donc vous gérez directement les connexions. Le motif de cette section stocke une connexion par requête sur l’objet de g Flask et le ferme automatiquement à la fin de la requête.
Créez config.py
Centralisez les paramètres de la base de données dans une classe de configuration. Les variables d’environnement permettent de contourner les paramètres par défaut sans changer de code.
# config.py
import os
class Config:
"""Application configuration."""
DATABASE_SERVER = os.getenv("DB_SERVER", "<server>.database.windows.net")
DATABASE_NAME = os.getenv("DB_NAME", "<database>")
POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "10"))
Créez database.py
Le database.py module gère le cycle de vie de la connexion. L’objet g de Flask est un espace de noms propre à chaque requête ; y stocker la connexion garantit donc que chaque requête dispose de sa propre connexion, correctement libérée à la fin de la requête.
La get_connection_string() fonction construit la chaîne de connexion à partir de la configuration de l’application. La get_db() fonction crée une connexion dès le premier appel et la réutilise pour le reste de la requête. La fonction close_db() s’exécute automatiquement à la fin de chaque requête, annule la transaction en cas d’exception et la valide dans le cas contraire. La init_app() fonction détecte ce comportement de démontage avec l’application Flask.
# database.py
import mssql_python
from flask import g, current_app
def get_connection_string() -> str:
"""Build connection string from Flask app config."""
cfg = current_app.config
return (
f"Server={cfg['DATABASE_SERVER']};"
f"Database={cfg['DATABASE_NAME']};"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes"
)
def get_db():
"""Get a database cursor for the current request.
The connection is stored on Flask's g object so it persists
for the duration of the request and is reused across calls.
"""
if "db_conn" not in g:
g.db_conn = mssql_python.connect(get_connection_string())
g.db_cursor = g.db_conn.cursor()
return g.db_cursor
def close_db(exception=None):
"""Close the database connection at the end of the request."""
cursor = g.pop("db_cursor", None)
conn = g.pop("db_conn", None)
if cursor is not None:
cursor.close()
if conn is not None:
if exception:
conn.rollback()
else:
conn.commit()
conn.close()
def init_app(app):
"""Register database teardown with the Flask app."""
app.teardown_appcontext(close_db)
Note
ActiveDirectoryDefault utilise DefaultAzureCredential, qui tente successivement d’utiliser plusieurs fournisseurs d’informations d’identification. La première connexion peut être lente car le SDK parcourt la chaîne jusqu’à ce qu’il trouve un fournisseur fonctionnel. En production, si vous savez quel type d’identifiant votre environnement utilise, spécifiez-le directement (par exemple, ActiveDirectoryMSI pour l’identité gérée) afin d’éviter la marche en chaîne. Pour plus d’informations, consultez Authentification Microsoft Entra.
Application Flask
L’exemple suivant montre une application Flask complète avec des itinéraires pour lister, récupérer, créer, mettre à jour et supprimer des produits.
Créez app.py
Le module application crée l’application Flask, charge la configuration et enregistre le démantèlement de la base de données. Chaque fonction de routage appelle get_db() pour obtenir un curseur, exécute des requêtes avec du SQL paramétré (en utilisant les espaces réservés %(name)s et un dictionnaire de valeurs), et renvoie des réponses JSON.
# app.py
from flask import Flask, jsonify, request, abort
from config import Config
from database import init_app, get_db
app = Flask(__name__)
app.config.from_object(Config)
init_app(app)
@app.route("/")
def index():
return jsonify({"message": "Product API", "docs": "/products"})
@app.route("/products")
def list_products():
"""List products with pagination."""
page = request.args.get("page", 1, type=int)
page_size = request.args.get("page_size", 10, type=int)
skip = (page - 1) * page_size
cursor = get_db()
cursor.execute("SELECT COUNT(*) FROM SalesLT.Product")
total = cursor.fetchval()
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, ProductCategoryID
FROM SalesLT.Product
ORDER BY ProductID
OFFSET %(skip)s ROWS
FETCH NEXT %(limit)s ROWS ONLY
""", {"skip": skip, "limit": page_size})
items = [{
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
} for row in cursor.fetchall()]
return jsonify({
"items": items,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size
})
@app.route("/products/<int:product_id>")
def get_product(product_id):
"""Get a single product by ID."""
cursor = get_db()
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, ProductCategoryID
FROM SalesLT.Product
WHERE ProductID = %(id)s
""", {"id": product_id})
row = cursor.fetchone()
if not row:
abort(404)
return jsonify({
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
})
@app.route("/products", methods=["POST"])
def create_product():
"""Create a new product."""
data = request.get_json()
if not data:
abort(400)
cursor = get_db()
# OUTPUT INSERTED returns the new row's columns in the same statement,
# so you don't need a separate SELECT to get the generated ID and defaults.
# ProductNumber is required and unique. StandardCost and SellStartDate are
# also NOT NULL in SalesLT.Product, so supply values for them.
cursor.execute("""
INSERT INTO SalesLT.Product
(Name, ProductNumber, ListPrice, Color, Size, ProductCategoryID, StandardCost, SellStartDate)
OUTPUT INSERTED.ProductID, INSERTED.Name, INSERTED.ProductNumber, INSERTED.ListPrice,
INSERTED.Color, INSERTED.ProductCategoryID
VALUES (%(name)s, %(product_number)s, %(price)s, %(color)s, %(size)s, %(category_id)s, 0, GETDATE())
""", {
"name": data["name"],
"product_number": data["product_number"],
"price": data["price"],
"color": data.get("color"),
"size": data.get("size"),
"category_id": data["category_id"]
})
row = cursor.fetchone()
return jsonify({
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
}), 201
@app.route("/products/<int:product_id>", methods=["PUT"])
def update_product(product_id):
"""Update an existing product."""
data = request.get_json()
if not data:
abort(400)
cursor = get_db()
updates = []
params = {"id": product_id}
for field in ("name", "product_number", "price", "color", "category_id"):
if field in data:
col = {"name": "Name", "product_number": "ProductNumber",
"price": "ListPrice", "color": "Color",
"category_id": "ProductCategoryID"}[field]
updates.append(f"{col} = %({field})s")
params[field] = data[field]
if not updates:
abort(400)
cursor.execute(f"""
UPDATE SalesLT.Product SET {', '.join(updates)}
OUTPUT INSERTED.ProductID, INSERTED.Name, INSERTED.ProductNumber, INSERTED.ListPrice,
INSERTED.Color, INSERTED.ProductCategoryID
WHERE ProductID = %(id)s
""", params)
row = cursor.fetchone()
if not row:
abort(404)
return jsonify({
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
})
@app.route("/products/<int:product_id>", methods=["DELETE"])
def delete_product(product_id):
"""Delete a product."""
cursor = get_db()
cursor.execute("DELETE FROM SalesLT.Product WHERE ProductID = %(id)s", {"id": product_id})
if cursor.rowcount == 0:
abort(404)
return "", 204
@app.route("/health")
def health_check():
"""Check database connectivity."""
try:
cursor = get_db()
cursor.execute("SELECT 1")
return jsonify({"status": "healthy", "database": "connected"})
except Exception as e:
return jsonify({"status": "unhealthy", "error": str(e)}), 503
Exécuter l’application
Démarrez le serveur de développement :
flask --app app run --debug --port 5000
Le serveur écoute sur http://localhost:5000. Ouvrez un second terminal et appelez les points de terminaison en utilisant curl pour confirmer que l’application communique avec votre base de données :
# Check database connectivity
curl http://localhost:5000/health
# List the first page of products
curl "http://localhost:5000/products?page_size=5"
# Get a single product by ID
curl http://localhost:5000/products/680
Note
Dans PowerShell, curl est un alias pour Invoke-WebRequest. Les commandes GET simples fonctionnent bien ici, mais la réponse revient sous forme d’objet plutôt qu’en JSON imprimé. Les commandes qui utilisent des options comme curl, -X, -H ou -d (comme l’exemple POST plus loin) ne fonctionnent pas telles quelles. Sous Windows, utilisez curl.exe pour exécuter les commandes exactement comme indiquées, ou utilisez celles de Invoke-RestMethod PowerShell (par exemple, Invoke-RestMethod http://localhost:5000/health), qui analysent également la réponse JSON pour vous.
Chaque terminaison retourne du JSON. Vous pouvez aussi ouvrir http://localhost:5000/products dans un navigateur pour consulter la liste paginationnée.
Regroupement de connexions
Sans pooling de connexions, chaque requête ouvre et ferme une connexion TCP vers Microsoft SQL, ce qui ajoute de la latence. Le pooling de connexions maintient un ensemble de connexions inactives prêtes à être réutilisées. Pour activer le pool de connexions, appelez mssql_python.pooling() une fois au niveau du module. Avec le pooling activé, conn.close() dans la phase de démontage de close_db renvoie la connexion au pool au lieu de la fermer.
Activer le regroupement de connexions
Activez le pooling en appelant mssql_python.pooling() au niveau du module avant l’ouverture de toute connexion :
# database.py with connection pooling
import mssql_python
from flask import g, current_app
# Configure pool at module level
mssql_python.pooling(max_size=20, idle_timeout=300)
def get_db():
"""Get a database cursor with connection pooling."""
if "db_conn" not in g:
g.db_conn = mssql_python.connect(get_connection_string())
g.db_cursor = g.db_conn.cursor()
return g.db_cursor
Gestion des erreurs
Flask permet d’enregistrer des gestionnaires pour des types d’exceptions spécifiques. En interceptant mssql_python.DatabaseError et mssql_python.IntegrityError, vous pouvez renvoyer des réponses d’erreur JSON structurées au lieu des pages d’erreur HTML par défaut.
Enregistrer des gestionnaires d’erreurs
Ajoutez ces manipulateurs à l’existant app.py, après la app = Flask(__name__) ligne. Étant donné que les gestionnaires font référence à l’objet app, ils doivent être définis après la création de l’application.
app.py nécessite import mssql_python en haut. Les gestionnaires renvoient des réponses JSON structurées au lieu de pages d’erreur HTML par défaut :
# app.py
import mssql_python
@app.errorhandler(mssql_python.DatabaseError)
def handle_database_error(error):
"""Handle database errors."""
return jsonify({"error": "Database error occurred"}), 500
@app.errorhandler(mssql_python.IntegrityError)
def handle_integrity_error(error):
"""Handle integrity constraint violations."""
error_msg = str(error)
if "UNIQUE" in error_msg:
return jsonify({"error": "Resource already exists"}), 409
if "FOREIGN KEY" in error_msg:
return jsonify({"error": "Referenced resource not found"}), 400
return jsonify({"error": "Data integrity error"}), 400
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Resource not found"}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({"error": "Bad request"}), 400
Blueprints
À mesure que votre application grandit, il devient difficile de maintenir toutes les routes dans un seul fichier. Flask Blueprints permet de regrouper les routes liées en modules distincts enregistrés dans l’application.
Organiser les itinéraires avec des plans
Créez un module blueprint pour les routes de produit qui importe get_db et définit les points de terminaison sous un préfixe URL partagé :
# blueprints/products.py
from flask import Blueprint, jsonify, request, abort
from database import get_db
products_bp = Blueprint("products", __name__, url_prefix="/api/products")
@products_bp.route("/")
def list_products():
"""List all products."""
cursor = get_db()
cursor.execute("""
SELECT ProductID, Name, ListPrice, Color, ProductCategoryID
FROM SalesLT.Product ORDER BY ProductID
""")
return jsonify([{
"id": row.ProductID,
"name": row.Name,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
} for row in cursor.fetchall()])
@products_bp.route("/<int:product_id>")
def get_product(product_id):
"""Get a product by ID."""
cursor = get_db()
cursor.execute(
"SELECT ProductID, Name, ListPrice, Color FROM SalesLT.Product WHERE ProductID = %(id)s",
{"id": product_id}
)
row = cursor.fetchone()
if not row:
abort(404)
return jsonify({"id": row.ProductID, "name": row.Name, "price": float(row.ListPrice), "color": row.Color})
Inscrivez le plan
Sauvegardez le blueprint sous forme blueprints/products.pyde , et ajoutez un fichier vide blueprints/__init__.py pour que Python traite le dossier comme un paquet. Ensuite, dans app.py, importez le blueprint avec vos autres importations et enregistrez-le après la app = Flask(__name__) ligne :
# app.py
from blueprints.products import products_bp
app.register_blueprint(products_bp)
Comme le blueprint définit url_prefix="/api/products", ses routes sont accessibles sous ce préfixe d’URL. Par exemple, la route de liste est disponible à http://localhost:5000/api/products/, distincte des /products routes définies directement dans app.py.
Testing
Flask fournit un client de test qui envoie des requêtes à votre application sans lancer un véritable serveur HTTP. Utilisez pytest fixtures pour créer le client et le réutiliser dans l’ensemble des tests.
Configuration du test avec pytest
Créer un accessoire pytest qui fournit un client de test et écrire des tests pour vérifier le comportement des routes :
# test_app.py
import uuid
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_health_check(client):
response = client.get("/health")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "healthy"
def test_list_products(client):
response = client.get("/products")
assert response.status_code == 200
data = response.get_json()
assert "items" in data
assert "total" in data
def test_create_product(client):
suffix = uuid.uuid4().hex[:8]
name = f"Test Product {suffix}"
response = client.post("/products", json={
"name": name,
"product_number": f"TEST-{suffix}",
"price": 19.99,
"category_id": 18
})
assert response.status_code == 201
data = response.get_json()
assert data["name"] == name
def test_get_product_not_found(client):
response = client.get("/products/99999")
assert response.status_code == 404
Ces tests s’exécutent sur votre base de données en direct plutôt que sur des mocks, donc test_create_product insère une vraie ligne dans SalesLT.Product. Dans AdventureWorksLT, Name et ProductNumber ont des contraintes uniques, donc le test génère une valeur unique pour chacun à chaque exécution. Si vous codez ces valeurs en dur à la place, le test échoue avec un conflit lors de la deuxième exécution, sauf si vous supprimez d’abord la ligne.
Exécuter les tests
Enregistrez les tests sous test_app.py dans le dossier de votre projet. Avec votre environnement virtuel activé, installez-le pytest et exécutez-le depuis ce dossier. Installer et exécuter pytest dans le même environnement virtuel que flask et mssql-python garantit que les tests importent les paquets utilisés par votre application.
pytest découvre test_app.py et rapporte automatiquement les résultats :
pip install pytest
pytest
pytest découvre test_app.py automatiquement et rapporte les résultats :
==================== test session starts ====================
collected 4 items
test_app.py .... [100%]
===================== 4 passed in 3.21s =====================