4.2 一对一关系
一对一其实是"戴了紧箍咒的一对多"。本节很短,重点是搞懂两处不同:
unique=True和Mapped["X"](不带 list)。
一、什么时候用一对一?
一个用户只有一份"详细资料"(简介、生日、头像等)。为什么不直接把这些字段放 users 表里?常见理由:
- 主表瘦身:users 表频繁查询(登录、列表页),把低频的大字段拆出去,主表更快
- 信息分级:敏感信息(实名、身份证号)单独一张表,方便单独控制权限
- 可选信息:很多用户根本没填资料,拆表避免主表一堆 NULL
users 表 profiles 表
┌────┬───────┐ ┌────┬──────────┬─────────┐
│ id │ name │ │ id │ bio │ user_id │ ← 外键 + 唯一约束
├────┼───────┤ ├────┼──────────┼─────────┤
│ 1 │ 张三 │ ←────────── │ 1 │ 后端工程师│ 1 │
│ 2 │ 李四 │ ←────────── │ 2 │ 设计师 │ 2 │
└────┴───────┘ └────┴──────────┴─────────┘
二、实现:外键 + unique + 单数类型注解
from typing import Optional
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
# 注意:Mapped["Profile"] 不是 list —— 一对一!
profile: Mapped[Optional["Profile"]] = relationship(
back_populates="user",
cascade="all, delete-orphan",
)
class Profile(Base):
__tablename__ = "profiles"
id: Mapped[int] = mapped_column(primary_key=True)
bio: Mapped[Optional[str]] = mapped_column(String(500))
birthday: Mapped[Optional[str]] = mapped_column(String(10))
# unique=True 是一对一的关键:一个 user_id 只能出现一次
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
user: Mapped["User"] = relationship(back_populates="profile")
和一对多相比只有两处不同:
| 一对多 | 一对一 | |
|---|---|---|
| 外键列 | ForeignKey("users.id") | ForeignKey("users.id"), unique=True |
| "一"方的类型注解 | Mapped[list["Post"]] | Mapped[Optional["Profile"]] |
原理:
unique=True从数据库层面保证一个用户最多只有一份资料(想插第二份直接报 IntegrityError)Mapped[Optional["Profile"]](单数、可空)让 SQLAlchemy 知道这侧是"一个对象"而非"列表"——user.profile返回 Profile 或 None,而不是 list
📌 在 2.0 里,类型注解写成单数就足以让 SQLAlchemy 识别为一对一,不再需要老教程里的
uselist=False参数(写了也不算错)。
三、使用
with SessionLocal() as session:
# 创建用户和资料
user = User(name="张三")
user.profile = Profile(bio="后端工程师,SQLAlchemy 爱好者", birthday="1999-01-01")
session.add(user)
session.commit()
# 正向访问
print(user.profile.bio) # 后端工程师,SQLAlchemy 爱好者
# 反向访问
profile = session.get(Profile, 1)
print(profile.user.name) # 张三
# 没资料的用户
lisi = User(name="李四")
session.add(lisi)
session.commit()
print(lisi.profile) # None ← 所以类型是 Optional
# 替换资料:因为配了 delete-orphan,旧资料自动删除
user.profile = Profile(bio="全栈工程师")
session.commit()
四、查询
from sqlalchemy import select
# 查有资料的用户
stmt = select(User).where(User.profile.has())
# 查简介里带"工程师"的用户
stmt = select(User).where(User.profile.has(Profile.bio.contains("工程师")))
💡 记忆:关系是列表(一对多)用
.any(条件);关系是单个对象(一对一/多对一)用.has(条件)。
📝 本节小结
- 一对一 = 一对多 + 外键加
unique=True+ "一"方注解用单数Mapped[Optional["X"]] - 适用场景:主表瘦身、敏感信息隔离、大量可选字段
- 单对象关系过滤用
.has(),列表关系用.any()
下一节是关系篇的重头戏 → 4.3 多对多关系