fastapi+sqlalchemy实现一对一、一对多、多对多关系数据操作

目录结构:

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"}
相关推荐
龙腾AI白云6 小时前
【基于Transformer的人工智能模型搭建与fine-tuning】
scikit-learn·fastapi
叼奶嘴的超人1 天前
手动创建Docker版Fastapi CI/CD镜像文件
ci/cd·docker·fastapi
regret~1 天前
【笔记】Ant Design+FastAPI 项目 Linux 服务器内网部署完整笔记
服务器·笔记·fastapi
regret~1 天前
【笔记】Ant Design(含Umi Max)+FastAPI 内网部署&接口代理 核心笔记
笔记·fastapi
全栈测试笔记2 天前
FastAPI系列(12):响应模型参数
开发语言·python·fastapi
叼奶嘴的超人2 天前
Fastapi之UV安装方式与使用方式
fastapi·uv
强化试剂瓶3 天前
Silane-PEG8-DBCO,硅烷-聚乙二醇8-二苯并环辛炔技术应用全解析
python·flask·numpy·pyqt·fastapi
曲幽3 天前
FastAPI日志实战:从踩坑到优雅配置,让你的应用会“说话”
python·logging·fastapi·web·error·log·info
布局呆星4 天前
FastAPI:高性能Python Web框架
fastapi