- 关系:使用
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,Team
和Hero
之间存在一种关系。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
的一对多关系中:
- **
Hero
模型中必须有team_id
外键字段**(映射到数据库列)。 - **
Team
模型中可以有heroes
关系属性**(纯 ORM 概念,无数据库列)。 - 这种设计是由关系数据库的基础原理和 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
对象时:
- 你只设置了它的 物理字段 (
name
,headquarters
)。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.程序结束或异常发生
当你的程序运行结束,或者发生未处理的异常导致程序中断时,会话也会被关闭。