共计 3681 个字符,预计需要花费 10 分钟才能阅读完成。
在真实业务中,学生选课、用户与角色、文章与标签等都属于“多对多”关系。用 SQLModel 建模多对多时,通常通过一张“关联表(link/association table)”把两张主表连接起来,还可以在关联表上记录额外信息(如选课成绩、创建时间等)。下面以“学生 - 课程 - 选课”为例完成从建模到查询的完整流程。
一、数据模型设计(包含关联载荷)
from __future__ import annotations
from typing import List, Optional
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship
# 关联表:既充当连接表,又可保存额外字段(成绩、时间)class Enrollment(SQLModel, table=True):
student_id: Optional[int] = Field(default=None, foreign_key="student.id", primary_key=True)
course_id: Optional[int] = Field(default=None, foreign_key="course.id", primary_key=True)
grade: Optional[str] = None
enrolled_at: datetime = Field(default_factory=datetime.utcnow)
class Student(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
# 多对多:通过 link_model=Enrollment 建立到 Course 的关系
courses: List["Course"] = Relationship(back_populates="students", link_model=Enrollment)
class Course(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str
students: List[Student] = Relationship(back_populates="courses", link_model=Enrollment)
要点说明:
1)关联表 Enrollment 以 student_id+course_id 组成联合主键,天然避免重复选课;
2)Relationship(..., link_model=Enrollment) 指定了多对多使用的连接表;
3)可以在关联表上添加业务字段(如 grade、enrolled_at)。
二、初始化数据库与建表
from sqlmodel import create_engine
engine = create_engine("sqlite:///./school.db", echo=False)
SQLModel.metadata.create_all(engine)
三、插入数据与建立多对多关联
方式 A(显式插入关联表,适合需要写入额外字段的场景):
from sqlmodel import Session
with Session(engine) as session:
alice = Student(name="Alice")
bob = Student(name="Bob")
py101 = Course(title="Python 101")
db101 = Course(title="Database 101")
session.add_all([alice, bob, py101, db101])
session.commit()
session.refresh(alice); session.refresh(bob); session.refresh(py101); session.refresh(db101)
# 建立关联并写入成绩
session.add(Enrollment(student_id=alice.id, course_id=py101.id, grade="A"))
session.add(Enrollment(student_id=alice.id, course_id=db101.id, grade="B+"))
session.add(Enrollment(student_id=bob.id, course_id=py101.id, grade="A-"))
session.commit()
方式 B(只建立关系不写额外字段时,可直接维护关系列表,需额外创建 Enrollment 时再补充字段,这里演示方式 A 更通用)。
四、查询:预加载避免 N+1
from sqlmodel import Session, select
from sqlalchemy.orm import selectinload
# 查询学生与其课程
with Session(engine) as session:
stmt = select(Student).options(selectinload(Student.courses))
for stu in session.exec(stmt):
print(stu.name, ">>", [c.title for c in stu.courses])
# 查询课程与已选学生
with Session(engine) as session:
stmt = select(Course).options(selectinload(Course.students))
for c in session.exec(stmt):
print(c.title, ">>", [s.name for s in c.students])
五、读取或更新关联表上的“载荷”字段
当需要读取 / 更新成绩时,可直接操作 Enrollment:
from sqlalchemy import and_
with Session(engine) as session:
# 查找 Alice 在 Python 101 的选课记录
link = session.get(
Enrollment,
(1, 1) # (student_id, course_id) —— 这里仅举例,实际请用变量 alice.id、py101.id
)
# 或者用条件查询(更直观)# link = session.exec(# select(Enrollment).where(# and_(Enrollment.student_id == alice.id, Enrollment.course_id == py101.id)
# )
# ).first()
if link:
print(" 原成绩:", link.grade)
link.grade = "A+"
session.add(link)
session.commit()
六、删除关系与数据一致性
删除“选课关系”而不删学生 / 课程:
with Session(engine) as session:
link = session.exec(select(Enrollment).where(and_(Enrollment.student_id == alice.id, Enrollment.course_id == db101.id)
)
).first()
if link:
session.delete(link)
session.commit()
如需级联删除(删除学生时一并清理选课关系),可在实际生产中结合数据库约束或在业务层先删关联再删主记录。
七、分页与条件筛选(多对多联合场景)
def list_students_in_course(course_title: str, page: int = 1, page_size: int = 10):
offset = (page - 1) * page_size
with Session(engine) as session:
# 先按课程名过滤,再预加载学生列表
course_stmt = select(Course).where(Course.title == course_title).options(selectinload(Course.students)
)
course = session.exec(course_stmt).first()
if not course:
return []
# 简单分页(对已加载列表切片)return course.students[offset: offset + page_size]
八、最佳实践小结
1)多对多优先使用“显式关联表”,便于扩展业务字段与约束;
2)关联表采用联合主键或唯一组合约束,避免重复记录;
3)查询多对多关系时使用 selectinload/joinedload 预加载,提升性能;
4)对关联关系的新增 / 删除,统一封装成服务层方法,保证一致性与可维护性。
到这里,你已经掌握了 SQLModel 中多对多关系的建模、增删改查与性能要点。下一节将实战整合 FastAPI + SQLModel,多对多关系下的 REST API 设计与实现。