目录结构:
javascript
project/
├── main.py ← 唯一入口,挂载多个 APIRouter
├── database.py ← 引擎、Session、Base
├── requirements.txt
│
├── app_user_profile/ ← 一对一关系(User ↔ Profile)
│ ├── __init__.py
│ ├── models.py
│ ├── schemas.py
│ ├── crud.py
│ └── router.py
│
├── app_department_employee/ ← 一对多关系(Department → Employee)
│ ├── __init__.py
│ ├── models.py
│ ├── schemas.py
│ ├── crud.py
│ └── router.py
│
├── app_post_tag/ ← 多对多关系(Post ↔ Tag)
│ ├── __init__.py
│ ├── models.py
│ ├── schemas.py
│ ├── crud.py
│ └── router.py
└── alembic/ (可选,后面讲迁移时再建)
0. 公共文件
共享文件 database.py 这个文件定义了数据库引擎和会话,所有子应用共享它。
python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
DATABASE_URL = "sqlite:///./example.db"
# 创建数据库引擎
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False} # 只对 sqlite 需要
)
# 创建会话本地类
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# 创建基础类
class Base(DeclarativeBase):
pass
主文件:main.py
python
# main.py
from fastapi import FastAPI
from app_one_to_one.routers import router as one_to_one_router
from database import engine, Base
# 创建主 FastAPI 应用
app = FastAPI(title="FastAPI SQLAlchemy Relationships Tutorial")
# 创建所有表(在启动时运行一次)
Base.metadata.create_all(bind=engine)
# 挂载子应用路由,使用前缀区分
app.include_router(one_to_one_router, prefix="/one-to-one", tags=["One-to-One"])
# 运行命令:uvicorn main:app --reload
# 在 main.py 中统一管理,确保所有子应用通过一个端口运行,便于测试和部署。
1 一对一关系(用户与配置信息)
一对一关系示例:一个用户(User)只有一个配置文件(Profile),反之亦然。我们使用 app_one_to_one 子应用。
one_to_one/models.py 定义模型,使用 relationship 建立一对一关系。
python
# app_one_to_one/models.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
class User(Base):
__tablename__ = "users_one_to_one"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
# 正向关系:用户到配置文件(一对一)
# back_populates 指定反向关系的属性名,确保双向同步
# uselist=False 强制一对一(否则默认为一对多)
# cascade="all, delete-orphan":级联所有操作,并删除孤儿(如果配置文件从用户移除,它将被删除)
profile = relationship("Profile", back_populates="user", uselist=False, cascade="all, delete-orphan")
class Profile(Base):
__tablename__ = "profiles_one_to_one"
id = Column(Integer, primary_key=True, index=True)
bio = Column(String)
user_id = Column(Integer, ForeignKey("users_one_to_one.id"), unique=True) # 外键,确保一对一
# 反向关系:配置文件到用户
# back_populates 必须匹配正向的属性名
user = relationship("User", back_populates="profile")
在 app_one_to_one/models.py 中定义模型。注意:
- 使用
relationship定义关系。 back_populates用于双向同步:确保正向和反向关系一致。如果不配置,关系不会自动填充反向引用,可能导致查询不一致或错误。uselist=False指定一对一(默认是一对多)。cascade="all, delete-orphan":自动级联操作。"all" 包括 save-update, merge, refresh-expire 等;"delete-orphan" 表示如果子对象从父对象中移除,它将被删除(孤儿删除)。这有助于维护数据完整性,避免孤立记录。
在 app_one_to_one/routes.py 中定义路由与CRUD操作。所有操作使用依赖注入的 db 会话。
python
# app_one_to_one/routes.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from .models import User, Profile
router = APIRouter()
# 数据插入:创建用户和关联的配置文件
@router.post("/create-user-with-profile")
def create_user_with_profile(username: str, bio: str, db: Session = Depends(get_db)):
user = User(username=username)
profile = Profile(bio=bio)
user.profile = profile # 关联:正向设置
db.add(user) # 添加用户,会级联添加配置文件(由于 cascade)
db.commit()
db.refresh(user)
return {"user_id": user.id, "profile_id": profile.id}
# 更新:更新用户的配置文件
@router.put("/update-profile/{user_id}")
def update_profile(user_id: int, new_bio: str, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user or not user.profile:
raise HTTPException(status_code=404, detail="User or Profile not found")
user.profile.bio = new_bio # 正向更新
db.commit()
db.refresh(user)
return {"updated_bio": user.profile.bio}
# 正向查询:从用户查询配置文件
@router.get("/query-forward/{user_id}")
def query_forward(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
profile = user.profile # 正向访问
return {"username": user.username, "bio": profile.bio if profile else None}
# 反向查询:从配置文件查询用户
@router.get("/query-reverse/{profile_id}")
def query_reverse(profile_id: int, db: Session = Depends(get_db)):
profile = db.query(Profile).filter(Profile.id == profile_id).first()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
user = profile.user # 反向访问(依赖 back_populates)
return {"bio": profile.bio, "username": user.username}
# 数据删除:删除用户,会级联删除配置文件(由于 cascade)
@router.delete("/delete-user/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user) # 删除用户,级联删除 profile
db.commit()
return {"detail": "User and Profile deleted"}
2 一对多关系(部门与员工)
一对多关系示例:一个部门(Department)有多个员工(Employee),但一个员工只属于一个部门。我们使用 app_one_to_many 子应用。
app_one_to_many/models.py 定义模型,使用 relationship 建立一对多关系。
python
# app_one_to_many/models.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
class Department(Base):
__tablename__ = "departments_one_to_many"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True)
# 正向关系:部门到员工(一对多)
# back_populates 指定反向
# cascade="all, delete-orphan":级联所有操作,删除孤儿员工(如果从部门移除)
employees = relationship("Employee", back_populates="department", cascade="all, delete-orphan")
class Employee(Base):
__tablename__ = "employees_one_to_many"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
department_id = Column(Integer, ForeignKey("departments_one_to_many.id")) # 外键
# 反向关系:员工到部门(多对一)
# back_populates 匹配正向
department = relationship("Department", back_populates="employees")
注意:
- 正向:部门到员工(一对多,使用 relationship 默认 uselist=True)。
- 反向:员工到部门(多对一)。
- back_populates 确保双向一致。
- cascade="all, delete-orphan" 在一对多中特别有用:删除部门时,所有员工被删除;如果员工从部门移除,它将成为孤儿并被删除。
路由和CRUD操作(routes.py)
python
# app_one_to_many/routes.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from .models import Department, Employee
router = APIRouter()
# 数据插入:创建部门和多个员工
@router.post("/create-department-with-employees")
def create_department_with_employees(name: str, employee_names: list[str], db: Session = Depends(get_db)):
dept = Department(name=name)
for emp_name in employee_names:
emp = Employee(name=emp_name)
dept.employees.append(emp) # 正向添加
db.add(dept) # 添加部门,级联添加员工
db.commit()
db.refresh(dept)
return {"dept_id": dept.id, "employee_ids": [e.id for e in dept.employees]}
# 更新:更新员工的部门(转移员工)
@router.put("/update-employee-department/{employee_id}")
def update_employee_department(employee_id: int, new_dept_id: int, db: Session = Depends(get_db)):
emp = db.query(Employee).filter(Employee.id == employee_id).first()
new_dept = db.query(Department).filter(Department.id == new_dept_id).first()
if not emp or not new_dept:
raise HTTPException(status_code=404, detail="Not found")
emp.department = new_dept # 反向更新(会自动从旧部门移除,由于 back_populates)
db.commit()
db.refresh(emp)
return {"employee_name": emp.name, "new_dept_name": new_dept.name}
# 正向查询:从部门查询员工
@router.get("/query-forward/{dept_id}")
def query_forward(dept_id: int, db: Session = Depends(get_db)):
dept = db.query(Department).filter(Department.id == dept_id).first()
if not dept:
raise HTTPException(status_code=404, detail="Department not found")
employees = dept.employees # 正向访问列表
return {"dept_name": dept.name, "employees": [e.name for e in employees]}
# 反向查询:从员工查询部门
@router.get("/query-reverse/{employee_id}")
def query_reverse(employee_id: int, db: Session = Depends(get_db)):
emp = db.query(Employee).filter(Employee.id == employee_id).first()
if not emp:
raise HTTPException(status_code=404, detail="Employee not found")
dept = emp.department # 反向访问
return {"employee_name": emp.name, "dept_name": dept.name if dept else None}
# 数据删除:删除部门,会级联删除所有员工
@router.delete("/delete-department/{dept_id}")
def delete_department(dept_id: int, db: Session = Depends(get_db)):
dept = db.query(Department).filter(Department.id == dept_id).first()
if not dept:
raise HTTPException(status_code=404, detail="Department not found")
db.delete(dept) # 删除部门,级联删除员工
db.commit()
return {"detail": "Department and Employees deleted"}
# 额外:移除员工(会触发 delete-orphan,如果不重新分配)
@router.delete("/remove-employee-from-dept/{employee_id}")
def remove_employee_from_dept(employee_id: int, db: Session = Depends(get_db)):
emp = db.query(Employee).filter(Employee.id == employee_id).first()
if not emp or not emp.department:
raise HTTPException(status_code=404, detail="Not found")
emp.department.employees.remove(emp) # 从部门移除,成为孤儿并删除
db.commit()
return {"detail": "Employee removed and deleted (orphan)"}
如果想在移除员工时不删除员工本身,只是解除与部门的关联,则可以:
python
# 移除员工与部门的关联(不删除员工)
@router.delete("/remove-employee-from-dept/{employee_id}")
def remove_employee_from_dept(employee_id: int, db: Session = Depends(get_db)):
"""
将员工从部门中移除(解除关联),但不删除员工
方法:将员工的 department_id 设置为 None
"""
emp = db.query(Employee).filter(Employee.id == employee_id).first()
if not emp:
raise HTTPException(status_code=404, detail="员工不存在")
if not emp.department:
raise HTTPException(status_code=400, detail="该员工未分配部门")
# 方法1:直接设置外键为 None(推荐)
emp.department_id = None
# 方法2(效果相同):
# emp.department = None
db.commit()
db.refresh(emp)
return {
"detail": "员工已从部门移除(员工未删除)",
"employee_id": emp.id,
"employee_name": emp.name,
"department_id": None
}
3 多对多关系(学生与课程)
多对多关系示例:学生(Student)可以选多门课程(Course),课程也可以有多个学生。我们使用关联表(enrollments)。
模型创建(models.py)
python
# app_many_to_many/models.py
from sqlalchemy import Column, Integer, String, ForeignKey, Table
from sqlalchemy.orm import relationship
from database import Base
# 对于多对多关系,需要一个中间关联表
# 关联表:多对多中间表
enrollments = Table(
"enrollments_many_to_many",
Base.metadata,
Column("student_id", Integer, ForeignKey("students_many_to_many.id"), primary_key=True),
Column("course_id", Integer, ForeignKey("courses_many_to_many.id"), primary_key=True)
)
class Student(Base):
__tablename__ = "students_many_to_many"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
# 正向关系:学生到课程(多对多)
# secondary 指定关联表
# back_populates 指定反向
# 多对多关系下,cascade 通常不设置,因为关联表不存储额外数据
courses = relationship("Course", secondary=enrollments, back_populates="students")
class Course(Base):
__tablename__ = "courses_many_to_many"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
# 反向关系:课程到学生
# secondary 相同
# back_populates 匹配正向
students = relationship("Student", secondary=enrollments, back_populates="courses")
注意:
- 多对多需要中间关联表(Enrollment)。
- 使用 relationship 与 secondary 指定关联表。
- back_populates 在两侧配置,确保双向。
- cascade="all, delete-orphan" 可以应用到关联,但这里我们不强制(取决于需求;如果删除学生,不一定删除课程)。示例中未使用 cascade 以避免意外删除交叉引用,但你可以添加以级联删除关联记录。
路由和CRUD操作(routes.py)
python
# app_many_to_many/routes.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from .models import Student, Course, enrollments
router = APIRouter()
# 数据插入:创建学生和课程,并关联
@router.post("/create-student-with-courses")
def create_student_with_courses(student_name: str, course_titles: list[str], db: Session = Depends(get_db)):
student = Student(name=student_name)
for title in course_titles:
course = Course(title=title)
student.courses.append(course) # 正向添加(会自动插入关联表)
db.add(course) # 需要单独添加课程,因为没有 cascade
db.add(student)
db.commit()
db.refresh(student)
return {"student_id": student.id, "course_ids": [c.id for c in student.courses]}
# 更新:为学生添加/移除课程
@router.put("/update-student-courses/{student_id}")
def update_student_courses(student_id: int, add_course_id: int = None, remove_course_id: int = None, db: Session = Depends(get_db)):
student = db.query(Student).filter(Student.id == student_id).first()
if not student:
raise HTTPException(status_code=404, detail="Student not found")
if add_course_id:
course = db.query(Course).filter(Course.id == add_course_id).first()
if course and course not in student.courses:
student.courses.append(course) # 添加
if remove_course_id:
course = db.query(Course).filter(Course.id == remove_course_id).first()
if course and course in student.courses:
student.courses.remove(course) # 移除(仅删除关联,不删课程本身)
db.commit()
db.refresh(student)
return {"student_name": student.name, "courses": [c.title for c in student.courses]}
# 正向查询:从学生查询课程
@router.get("/query-forward/{student_id}")
def query_forward(student_id: int, db: Session = Depends(get_db)):
student = db.query(Student).filter(Student.id == student_id).first()
if not student:
raise HTTPException(status_code=404, detail="Student not found")
courses = student.courses # 正向访问列表
return {"student_name": student.name, "courses": [c.title for c in courses]}
# 反向查询:从课程查询学生
@router.get("/query-reverse/{course_id}")
def query_reverse(course_id: int, db: Session = Depends(get_db)):
course = db.query(Course).filter(Course.id == course_id).first()
if not course:
raise HTTPException(status_code=404, detail="Course not found")
students = course.students # 反向访问
return {"course_title": course.title, "students": [s.name for s in students]}
# 数据删除:删除学生,会删除其关联但不删课程
@router.delete("/delete-student/{student_id}")
def delete_student(student_id: int, db: Session = Depends(get_db)):
student = db.query(Student).filter(Student.id == student_id).first()
if not student:
raise HTTPException(status_code=404, detail="Student not found")
# 先清空课程关联(否则外键约束可能出错)
student.courses.clear()
db.delete(student)
db.commit()
return {"detail": "Student deleted, associations removed"}
# 额外:删除课程,类似处理
@router.delete("/delete-course/{course_id}")
def delete_course(course_id: int, db: Session = Depends(get_db)):
course = db.query(Course).filter(Course.id == course_id).first()
if not course:
raise HTTPException(status_code=404, detail="Course not found")
course.students.clear()
db.delete(course)
db.commit()
return {"detail": "Course deleted, associations removed"}