1. 为什么需要关系型数据库操作
在Web开发中,数据关系处理是最基础也最关键的环节之一。FastAPI作为现代Python Web框架,配合SQLAlchemy这个强大的ORM工具,能够优雅地处理各种复杂的数据关系。我经历过太多因为数据关系处理不当导致的性能问题和逻辑错误,所以今天想系统分享下这方面的实战经验。
关系型数据库的核心价值在于通过表间关系保持数据一致性。以用户系统为例:
- 一对一:用户与身份证信息(一个用户对应唯一身份证)
- 一对多:用户与订单(一个用户有多个订单)
- 多对多:用户与角色(用户可以有多个角色,角色也属于多个用户)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础模型定义
2.1 最小化依赖安装
建议使用Poetry管理依赖:
bash复制poetry add fastapi sqlalchemy uvicorn
poetry add pydantic-settings # 用于配置管理
2.2 数据库连接配置
创建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"
# 生产环境建议使用:postgresql://user:password@postgresserver/db
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
3. 一对一关系实现
3.1 模型定义
典型场景:用户与用户档案
python复制from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True)
profile = relationship("Profile", back_populates="user", uselist=False) # 关键参数
class Profile(Base):
__tablename__ = "profiles"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
full_name = Column(String)
user = relationship("User", back_populates="profile")
3.2 操作要点
uselist=False明确声明一对一关系- 实际业务中建议添加
cascade="all, delete-orphan"实现级联删除 - 查询优化:默认懒加载,高频访问场景可配置
lazy="joined"
4. 一对多关系实战
4.1 博客系统案例
python复制class BlogUser(Base):
__tablename__ = "blog_users"
id = Column(Integer, primary_key=True)
name = Column(String)
posts = relationship("BlogPost", back_populates="author")
class BlogPost(Base):
__tablename__ = "blog_posts"
id = Column(Integer, primary_key=True)
title = Column(String)
author_id = Column(Integer, ForeignKey("blog_users.id"))
author = relationship("BlogUser", back_populates="posts")
4.2 性能优化技巧
- 分页查询避免N+1问题:
python复制# 错误做法:会引发N+1查询
users = db.query(BlogUser).all()
for user in users:
print(user.posts) # 每次循环都查询数据库
# 正确做法:使用joinedload
from sqlalchemy.orm import joinedload
users = db.query(BlogUser).options(joinedload(BlogUser.posts)).all()
- 批量插入优化:
python复制# 低效方式
for i in range(100):
post = BlogPost(title=f"Post {i}", author=user)
db.add(post)
# 高效方式
db.bulk_insert_mappings(
BlogPost,
[{"title": f"Post {i}", "author_id": user.id} for i in range(100)]
)
5. 多对多关系高级用法
5.1 关联表设计
学生选课系统示例:
python复制# 关联表(纯SQLAlchemy方式)
student_course = Table(
"student_course",
Base.metadata,
Column("student_id", Integer, ForeignKey("students.id")),
Column("course_id", Integer, ForeignKey("courses.id")),
)
class Student(Base):
__tablename__ = "students"
id = Column(Integer, primary_key=True)
courses = relationship("Course", secondary=student_course, back_populates="students")
class Course(Base):
__tablename__ = "courses"
id = Column(Integer, primary_key=True)
students = relationship("Student", secondary=student_course, back_populates="courses")
5.2 带额外字段的关联表
当关联表需要存储额外信息(如选课时间、成绩)时:
python复制class Enrollment(Base):
__tablename__ = "enrollments"
student_id = Column(Integer, ForeignKey("students.id"), primary_key=True)
course_id = Column(Integer, ForeignKey("courses.id"), primary_key=True)
enrolled_at = Column(DateTime, default=datetime.utcnow)
grade = Column(Float)
student = relationship("Student", back_populates="enrollments")
course = relationship("Course", back_populates="enrollments")
class Student(Base):
__tablename__ = "students"
id = Column(Integer, primary_key=True)
enrollments = relationship("Enrollment", back_populates="student")
class Course(Base):
__tablename__ = "courses"
id = Column(Integer, primary_key=True)
enrollments = relationship("Enrollment", back_populates="course")
6. FastAPI集成实践
6.1 依赖注入模式
python复制from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/")
def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = User(**user.dict())
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
6.2 关系数据返回处理
使用Pydantic模型处理嵌套关系:
python复制class ProfileBase(BaseModel):
full_name: str
class UserBase(BaseModel):
username: str
profile: ProfileBase | None = None
class Config:
orm_mode = True
@app.get("/users/{user_id}", response_model=UserBase)
def read_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).options(joinedload(User.profile)).get(user_id)
if not user:
raise HTTPException(status_code=404)
return user
7. 生产环境注意事项
- 连接池配置:
python复制engine = create_engine(
DATABASE_URL,
pool_size=20,
max_overflow=10,
pool_timeout=30,
pool_recycle=3600 # 1小时回收连接
)
- 事务管理最佳实践:
python复制# 错误示范:没有正确处理异常
def update_user():
db = SessionLocal()
user = db.query(User).first()
user.name = "new name"
db.commit() # 如果这里出错,连接不会关闭
# 正确做法:使用contextlib
from contextlib import contextmanager
@contextmanager
def get_db():
db = SessionLocal()
try:
yield db
db.commit()
except:
db.rollback()
raise
finally:
db.close()
- 索引优化建议:
python复制# 在一对多/多对多的外键上创建索引
class BlogPost(Base):
__tablename__ = "blog_posts"
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey("blog_users.id"), index=True) # 添加索引
在实际项目中,我发现很多团队容易忽视关系加载策略的配置。比如在返回JSON响应时,如果使用默认的懒加载策略,会导致大量额外的数据库查询。我的经验是:
- 在API层明确指定需要的关联数据
- 对高频访问的关系配置
lazy="joined" - 使用
selectinload替代joinedload处理深层嵌套关系
