Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
FastAPI es un framework web moderno en Python para construir APIs. Combinado con mssql-python, puedes construir APIs REST de alto rendimiento respaldadas por Microsoft SQL y Azure SQL Database.
Prerequisites
- Python 3.10 o posterior.
- Instala los requisitos previos específicos del sistema operativo, que solo hay que instalar una vez. Los usuarios de Windows pueden saltarse este paso. Para detalles completos sobre la plataforma, véase Instalar mssql-python.
Creación de una base de datos SQL
Crea o conéctate a una base de datos SQL en una de las siguientes plataformas:
Los ejemplos de este artículo utilizan la base de datos de ejemplo AdventureWorksLT , concretamente la SalesLT.Product tabla. Si no tienes instalado AdventureWorksLT, consulta las bases de datos de ejemplo de AdventureWorks.
Configuración del proyecto
Creación de un entorno virtual
Crea y activa un entorno virtual para que los paquetes de este proyecto permanezcan aislados de otras instalaciones de Python. Este paso también evita el problema común de instalar paquetes en un intérprete mientras ejecutas tu aplicación o pruebas con otro.
py -m venv .venv
.\.venv\Scripts\Activate.ps1
Después de activar el entorno, python, pip y pytest, todos apuntan al mismo intérprete. Ejecuta los comandos restantes de este artículo desde el entorno activado.
Note
En Windows para Arm, cree el entorno con una compilación Arm64 de Python para que mssql-python y sus dependencias se instalen desde wheels precompilados. En una máquina con más de una versión de Python, py -m venv puede que selecciones una versión o arquitectura diferente a la que esperas, así que verifica después python -c "import sys, sysconfig; print(sys.version, sysconfig.get_platform())" de activarla. Si pip intenta compilar cryptography desde el código fuente (un error de Rust y OpenSSL), instale primero una versión basada en wheel con pip install --only-binary=:all: cryptography, y luego instale el resto.
Instalación de dependencias
Instala los paquetes necesarios con pip:
pip install fastapi uvicorn mssql-python pydantic
Estructura del proyecto
Organiza tu proyecto con módulos separados para bases de datos, esquemas y operaciones CRUD:
my_api/
├── main.py
├── database.py
├── models.py
├── schemas.py
├── crud.py
└── routers/
└── products.py
Gestión de conexiones de bases de datos
FastAPI utiliza inyección de dependencias para proporcionar recursos como conexiones a bases de datos a los manejadores de rutas. El patrón en esta sección abre una conexión, genera un cursor y utiliza el gestor de contexto de conexiones mssql-python para hacer commit en caso de éxito, revertir una excepción y cerrar la conexión.
Cree database.py
La get_connection_string() función construye la cadena de conexión ODBC a partir de valores de configuración.
Depends() de FastAPI llama get_db_dependency() una vez por solicitud y gestiona su ciclo de vida..
# 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 utiliza DefaultAzureCredential, que prueba varios proveedores de credenciales de forma secuencial. La primera conexión puede ser lenta porque el SDK recorre la cadena hasta encontrar un proveedor que funcione. En producción, si sabes qué tipo de credencial utiliza tu entorno, especifícala directamente (por ejemplo, ActiveDirectoryMSI para identidad gestionada) para evitar el recorrido en cadena. Para más información, consulte Autenticación de 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
Modelos de Pydantic
Los modelos pydânticos definen las reglas de forma y validación para los datos de petición y respuesta. FastAPI utiliza estos modelos para analizar JSON entrante, validar restricciones de campos y generar automáticamente documentación OpenAPI.
Cree schemas.py
Separar los esquemas en Base, Create, Update, y variantes de respuesta. El Base esquema contiene campos compartidos, Create hereda de él para las operaciones de inserción y Update hace que todos los campos sean opcionales para actualizaciones parciales.
# 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
Operaciones CRUD
Encapsula las consultas a la base de datos en una clase dedicada para mantener los controladores de ruta ligeros. Cada método estático toma un cursor (inyectado por FastAPI) y gestiona una operación usando consultas parametrizadas (%(name)s marcadores de posición con un diccionario de valores) para evitar la inyección SQL. Esta separación facilita la prueba y reutilización de la lógica de negocio.
Cree 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()]
Aplicación FastAPI
Crear main.py
El módulo principal conecta todo. Cada ruta declara cursor = Depends(get_db_dependency), lo que le indica a FastAPI que llame al generador, pase el cursor generado al manejador y realice la limpieza después. FastAPI también valida los cuerpos de solicitud conforme a tus esquemas de Pydantic antes de que se ejecute el controlador.
# 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")
Ejecutar la aplicación
uvicorn main:app --reload --host 0.0.0.0 --port 8000
Prueba y despliega la aplicación
Utiliza el artículo complementario para completar la solicitud:
Gestión de errores
El artículo complementario trata sobre la gestión de excepciones en bases de datos.
Gestor global de excepciones
Ver Gestionar errores de base de datos.
Agrupación de conexiones
El artículo complementario trata sobre la configuración del pool de conexión.
Módulo de base de datos mejorado
Consulta Configurar la agrupación de conexiones.
Middleware de autenticación
Consulta Añadir dependencias de autenticación.
Testing
El artículo complementario trata sobre las pruebas de integración.
Configuración de pruebas
Ver Probar la aplicación.
Configuración de la implementación
El artículo complementario trata sobre la configuración y operaciones de despliegue.
Variables de entorno
Consulta Configurar configuración de despliegue y la lista de comprobación de despliegue.