一对一关系

典型场景:一个用户有一个用户详情(扩展信息)。

一、场景说明

用户表 (users)                     用户详情表 (user_profiles)
┌────┬──────┐                     ┌────┬─────────┬───────┬─────────┐
│ id │ name │                     │ id │ user_id │ phone │ address │
├────┼──────┤                     ├────┼─────────┼───────┼─────────┤
│  1 │ 张三 │  ────── 1:1 ──────  │  1 │    1    │ 138.. │ 北京    │
│  2 │ 李四 │                     │  2 │    2    │ 139.. │ 上海    │
└────┴──────┘                     └────┴─────────┴───────┴─────────┘
  • 一个用户只有一份详情(一对一)
  • 一份详情只属于一个用户(一对一)
  • user_id 是外键,指向 users.id,并且加了 unique 约束

二、和一对多的区别

只有一处不同:uselist=False

# 一对多:返回列表(一个用户有多篇文章)
articles = relationship("Article", back_populates="author")
# user.articles → [<Article>, <Article>, ...]

# 一对一:返回单个对象(一个用户只有一份详情)
profile = relationship("UserProfile", back_populates="user", uselist=False)
# user.profile → <UserProfile> 或 None

另外,外键字段加 unique=True,保证数据库层面也是一对一:

user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)

三、定义模型

from sqlalchemy import Column, Integer, String, ForeignKey, Text
from sqlalchemy.orm import relationship
from database import Base


class User(Base):
    __tablename__ = "users"

    id   = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)

    # uselist=False:一对一,返回单个对象而不是列表
    profile = relationship("UserProfile", back_populates="user", uselist=False)


class UserProfile(Base):
    __tablename__ = "user_profiles"

    id      = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
    phone   = Column(String(20))
    address = Column(Text)

    user = relationship("User", back_populates="profile")

四、逐行解释

1. ForeignKey + unique

user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
  • ForeignKey("users.id") — 外键,指向 users 表
  • unique=True — 唯一约束,保证一个 user_id 只出现一次(数据库层面的一对一)
  • nullable=False — 不允许为空

2. uselist=False

profile = relationship("UserProfile", back_populates="user", uselist=False)
  • uselist=False — 不返回列表,返回单个对象
  • 没有这个参数的话,user.profile 返回的是 [<UserProfile>] 列表
  • 加了之后,user.profile 返回的是 <UserProfile> 对象或 None

五、完整示例

5.1 建表并插入数据

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Text, select
from sqlalchemy.orm import sessionmaker, DeclarativeBase, relationship, joinedload


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"
    id      = Column(Integer, primary_key=True)
    name    = Column(String(50), nullable=False)
    profile = relationship("UserProfile", back_populates="user", uselist=False)


class UserProfile(Base):
    __tablename__ = "user_profiles"
    id      = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
    phone   = Column(String(20))
    address = Column(Text)
    user    = relationship("User", back_populates="profile")


engine = create_engine("sqlite:///./test.db", connect_args={"check_same_thread": False})
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
db = Session()


# ---------- 创建用户 ----------
user1 = User(name="张三")
user2 = User(name="李四")
db.add_all([user1, user2])
db.commit()
db.refresh(user1)
db.refresh(user2)

# ---------- 创建用户详情 ----------
profile1 = UserProfile(user_id=user1.id, phone="13800138000", address="北京市朝阳区")
profile2 = UserProfile(user_id=user2.id, phone="13900139000", address="上海市浦东区")
db.add_all([profile1, profile2])
db.commit()

5.2 正向查询:用户 → 详情

user = db.execute(select(User).where(User.id == 1)).scalars().first()

print(user.name)             # 张三
print(user.profile)          # <UserProfile(id=1, phone='13800138000')>
print(user.profile.phone)    # 13800138000
print(user.profile.address)  # 北京市朝阳区

如果没有详情:

user = User(name="新用户")
db.add(user)
db.commit()
db.refresh(user)

print(user.profile)  # None(不是空列表,因为 uselist=False)

5.3 反向查询:详情 → 用户

profile = db.execute(select(UserProfile).where(UserProfile.id == 1)).scalars().first()

print(profile.phone)      # 13800138000
print(profile.user.name)  # 张三

5.4 预加载(一条 SQL 查完)

# 没有预加载:两条 SQL
user = db.execute(select(User).where(User.id == 1)).scalars().first()
print(user.profile)  # 触发第二条 SQL

# 有预加载:一条 SQL(LEFT JOIN)
stmt = select(User).options(joinedload(User.profile))
user = db.execute(stmt).scalars().first()
print(user.profile)  # 不触发新 SQL

5.5 通过 relationship 创建详情

# 方式一:手动写 user_id
profile = UserProfile(user_id=user.id, phone="新号码", address="新地址")
db.add(profile)
db.commit()

# 方式二:直接赋值 profile 属性
user.profile = UserProfile(phone="新号码", address="新地址")
db.commit()
# SQLAlchemy 自动设置 user_id

5.6 更新详情

user = db.execute(select(User).where(User.id == 1)).scalars().first()

# 直接改属性
user.profile.phone = "13900139000"
db.commit()

# 没有详情时先创建
if user.profile is None:
    user.profile = UserProfile(phone="新号码", address="新地址")
    db.commit()

六、什么时候用一对一

场景说明
用户 + 用户详情核心信息和扩展信息分开
用户 + 用户配置每个用户一份配置
订单 + 订单物流一个订单一份物流信息
文章 + 文章统计一个文章一份统计数据

核心判断标准:这个信息是不是只有一份? 是的话用一对一,可能有多份的话用一对多。