Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Flask è un framework web Python leggero che ti dà il pieno controllo sulla struttura dell'applicazione. Combinato con mssql-python, puoi costruire applicazioni web e API REST supportate da Microsoft SQL e database SQL di Azure con un overhead minimo.
Prerequisiti
- Python 3.10 o versione successiva.
- Pacchetti
mssql-pythoneflask. Installa entrambi conpip install flask mssql-python. - Installare prerequisiti specifici del sistema operativo monouso. Gli utenti Windows possono saltare questo passaggio. Per dettagli completi sulla piattaforma, vedi Installa mssql-python.
Creare un database SQL
Crea o collegati a un database SQL su una delle seguenti piattaforme:
Gli esempi in questo articolo utilizzano il database di esempio AdventureWorksLT , in particolare la SalesLT.Product tabella. Se non hai installato AdventureWorksLT, consulta i database di esempio di AdventureWorks.
Configurazione del progetto
Installa le dipendenze
Installa i pacchetti necessari con pip:
pip install flask mssql-python
Struttura del progetto
Organizza il tuo progetto con moduli separati per configurazione, gestione delle connessioni, rotte e test:
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
Gestione delle connessioni al database
Flask non include un livello database integrato, quindi gestisci direttamente le connessioni. Il pattern in questa sezione memorizza una connessione per ogni richiesta sull'oggetto di g Flask e la chiude automaticamente quando la richiesta termina.
Crea config.py
Centralizza le impostazioni del database in una classe di configurazione. Le variabili di ambiente ti permettono di sovrascrivere i valori predefiniti senza cambiare codice.
# 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"))
Crea database.py
Il database.py modulo gestisce il ciclo di vita della connessione. L'oggetto di g Flask è uno spazio per richiesta, quindi memorizzare la connessione lì garantisce che ogni richiesta riceva la propria connessione che viene ripulita quando la richiesta termina.
La get_connection_string() funzione costruisce la stringa di connessione dalla configurazione dell'app. La get_db() funzione crea una connessione alla prima chiamata e la riutilizza per il resto della richiesta. La funzione close_db() viene eseguita automaticamente alla fine di ogni richiesta, annullando la transazione se si verifica un'eccezione e confermandola in caso contrario. La init_app() funzione registra questo comportamento di teardown con l'app 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 usa DefaultAzureCredential, che prova più fornitori di credenziali in sequenza. La prima connessione può essere lenta perché l'SDK percorre la catena finché non trova un fornitore funzionante. In produzione, se sai quale tipo di credenziale utilizza il tuo ambiente, specificalo direttamente (ad esempio, ActiveDirectoryMSI per l'identità gestita) per evitare il chain walk. Per altre informazioni, vedere Autenticazione di Microsoft Entra.
Applicazione Flask
Il seguente esempio mostra un'applicazione Flask completa con percorsi per elencare, recuperare, creare, aggiornare e cancellare prodotti.
Crea app.py
Il modulo applicativo crea l'app Flask, carica la configurazione e registra il dismantellamento del database. Ogni funzione di routing chiama get_db() per ottenere un cursore, esegue query con SQL parametrizzato (utilizzando i segnaposto %(name)s e un dizionario di valori) e restituisce risposte 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
Eseguire l'applicazione
Avviare il server di sviluppo:
flask --app app run --debug --port 5000
Il server ascolta su http://localhost:5000. Apri un secondo terminale e chiama gli endpoint utilizzando curl per confermare che l'app si stia connettendo al database:
# 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
In PowerShell curl è un alias per Invoke-WebRequest. I semplici comandi GET qui funzionano bene, ma la risposta torna come oggetto invece che come JSON stampato. I comandi che usano curl flag come -X, -H, o -d (come nell'esempio POST più avanti) non funzionano come scritti. Su Windows, usa curl.exe per eseguire i comandi esattamente come mostrati, oppure usa quello di Invoke-RestMethod PowerShell (ad esempio, Invoke-RestMethod http://localhost:5000/health), che analizza anche la risposta JSON per te.
Ogni endpoint restituisce JSON. Puoi anche aprire http://localhost:5000/products in un browser per visualizzare la lista paginata.
Pool di connessioni
Senza il pool di connessioni, ogni richiesta apre e chiude una connessione TCP a Microsoft SQL, che aggiunge latenza. Il pool di connessione mantiene un insieme di connessioni inattive pronte per il riutilizzo. Per abilitare il pooling delle connessioni, chiama mssql_python.pooling() una sola volta a livello di modulo. Con il pooling attivato, conn.close() nel close_db teardown la connessione viene restituita al pool invece di chiuderlo.
Abilitare il pool di connessioni
Abilita il pooling chiamando mssql_python.pooling() a livello di modulo prima che vengano aperte eventuali connessioni:
# 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
Gestione degli errori
Flask ti permette di registrare i gestori per tipi specifici di eccezione. Intercettare mssql_python.DatabaseError e mssql_python.IntegrityError consente di restituire risposte di errore JSON strutturate invece delle pagine di errore HTML predefinite.
Registrare i gestori degli errori
Aggiungi questi handler all'esistente app.py, dopo la app = Flask(__name__) linea. Poiché gli handler fanno riferimento all'oggetto app, devono venire dopo che l'app è stata creata.
app.py richiede import mssql_python in alto. I gestori restituiscono risposte JSON strutturate invece delle pagine di errore HTML predefinite:
# 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
Man mano che la tua applicazione cresce, mettere tutte le route in un unico file diventa difficile da mantenere. Flask Blueprints ti permette di raggruppare percorsi correlati in moduli separati registrati nell'app.
Organizza i percorsi con i modelli
Crea un modulo blueprint per le rotte di prodotto che importi get_db e definisce endpoint sotto un prefisso URL condiviso:
# 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})
Registra il progetto
Salva il blueprint come blueprints/products.py, e aggiungi un file vuoto blueprints/__init__.py così Python tratta la cartella come un pacchetto. Poi, in app.py, importa il blueprint insieme alle altre importazioni e registralo dopo la riga app = Flask(__name__):
# app.py
from blueprints.products import products_bp
app.register_blueprint(products_bp)
Poiché il blueprint imposta url_prefix="/api/products", le sue rotte vengono esposte sotto quel prefisso. Ad esempio, la rotta elenco è disponibile in http://localhost:5000/api/products/, separata dalle rotte /products definite direttamente in app.py.
Testing
Flask fornisce un client di test che invia richieste alla tua applicazione senza avviare un vero server HTTP. Usa pytest i fixture per creare il client e riutilizzalo tra i test.
Configurazione del test con pytest
Crea un fix pytest che fornisca un client di test e scrivi test per verificare il comportamento delle route:
# 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
Questi test si eseguono sul tuo database live invece che sulle mock, quindi test_create_product inserisce una riga reale in SalesLT.Product. In AdventureWorksLT, sia Name che ProductNumber hanno vincoli unici, quindi il test genera un valore unico per ciascuno a ogni esecuzione. Se invece codifica quei valori in modo rigido, il test fallisce con un conflitto alla seconda esecuzione, a meno che tu non elimini prima la riga.
Esegui i test
Salva i test come test_app.py nella cartella del progetto. Con il tuo ambiente virtuale attivato, installalo pytest ed eseguilo da quella cartella. Installare e girare pytest all'interno dello stesso ambiente virtuale di flask e mssql-python garantisce che i test importino i pacchetti che la tua app utilizza.
pytest scopre test_app.py e riporta automaticamente i risultati:
pip install pytest
pytest
pytest scopre test_app.py automaticamente e riporta i risultati:
==================== test session starts ====================
collected 4 items
test_app.py .... [100%]
===================== 4 passed in 3.21s =====================