跳到主要内容

4.2 一对一关系

一对一其实是"戴了紧箍咒的一对多"。本节很短,重点是搞懂两处不同:unique=TrueMapped["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 多对多关系