Python基础入门 Day64:SQLModel 中的多对多关系映射

96次阅读
没有评论

共计 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)关联表 Enrollmentstudent_id+course_id 组成联合主键,天然避免重复选课;
2)Relationship(..., link_model=Enrollment) 指定了多对多使用的连接表;
3)可以在关联表上添加业务字段(如 gradeenrolled_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 设计与实现。

正文完
 0
评论(没有评论)