1. 为什么需要ORM响应页面数据?
在FastAPI项目中处理前端页面数据时,我们通常会遇到一个典型问题:如何将数据库查询结果高效地转换为前端需要的JSON格式?直接使用原始SQL查询虽然灵活,但会面临几个痛点:
- 手动拼装JSON结构繁琐且容易出错
- 字段类型转换需要额外处理
- 关联查询结果需要复杂的嵌套处理
- 分页等通用功能需要重复实现
以用户列表页面为例,假设我们需要返回如下结构的数据:
json复制{
"users": [
{
"id": 1,
"name": "张三",
"role": {
"id": 1,
"name": "管理员"
}
}
],
"pagination": {
"total": 100,
"page": 1,
"per_page": 20
}
}
如果手动实现这个结构,我们需要:
- 编写用户表和角色表的JOIN查询
- 手动处理角色对象的嵌套
- 单独计算分页信息
- 确保所有日期字段都转为ISO格式字符串
而使用ORM可以极大简化这个过程。以SQLAlchemy为例,只需定义好模型关系,ORM会自动处理:
- 关联对象的嵌套
- 数据类型的转换
- 复杂查询的构建
2. FastAPI中的ORM选型与实践
2.1 主流Python ORM对比
FastAPI官方推荐使用SQLAlchemy或Tortoise-ORM,以下是它们的核心特点:
| 特性 | SQLAlchemy | Tortoise-ORM | Django ORM |
|---|---|---|---|
| 异步支持 | 1.4+版本支持 | 原生支持 | 3.0+版本支持 |
| 学习曲线 | 较陡峭 | 中等 | 平缓 |
| 性能 | 高 | 中 | 中 |
| 生态完整性 | 非常完整 | 正在完善 | 非常完整 |
| 适用场景 | 复杂业务系统 | 快速开发 | Django项目 |
对于大多数FastAPI项目,SQLAlchemy 1.4+是更稳妥的选择,因为它:
- 支持同步和异步两种模式
- 有成熟的生态和社区支持
- 灵活的查询构建能力
2.2 基础集成步骤
安装依赖:
bash复制pip install sqlalchemy fastapi pydantic
数据库配置(database.py):
python复制from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
定义模型(models.py):
python复制from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50))
email = Column(String(100), unique=True)
role_id = Column(Integer, ForeignKey("roles.id"))
role = relationship("Role", back_populates="users")
class Role(Base):
__tablename__ = "roles"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50))
users = relationship("User", back_populates="role")
注意:relationship的back_populates参数必须成对使用,它定义了双向关系
3. 响应模型设计与序列化
3.1 Pydantic响应模型
FastAPI使用Pydantic模型来处理响应数据的序列化。我们需要为每个ORM模型创建对应的Pydantic模型:
python复制from pydantic import BaseModel
from typing import List, Optional
class RoleBase(BaseModel):
name: str
class Role(RoleBase):
id: int
class Config:
orm_mode = True
class UserBase(BaseModel):
name: str
email: str
class User(UserBase):
id: int
role: Role
class Config:
orm_mode = True
关键点说明:
orm_mode = True允许Pydantic模型从ORM对象读取数据- 嵌套模型会自动处理关联对象的序列化
- 可以使用
exclude参数隐藏敏感字段
3.2 分页响应模型
对于分页数据,我们可以定义通用分页响应模型:
python复制from typing import Generic, TypeVar, List
from pydantic.generics import GenericModel
T = TypeVar('T')
class Pagination(BaseModel):
total: int
page: int
per_page: int
class PaginatedResponse(GenericModel, Generic[T]):
data: List[T]
pagination: Pagination
使用示例:
python复制@app.get("/users/", response_model=PaginatedResponse[User])
async def get_users(page: int = 1, per_page: int = 20):
# 查询逻辑
return {
"data": users,
"pagination": {
"total": total,
"page": page,
"per_page": per_page
}
}
4. 高级查询与性能优化
4.1 关联加载策略
N+1查询问题是ORM常见性能陷阱。假设我们查询用户列表并需要显示角色信息:
python复制# 错误的做法 - 会产生N+1查询
users = db.query(User).all()
for user in users:
print(user.role.name) # 每次访问都会产生新的查询
正确的关联加载方式:
python复制from sqlalchemy.orm import joinedload
# 方法1:使用joinedload立即加载
users = db.query(User).options(joinedload(User.role)).all()
# 方法2:使用selectinload(适合一对多关系)
from sqlalchemy.orm import selectinload
users = db.query(User).options(selectinload(User.role)).all()
不同加载策略的对比:
| 策略 | 原理 | 适用场景 | 优缺点 |
|---|---|---|---|
| joinedload | 使用JOIN一次性加载 | 一对一或少量多对一关系 | 减少查询次数但可能数据冗余 |
| selectinload | 使用IN查询二次加载 | 一对多或多对多关系 | 查询次数固定但IN可能受限 |
| subqueryload | 使用子查询二次加载 | 复杂关系 | 可能性能较差 |
4.2 查询构建技巧
- 动态字段选择:
python复制from sqlalchemy.orm import load_only
@app.get("/users/minimal")
async def get_users_minimal():
users = db.query(User).options(load_only(User.id, User.name)).all()
return users
- 条件过滤:
python复制from sqlalchemy import or_
@app.get("/users/search")
async def search_users(keyword: str):
users = db.query(User).filter(
or_(
User.name.ilike(f"%{keyword}%"),
User.email.ilike(f"%{keyword}%")
)
).all()
return users
- 复合排序:
python复制from sqlalchemy import desc
@app.get("/users/sorted")
async def get_sorted_users():
users = db.query(User).order_by(
desc(User.created_at),
User.name
).all()
return users
5. 实战:用户管理页面API实现
5.1 完整端点实现
python复制from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
router = APIRouter()
# 依赖项获取数据库会话
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@router.get("/users/", response_model=PaginatedResponse[User])
async def list_users(
page: int = 1,
per_page: int = 20,
name: str = None,
role_id: int = None,
db: Session = Depends(get_db)
):
query = db.query(User).options(joinedload(User.role))
# 应用过滤条件
if name:
query = query.filter(User.name.ilike(f"%{name}%"))
if role_id:
query = query.filter(User.role_id == role_id)
# 计算总数
total = query.count()
# 应用分页
users = query.offset((page - 1) * per_page).limit(per_page).all()
return {
"data": users,
"pagination": {
"total": total,
"page": page,
"per_page": per_page
}
}
5.2 常见问题处理
- 循环引用问题:
当模型存在双向关系时,直接序列化可能导致无限递归。解决方案:
python复制class User(BaseModel):
id: int
name: str
role: Optional[Role] # 使用Optional避免循环
class Config:
orm_mode = True
json_encoders = {
datetime: lambda v: v.isoformat()
}
- 性能监控:
使用SQLAlchemy的事件监听来监控查询性能:
python复制from sqlalchemy import event
import time
@event.listens_for(engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
context._query_start_time = time.time()
@event.listens_for(engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
duration = time.time() - context._query_start_time
if duration > 0.5: # 记录慢查询
logger.warning(f"Slow query: {statement} took {duration:.2f}s")
- 批量操作优化:
对于批量插入/更新,使用bulk操作提升性能:
python复制# 普通方式 - 性能差
for item in items:
db.add(User(**item))
# 批量方式 - 性能好
db.bulk_insert_mappings(User, items)
6. 部署与性能调优
6.1 数据库连接池配置
在生产环境中,合理的连接池配置至关重要:
python复制from sqlalchemy.pool import QueuePool
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
poolclass=QueuePool,
pool_size=20, # 保持的连接数
max_overflow=10, # 超过pool_size后允许创建的连接数
pool_timeout=30, # 获取连接的超时时间(秒)
pool_recycle=3600 # 连接回收时间(秒)
)
6.2 异步支持
对于高并发场景,可以使用SQLAlchemy 1.4+的异步支持:
python复制from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
ASYNC_DATABASE_URL = "postgresql+asyncpg://user:password@localhost/db"
async_engine = create_async_engine(ASYNC_DATABASE_URL)
AsyncSessionLocal = sessionmaker(
bind=async_engine,
class_=AsyncSession,
expire_on_commit=False
)
async def get_async_db():
async with AsyncSessionLocal() as db:
yield db
6.3 缓存策略
对于读多写少的数据,可以引入缓存层:
python复制from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.decorator import cache
@app.on_event("startup")
async def startup():
FastAPICache.init(RedisBackend("redis://localhost"))
@router.get("/users/{user_id}")
@cache(expire=60) # 缓存60秒
async def get_user(user_id: int, db: Session = Depends(get_db)):
return db.query(User).get(user_id)
7. 测试与调试技巧
7.1 单元测试配置
使用pytest编写ORM相关测试:
python复制import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture
def test_db():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(bind=engine)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
yield
Base.metadata.drop_all(bind=engine)
def test_list_users(test_db):
client = TestClient(app)
response = client.get("/users/")
assert response.status_code == 200
assert "pagination" in response.json()
7.2 SQL日志调试
开发阶段可以开启SQL日志:
python复制import logging
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
或者在FastAPI中间件中记录慢查询:
python复制@app.middleware("http")
async def log_sql_queries(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
queries = request.state.get("queries", [])
slow_queries = [q for q in queries if q["duration"] > 0.5]
if slow_queries:
logger.warning(f"Slow queries detected in {request.url}:")
for query in slow_queries:
logger.warning(f"{query['statement']} took {query['duration']:.2f}s")
return response
8. 安全最佳实践
8.1 敏感字段处理
对于密码等敏感字段,应该:
- 在Pydantic模型中排除:
python复制class UserCreate(BaseModel):
name: str
email: str
password: str
class UserResponse(BaseModel):
id: int
name: str
email: str
class Config:
orm_mode = True
- 在ORM层面使用hybrid_property:
python复制from sqlalchemy.ext.hybrid import hybrid_property
class User(Base):
__tablename__ = "users"
_password = Column("password", String(128))
@hybrid_property
def password(self):
raise AttributeError("Password is not readable")
@password.setter
def password(self, value):
self._password = hash_password(value)
8.2 批量操作防护
对于批量查询接口,应该:
- 限制最大返回数量:
python复制MAX_PER_PAGE = 100
@router.get("/users/")
async def list_users(per_page: int = 20):
if per_page > MAX_PER_PAGE:
raise HTTPException(400, f"per_page cannot exceed {MAX_PER_PAGE}")
# ...
- 添加速率限制:
python复制from fastapi import Request
from fastapi.middleware import Middleware
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.get("/users/")
@limiter.limit("100/minute")
async def list_users(request: Request):
# ...
9. 项目结构建议
对于大型项目,推荐的组织结构:
code复制project/
├── app/
│ ├── core/ # 核心配置
│ │ ├── config.py
│ │ └── security.py
│ ├── models/ # 数据库模型
│ │ ├── base.py # 基础模型
│ │ ├── user.py
│ │ └── role.py
│ ├── schemas/ # Pydantic模型
│ │ ├── user.py
│ │ └── common.py # 通用模型
│ ├── api/ # 路由
│ │ ├── v1/ # API版本
│ │ │ ├── users.py
│ │ │ └── roles.py
│ ├── db/ # 数据库相关
│ │ ├── session.py
│ │ └── utils.py # 数据库工具
│ └── main.py # FastAPI应用
├── tests/ # 测试
└── alembic/ # 数据库迁移
关键设计原则:
- 模型与模式分离(models vs schemas)
- 按业务功能组织路由
- 核心配置集中管理
- 测试与主代码结构对应
10. 性能监控与优化
10.1 监控指标
关键ORM性能指标:
- 查询响应时间分布
- N+1查询发生率
- 最频繁执行的查询
- 连接池使用情况
使用Prometheus监控示例:
python复制from prometheus_fastapi_instrumentator import Instrumentator
@app.on_event("startup")
async def startup():
Instrumentator().instrument(app).expose(app)
10.2 优化案例
案例:用户列表页从2s优化到200ms
优化前:
- 查询所有用户(1次查询)
- 每个用户访问role属性(N次查询)
- 总计:N+1次查询
优化步骤:
- 使用joinedload预加载角色
- 添加适当的数据库索引
- 实现前端分页而非全量查询
- 对不变化的角色数据添加缓存
优化后SQL日志:
sql复制SELECT users.id, users.name, roles.id AS role_id, roles.name AS role_name
FROM users LEFT OUTER JOIN roles ON users.role_id = roles.id
LIMIT 20 OFFSET 0
11. 错误处理与调试
11.1 常见ORM错误
- 会话管理错误:
python复制# 错误:在会话外访问延迟加载的属性
user = db.query(User).first()
db.close()
print(user.role) # 抛出DetachedInstanceError
- 事务处理错误:
python复制# 错误:未处理异常导致事务未提交
try:
user = User(name="test")
db.add(user)
raise ValueError("模拟错误")
db.commit() # 不会执行
except:
pass
# 实际:事务未提交,但对象可能处于不一致状态
正确做法:
python复制try:
user = User(name="test")
db.add(user)
db.commit() # 先提交
raise ValueError("模拟错误")
except:
db.rollback() # 明确回滚
11.2 调试技巧
- 使用echo=True查看SQL:
python复制engine = create_engine("sqlite://", echo=True)
- 检查生成的SQL:
python复制from sqlalchemy.dialects import postgresql
query = db.query(User).filter(User.name == "test")
print(query.statement.compile(dialect=postgresql.dialect()))
- 使用SQLAlchemy-Utils的调试工具:
python复制from sqlalchemy_utils import explain
query = db.query(User)
print(explain(query.statement, db.bind))
12. 迁移与版本控制
12.1 Alembic配置
数据库迁移配置(alembic.ini):
ini复制[alembic]
script_location = alembic
sqlalchemy.url = sqlite:///./test.db
迁移环境(alembic/env.py):
python复制from app.models.base import Base
from app.core.config import settings
target_metadata = Base.metadata
12.2 创建迁移
生成迁移脚本:
bash复制alembic revision --autogenerate -m "add user table"
应用迁移:
bash复制alembic upgrade head
12.3 迁移最佳实践
- 总是先测试迁移脚本
- 生产环境使用事务性迁移
- 大表变更使用在线DDL工具
- 维护回滚脚本
13. 扩展:GraphQL集成
对于复杂的前端数据需求,可以考虑GraphQL:
python复制import strawberry
from strawberry.fastapi import GraphQLRouter
@strawberry.type
class UserType:
id: int
name: str
email: str
@strawberry.type
class Query:
@strawberry.field
async def users(self, info) -> List[UserType]:
db = info.context["db"]
return db.query(User).all()
schema = strawberry.Schema(Query)
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")
优势:
- 前端可以精确指定需要的字段
- 减少接口版本兼容问题
- 自动处理嵌套关系
14. 微服务场景下的特殊考虑
当FastAPI作为微服务使用时:
- 数据库会话生命周期管理:
python复制async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
- 分布式事务处理:
python复制from saga_pattern import Saga
@app.post("/orders")
async def create_order(db: AsyncSession = Depends(get_db)):
saga = Saga()
try:
async with db.begin():
# 步骤1:创建订单
order = Order(...)
db.add(order)
# 步骤2:扣减库存(调用库存服务)
await saga.add_compensation(
"inventory",
post("http://inventory-service/stock", json={"product": 1, "qty": -1}),
post("http://inventory-service/stock", json={"product": 1, "qty": 1})
)
# 提交本地事务
await db.commit()
# 执行saga
await saga.execute()
except:
await saga.compensate()
raise
15. 前端集成建议
15.1 分页参数约定
推荐的前端分页参数格式:
javascript复制// 请求
GET /users?page=1&per_page=20&sort=-created_at,name
// 响应
{
"data": [...],
"pagination": {
"total": 100,
"page": 1,
"per_page": 20,
"total_pages": 5
}
}
后端实现:
python复制from fastapi import Query
@app.get("/users")
async def list_users(
page: int = Query(1, gt=0),
per_page: int = Query(20, gt=0, le=100),
sort: str = Query(None, regex=r"^[-a-z,]+$")
):
query = db.query(User)
# 处理排序
if sort:
for field in sort.split(","):
direction = field.startswith("-")
field_name = field.lstrip("-")
if hasattr(User, field_name):
column = getattr(User, field_name)
query = query.order_by(column.desc() if direction else column.asc())
# ...分页逻辑
15.2 字段选择与扩展
GraphQL风格字段选择:
javascript复制GET /users?fields=id,name,role{id,name}
后端实现:
python复制from sqlalchemy.orm import load_only, contains_eager
@app.get("/users")
async def list_users(fields: str = None):
query = db.query(User)
if fields:
# 解析字段选择
selected = parse_fields(fields)
# 应用字段加载
if "role" in selected["relations"]:
query = query.options(contains_eager(User.role))
if selected["attributes"]:
query = query.options(load_only(*selected["attributes"]))
return query.all()
16. 性能基准测试
使用locust进行压力测试:
python复制from locust import HttpUser, task, between
class ORMUser(HttpUser):
wait_time = between(1, 5)
@task
def list_users(self):
self.client.get("/users?per_page=20")
@task(3)
def get_user(self):
self.client.get("/users/1")
典型优化前后的性能对比:
| 场景 | 请求量 (RPS) | 平均响应时间 | 错误率 |
|---|---|---|---|
| 基础实现 | 120 | 450ms | 0.1% |
| 预加载优化 | 350 | 180ms | 0% |
| 添加缓存后 | 1500 | 50ms | 0% |
| 异步+连接池优化 | 3000 | 30ms | 0% |
17. 日志与监控集成
17.1 结构化日志
python复制import structlog
logger = structlog.get_logger()
@app.middleware("http")
async def log_requests(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
structlog.contextvars.bind_contextvars(
path=request.url.path,
method=request.method,
status=response.status_code,
duration=process_time
)
queries = request.state.get("queries", [])
logger.info(
"request_completed",
query_count=len(queries),
slow_queries=sum(1 for q in queries if q["duration"] > 0.5)
)
return response
17.2 OpenTelemetry集成
python复制from opentelemetry import trace
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
tracer = trace.get_tracer(__name__)
SQLAlchemyInstrumentor().instrument(engine=engine)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
with tracer.start_as_current_span("get_user"):
# 查询逻辑
return db.query(User).get(user_id)
18. 容器化部署建议
18.1 Dockerfile配置
dockerfile复制FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
18.2 健康检查配置
python复制from fastapi import Response
@app.get("/health")
async def health_check(db: Session = Depends(get_db)):
try:
# 检查数据库连接
db.execute("SELECT 1")
return Response(status_code=200)
except:
return Response(status_code=503)
Kubernetes健康检查配置:
yaml复制livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
19. 本地开发环境优化
19.1 开发工具推荐
-
SQLAlchemy-Utils:提供各种有用的工具函数
python复制from sqlalchemy_utils import create_database, drop_database create_database(engine.url) -
Alembic Autogenerate:自动生成迁移脚本
bash复制alembic revision --autogenerate -m "description" -
PgHero:PostgreSQL性能监控
python复制from pghero import PgHero pghero = PgHero("postgres://user:password@localhost/db")
19.2 开发配置建议
config.py示例:
python复制import os
from pydantic import BaseSettings
class Settings(BaseSettings):
ENV: str = "dev"
DATABASE_URL: str = "sqlite:///./test.db"
DEBUG: bool = True
class Config:
env_file = ".env"
settings = Settings()
开发环境启动脚本:
bash复制#!/bin/bash
# 启动开发服务器
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
20. 持续集成与测试
20.1 GitHub Actions配置
yaml复制name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: |
pytest --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v1
20.2 测试策略
- 模型测试:
python复制def test_user_model():
user = User(name="Test", email="test@example.com")
assert user.name == "Test"
assert user.email == "test@example.com"
- 服务层测试:
python复制async def test_create_user(test_db):
service = UserService(test_db)
user = await service.create_user(name="Test", email="test@example.com")
assert user.id is not None
- API测试:
python复制def test_list_users(client):
response = client.get("/users/")
assert response.status_code == 200
assert "pagination" in response.json()
21. 文档生成与API描述
21.1 OpenAPI扩展
自定义OpenAPI文档:
python复制app = FastAPI(
title="User Management API",
description="API for managing users with ORM integration",
version="1.0.0",
openapi_tags=[{
"name": "users",
"description": "Operations with users"
}]
)
@app.get("/users/", tags=["users"], summary="List users")
async def list_users():
"""Retrieve a paginated list of users with their roles."""
pass
21.2 响应示例
为API添加响应示例:
python复制from fastapi.responses import JSONResponse
@app.get(
"/users/",
responses={
200: {
"content": {
"application/json": {
"example": {
"data": [{
"id": 1,
"name": "John Doe",
"role": {"id": 1, "name": "admin"}
}],
"pagination": {
"total": 100,
"page": 1,
"per_page": 20
}
}
}
}
}
}
)
async def list_users():
pass
22. 高级主题:多租户支持
22.1 架构设计
多租户常见实现方案:
-
独立数据库:每个租户有单独的数据库
- 优点:完全隔离
- 缺点:运维复杂
-
共享数据库,独立Schema:
- 优点:较好隔离
- 缺点:迁移复杂
-
共享Schema,租户ID区分:
- 优点:简单
- 缺点:容易出错
22.2 实现示例
使用租户ID过滤:
python复制from fastapi import Request
def get_tenant_id(request: Request):
return request.headers.get("X-Tenant-ID")
@app.middleware("http")
async def tenant_middleware(request: Request, call_next):
tenant_id = get_tenant_id(request)
request.state.tenant_id = tenant_id
response = await call_next(request)
return response
def get_db(request: Request):
tenant_id = request.state.tenant_id
engine = get_engine_for_tenant(tenant_id)
return SessionLocal(bind=engine)
@app.get("/users/")
async def list_users(db: Session = Depends(get_db)):
return db.query(User).filter(User.tenant_id == request.state.tenant_id).all()
23. 扩展:全文搜索集成
23.1 PostgreSQL全文搜索
python复制from sqlalchemy import func
@app.get("/users/search")
async def search_users(q: str, db: Session = Depends(get_db)):
query = db.query(User).filter(
func.to_tsvector('english', User.name + ' ' + User.email)
.match(q, postgresql_regconfig='english')
)
return query.all()
23.2 Elasticsearch集成
python复制from elasticsearch import AsyncElasticsearch
es = AsyncElasticsearch("http://localhost:9200")
@app.on_event("shutdown")
async def shutdown():
await es.close()
@app.get("/users/search")
async def search_users(q: str):
body = {
"query": {
"multi_match": {
"query": q,
"fields": ["name", "email"]
}
}
}
response = await es.search(index="users", body=body)
return [hit["_source"] for hit in response["hits"]["hits"]]
24. 安全加固措施
24.1 SQL注入防护
虽然ORM通常能防止SQL注入,但仍需注意:
- 避免直接使用字符串拼接:
python复制# 错误
db.execute(f"SELECT * FROM users WHERE name = '{name}'")
# 正确
db.query(User).filter(User.name == name)
- 原始SQL使用参数化查询:
python复制# 正确
db.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})
24.2 敏感数据审计
记录数据变更:
python复制from sqlalchemy import event
@event.listens_for(User, 'after_update')
def receive_after_update(mapper, connection, target):
changes = {}
for attr in state.attrs:
hist = attr.load_history()
if hist.has_changes():
changes[attr.key] = {
"old": hist.deleted[0] if hist.deleted else None,
"new": hist.added[0] if hist.added else None
}
if changes:
audit_log = AuditLog(
table="users",
record_id=target.id,
action="update",
changes
