SQLmodel 入门 (二)关系补充

  • 关系​:使用 Relationship 和 foreign_key 来定义表之间的关联。
  • Team → Hero​ (一对多): 一个团队有多个英雄 (team.heroes)
  • Hero → Team​ (多对一): 一个英雄属于一个团队 (hero.team)

定义关系:

from typing import List, Optional
from sqlmodel import Field, SQLModel, Relationship

class Team(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    headquarters: str

    # 定义关系:一个 Team 拥有多个 Hero
    heroes: List["Hero"] = Relationship(back_populates="team")

class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    secret_name: str
    age: Optional[int] = Field(default=None, nullable=True)

    # 外键,指向 Team 表的主键 id
    team_id: Optional[int] = Field(default=None, foreign_key="team.id")
    # 定义关系:一个 Hero 属于一个 Team
    team: Optional[Team] = Relationship(back_populates="heroes")

1.代码解析:

  • heroes: List["Hero"] = Relationship(back_populates="team")

这行代码位于Team模型内部,它定义了一个指向Hero模型的关系。

  • heroes: List["Hero"]:这表示一个Team实例将有一个名为heroes的属性,该属性是一个Hero对象的列表。也就是说,一个团队可以拥有多个英雄。使用引号 "Hero"(称为前向引用)是因为此时 Hero 类可能尚未完全定义。
  • Relationship:这是SQLModel中用于定义模型间关系的函数。它告诉SQLModel,TeamHero之间存在一种关系。
  • back_populates="team":这是建立双向关系的关键。它表示在Hero模型中也有一个对应的关系字段(名为team),并且这两个关系是相互关联的。当你在一个团队中添加英雄,或者在英雄中设置团队时,SQLModel会自动保持两者的一致性。
  • Hero模型中,应该有如下对应的定义:
  • class Hero(SQLModel, table=True):
        # ... 其他字段 ...
        team_id: Optional[int] = Field(default=None, foreign_key="team.id")
        team: Optional["Team"] = Relationship(back_populates="heroes")
  • 这样,两个模型之间就建立了双向的一对多关系(一个团队有多个英雄,一个英雄属于一个团队)

2.问题:

  • 定义Team 时没有一个叫做 hero_id 的外键,只有一个heroes ,是因为Team与 heroes是 一对多,而 Hero 中 必须有一个叫做 team_id 的外键和一个 team 字段?
  • team_id是数据库列,而herose和team 字段都不是数据库列?

是的,在 Team 和 Hero 的一对多关系中:​

  1. ​**Hero 模型中必须有 team_id 外键字段**​(映射到数据库列)。
  2. ​**Team 模型中可以有 heroes 关系属性**​(纯 ORM 概念,无数据库列)。
  3. 这种设计是由关系数据库的基础原理和 ​ORM 的面向对象抽象共同决定的。

关系数据库的基础原理:

  • 数据库只认识 ​表 (table)​行 (row / record)​​ 和 ​列 (column)​
  • 它通过 ​外键约束 (FOREIGN KEY CONSTRAINT)​​ 来维护表之间的关系(参照完整性)。
  • 因此,​**team_id 是必需的真实列**,因为它承载了物理上的外键关系。
  • ​**heroes 和 team 不能是数据库列**​:
    • team 表中的一行代表一个团队,无法容纳多个 hero_id(这违背了第一范式 1NF)。
    • hero 表中的一行代表一个英雄,team_id 已经足以关联其团队,不需要再冗余存储一个团队对象(team 属性)。

ORM 的面向对象抽象:

SQLModel/SQLAlchemy ORM 的核心目标之一,是让开发者能够用面向对象的方式操作关系数据库,而无需过多关注底层的 SQL 细节。

你创建 Team 对象时​:

  • 你只设置了它的 ​物理字段​ (nameheadquarters)。heroes 关系属性在 ORM 内部被初始化为一个特殊的、空的集合代理对象(不是真正的空列表)。
  • Team.heroes 这不是一个数据库字段!​​ Relationship 声明告诉 ORM:“我知道 hero 表里有个 team_id 外键指向 team.id。请给我提供一个便捷的途径,让一个 Team 对象能直接访问到所有关联的 Hero 对象。”

Hero.team 这也不是一个数据库字段!​​ 它告诉 ORM:“我知道本对象 (Hero) 的 team_id 字段指向 team 表的 id。请给我提供一个便捷的途径,让我能直接访问这个 Hero 所属的 Team 对象。”

使用关系:​

def create_heroes_with_teams():
    with Session(engine) as session:
        # 创建团队
        team_z_force = Team(name="Z-Force", headquarters="Sister Margaret's Bar")
        team_preventers = Team(name="Preventers", headquarters="Sharp Tower")

        # 创建英雄并关联团队
        hero_deadpond = Hero(name="Deadpond", secret_name="Dive Wilson", team=team_z_force)
        hero_rusty_man = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48, team=team_preventers)
        hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador", team=team_preventers)

        session.add(hero_deadpond)
        session.add(hero_rusty_man)
        session.add(hero_spider_boy)
        session.commit()

        # 查询时可以使用关系
        # 例如:查询一个团队的所有英雄
        statement = select(Team).where(Team.name == "Preventers")
        preventers_team = session.exec(statement).first()
        if preventers_team:
            print(f"Team: {preventers_team.name}")
            for hero in preventers_team.heroes: # 直接通过关系属性访问
                print(f"  Hero: {hero.name}")

1.Hero.team 不是数据库列 但是创建英雄时必须关联团队,通过 team=team_preventers 这样的方式赋值?

2.为什么添加team对象时没有设置heroes,但是可以通过preventers_team.heroes获取heroes?

  • 在SQLModel(以及SQLAlchemy)中,关系字段(如heroes)通常被定义为“反向关系”。这意味着它们并不直接对应数据库中的列,而是通过ORM动态生成的。
  • 当我们创建一个新的Team对象时,我们可能并没有初始化heroes属性,因为它是一个关系字段,而不是数据库列。但是,当我们通过Session将对象添加到数据库并随后进行查询时,ORM会自动处理这些关系。
  • 在创建英雄时,我们通过team=team_preventers将英雄与团队关联起来。这意味着在Hero表中,这些英雄记录的team_id字段会被设置为team_preventers的ID。
  • 然后访问preventers_team.heroes,SQLModel会自动执行一个查询来获取所有team_id等于preventers_team.id的英雄对象。这就是ORM的惰性加载(默认行为)——当你访问关系属性时,ORM会自动从数据库中加载相关对象。
  • 所以,即使我们在创建团队时没有显式地给heroes赋值,ORM通过我们在英雄对象上设置的团队关系(即设置了外键team_id)以及定义的双向关系(back_populates),能够自动填充heroes列表。

延迟加载功能

注意事项:

  • 这种延迟加载意味着每次访问 team.heroes 都可能触发数据库查询。在高性能场景中,你可能需要使用预先加载​(Eager Loading)来一次性获取所有相关数据。
  • 如果会话(Session)已经关闭,尝试访问关系属性会引发错误,因为无法执行新的查询。

预先加载功能:

这是一种高级技巧,在查询主对象的同时,通过一条 SQL 语句(使用 JOIN)将其关联的对象也一并查询出来,避免多次往返数据库。这在 SQLModel 中通常通过 select 语句的 .options() 方法实现。

from sqlmodel import select
from sqlalchemy.orm import selectinload

with Session(engine) as session:
    # 使用 selectinload 一次性加载团队及其所有英雄
    statement = select(Team).where(Team.id == 1).options(selectinload(Team.heroes))
    team = session.exec(statement).first()

    # 因为 heroes 已经被预先加载了,所以访问它不会触发新的查询
    print(team.heroes) 

# 即使会话关闭,因为数据已经完全加载到内存中,
# team.heroes 列表仍然可以访问(但你不能通过它再加载新的关联对象)。

会话关闭

1.显式关闭(最常见、最推荐)​
使用 Python 的 with 语句(上下文管理器)是管理会话生命周期的最佳方式。​当 with 代码块执行结束时,会话会自动关闭。​

2.手动关闭
你也可以手动调用 .close() 方法。

session = Session(engine) # 创建会话
# ... 一些操作 ...
session.close() # 手动关闭会话

3.程序结束或异常发生
当你的程序运行结束,或者发生未处理的异常导致程序中断时,会话也会被关闭。