Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
FastAPI je moderní Python webový framework určený pro tvorbu API. Ve spojení s mssql-python můžete vytvářet vysoce výkonná REST API podporovaná Microsoft SQL a Azure SQL Database.
Předpoklady
- Python 3.10 nebo novější.
- Nainstalujte požadavky specifické pro jednorázový operační systém. Uživatelé Windows mohou tento krok přeskočit. Pro úplné podrobnosti o platformě viz Instalace mssql-python.
Vytvoření databáze SQL
Vytvořte nebo se připojte k SQL databázi na jedné z následujících platforem:
Příklady v tomto článku využívají databázi AdventureWorksLT , konkrétně tabulku SalesLT.Product . Pokud nemáte AdventureWorksLT nainstalovaný, podívejte se na ukázkové databáze AdventureWorks.
Nastavení projektu
Vytvoření virtuálního prostředí
Vytvořte a aktivujte virtuální prostředí, aby balíčky tohoto projektu zůstaly izolované od ostatních instalací Python. Tento krok také zabraňuje běžnému problému instalace balíčků do jednoho interpreteru při spuštění aplikace nebo testů s jiným.
py -m venv .venv
.\.venv\Scripts\Activate.ps1
Po aktivaci prostředí python, pip a pytest všechny odkazují na stejný interpret. Spusť zbývající příkazy v tomto článku z aktivovaného prostředí.
Note
Na Windows na ARM vytvořte prostředí pomocí Arm64 verze Python, takže mssql-python jeho závislosti instalujte z předpřipravených koleček. Na počítači s více než jednou verzí Pythonu může py -m venv vybrat jinou verzi nebo architekturu, než očekáváte, proto to po aktivaci ověřte pomocí python -c "import sys, sysconfig; print(sys.version, sysconfig.get_platform())". Pokud se pip pokusí sestavit cryptography ze zdroje (chyba v sadě nástrojů Rust a OpenSSL), nejprve pomocí pip install --only-binary=:all: cryptography nainstalujte verzi z wheel balíčku a poté nainstalujte zbytek.
Nainstalujte závislosti
Nainstalujte požadované balíčky pomocí pip:
pip install fastapi uvicorn mssql-python pydantic
Struktura projektu
Organizujte svůj projekt do samostatných modulů pro databáze, schémata a operace CRUD:
my_api/
├── main.py
├── database.py
├── models.py
├── schemas.py
├── crud.py
└── routers/
└── products.py
Správa připojení k databázi
FastAPI využívá injekci závislostí k poskytování zdrojů, jako jsou databázová připojení, pro směrovací handlery. Vzor v této části otevře spojení, poskytne kurzor a pomocí správce kontextu připojení v mssql-python při úspěchu potvrdí transakci, při výjimce provede její vrácení zpět a nakonec spojení uzavře.
Vytvořte database.py
Funkce get_connection_string() vytváří ODBC připojovací řetězec z konfiguračních hodnot. FastAPI volá Depends()get_db_dependency() jednou na požadavek a spravuje jeho životní cyklus.
# database.py
import mssql_python
from collections.abc import Generator
# Configuration
DATABASE_CONFIG = {
"server": "<server>.database.windows.net",
"database": "<database>",
}
def get_connection_string() -> str:
"""Build connection string from config."""
return (
f"Server={DATABASE_CONFIG['server']};"
f"Database={DATABASE_CONFIG['database']};"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes"
)
Note
ActiveDirectoryDefault používá DefaultAzureCredential, která zkouší více poskytovatelů přihlašovacích údajů postupně. První spojení může být pomalé, protože SDK prochází řetězec, dokud nenajde funkčního poskytovatele. V produkci, pokud víte, jaký typ přihlašovacích údajů vaše prostředí používá, zadejte ho přímo (například ActiveDirectoryMSI pro spravovanou identitu), abyste se vyhnuli tzv. chain walk. Další informace naleznete v tématu ověřování Microsoft Entra.
def get_db_dependency() -> Generator:
"""FastAPI dependency for database cursor."""
with mssql_python.connect(get_connection_string()) as conn:
with conn.cursor() as cursor:
yield cursor
Pydantické modely
Pydantické modely definují tvar a validační pravidla pro data požadavků a odpovědí. FastAPI používá tyto modely k analýze příchozího JSON, ověřování polních omezení a automatickému generování dokumentace OpenAPI.
Vytvořte schemas.py
Rozdělte schémata na Base, Create, Update a varianty odpovědí. Schéma Base obsahuje sdílená pole, Create z něj dědí pro operace vložení a Update označuje všechna pole jako volitelná pro částečné aktualizace.
# schemas.py
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from typing import Optional
from datetime import datetime
# Product schemas
class ProductBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
product_number: str = Field(..., min_length=1, max_length=25)
price: float = Field(..., gt=0)
color: Optional[str] = Field(None, max_length=50)
size: Optional[str] = Field(None, max_length=50)
category_id: Optional[int] = None
class ProductCreate(ProductBase):
pass
class ProductUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
product_number: Optional[str] = Field(None, min_length=1, max_length=25)
price: Optional[float] = Field(None, gt=0)
color: Optional[str] = Field(None, max_length=50)
size: Optional[str] = Field(None, max_length=50)
category_id: Optional[int] = None
class Product(ProductBase):
id: int
model_config = ConfigDict(from_attributes=True)
# Pagination
class PaginatedResponse(BaseModel):
items: list
total: int
page: int
page_size: int
pages: int
operace CRUD
Zapouzdřete databázové dotazy do specializované třídy, aby obslužné funkce tras zůstaly jednoduché. Každá statická metoda vezme kurzor (injektovaný FastAPI) a provede jednu operaci pomocí parametrizovaných dotazů (%(name)s zástupců se slovníkem hodnot), aby zabránila SQL injekci. Toto oddělení usnadňuje testování a opětovné použití obchodní logiky.
Vytvořte crud.py
# crud.py
from typing import Optional, List
from schemas import ProductCreate, ProductUpdate, Product
class ProductCRUD:
"""CRUD operations for products."""
@staticmethod
def get(cursor, product_id: int) -> Optional[dict]:
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, Size
FROM SalesLT.Product
WHERE ProductID = %(id)s
""", {"id": product_id})
row = cursor.fetchone()
if row:
return {
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
}
return None
@staticmethod
def get_all(cursor, skip: int = 0, limit: int = 100) -> List[dict]:
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, Size
FROM SalesLT.Product
ORDER BY ProductID
OFFSET %(skip)s ROWS
FETCH NEXT %(limit)s ROWS ONLY
""", {"skip": skip, "limit": limit})
return [{
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
} for row in cursor.fetchall()]
@staticmethod
def count(cursor) -> int:
cursor.execute("SELECT COUNT(*) FROM SalesLT.Product")
return cursor.fetchval()
@staticmethod
def create(cursor, product: ProductCreate) -> dict:
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.Size
VALUES (%(name)s, %(product_number)s, %(price)s, %(color)s, %(size)s, %(category_id)s, 0, GETDATE())
""", {
"name": product.name,
"product_number": product.product_number,
"price": product.price,
"color": product.color,
"size": product.size,
"category_id": product.category_id
})
row = cursor.fetchone()
return {
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
}
@staticmethod
def update(cursor, product_id: int, product: ProductUpdate) -> Optional[dict]:
# Build dynamic update
updates = []
params = {"id": product_id}
if product.name is not None:
updates.append("Name = %(name)s")
params["name"] = product.name
if product.product_number is not None:
updates.append("ProductNumber = %(product_number)s")
params["product_number"] = product.product_number
if product.price is not None:
updates.append("ListPrice = %(price)s")
params["price"] = product.price
if product.category_id is not None:
updates.append("ProductCategoryID = %(category_id)s")
params["category_id"] = product.category_id
if not updates:
return ProductCRUD.get(cursor, product_id)
cursor.execute(f"""
UPDATE SalesLT.Product SET {', '.join(updates)}
OUTPUT INSERTED.ProductID, INSERTED.Name, INSERTED.ProductNumber,
INSERTED.ListPrice, INSERTED.Color, INSERTED.Size
WHERE ProductID = %(id)s
""", params)
row = cursor.fetchone()
if row:
return {
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
}
return None
@staticmethod
def delete(cursor, product_id: int) -> bool:
cursor.execute("""
DELETE FROM SalesLT.Product WHERE ProductID = %(id)s
""", {"id": product_id})
return cursor.rowcount > 0
@staticmethod
def search(cursor, query: str, skip: int = 0, limit: int = 100) -> List[dict]:
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, Size
FROM SalesLT.Product
WHERE Name LIKE %(query)s OR ProductNumber LIKE %(query)s
ORDER BY ProductID
OFFSET %(skip)s ROWS
FETCH NEXT %(limit)s ROWS ONLY
""", {"query": f"%{query}%", "skip": skip, "limit": limit})
return [{
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
} for row in cursor.fetchall()]
Aplikace FastAPI
Vytvořte main.py
Hlavní modul vše zapojuje dohromady. Každá trasa deklaruje cursor = Depends(get_db_dependency), což říká FastAPI, aby zavolal generátor, předal výsledný kurzor obslužníkovi a následně ho vyčistil. FastAPI také ověřuje těla požadavků podle vašich schémat Pydantic ještě před spuštěním obslužné funkce.
# main.py
from fastapi import FastAPI, HTTPException, Depends, Query
from typing import List
from database import get_db_dependency
from schemas import Product, ProductCreate, ProductUpdate, PaginatedResponse
from crud import ProductCRUD
app = FastAPI(
title="Product API",
description="REST API for products using mssql-python",
version="1.0.0"
)
@app.get("/")
def root():
return {"message": "Product API", "docs": "/docs"}
@app.get("/products", response_model=PaginatedResponse)
def list_products(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
cursor = Depends(get_db_dependency)
):
"""List all products with pagination."""
skip = (page - 1) * page_size
items = ProductCRUD.get_all(cursor, skip=skip, limit=page_size)
total = ProductCRUD.count(cursor)
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size
}
@app.get("/products/{product_id}", response_model=Product)
def get_product(product_id: int, cursor = Depends(get_db_dependency)):
"""Get a specific product by ID."""
product = ProductCRUD.get(cursor, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
@app.post("/products", response_model=Product, status_code=201)
def create_product(product: ProductCreate, cursor = Depends(get_db_dependency)):
"""Create a new product."""
return ProductCRUD.create(cursor, product)
@app.put("/products/{product_id}", response_model=Product)
def update_product(
product_id: int,
product: ProductUpdate,
cursor = Depends(get_db_dependency)
):
"""Update an existing product."""
updated = ProductCRUD.update(cursor, product_id, product)
if not updated:
raise HTTPException(status_code=404, detail="Product not found")
return updated
@app.delete("/products/{product_id}", status_code=204)
def delete_product(product_id: int, cursor = Depends(get_db_dependency)):
"""Delete a product."""
if not ProductCRUD.delete(cursor, product_id):
raise HTTPException(status_code=404, detail="Product not found")
@app.get("/products/search/", response_model=List[Product])
def search_products(
q: str = Query(..., min_length=1),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
cursor = Depends(get_db_dependency)
):
"""Search products by name or product number."""
skip = (page - 1) * page_size
return ProductCRUD.search(cursor, q, skip=skip, limit=page_size)
# Health check endpoint
@app.get("/health")
def health_check(cursor = Depends(get_db_dependency)):
"""Check database connectivity."""
try:
cursor.execute("SELECT 1")
return {"status": "healthy", "database": "connected"}
except Exception:
raise HTTPException(status_code=503, detail="Database unavailable")
Spuštění aplikace
uvicorn main:app --reload --host 0.0.0.0 --port 8000
Otestujte a nasaděte aplikaci
Použijte doprovodný článek k dokončení žádosti:
Zpracování chyb
Doprovodný článek se zabývá zpracováním výjimek v databázi.
Globální obsluha výjimek
Viz Vyřizování chyb v databázi.
Sdílení připojení
Doprovodný článek se zabývá konfigurací spojovacího poolu.
Rozšířený databázový modul
Viz Konfigurace sdružování připojení.
middleware pro ověřování
Viz Přidat autentizační závislosti.
Testing
Doprovodný článek se zabývá integračním testováním.
Testovací nastavení
Viz Otestujte aplikaci.
Konfigurace nasazení
Doprovodný článek se zabývá konfigurací nasazení a provozem.
Proměnné prostředí
Viz Nastavit nastavení nasazení a kontrolní seznam nasazení.