第10章:SQLAlchemy关系映射——一对一与一对多
一、项目背景
“为什么订单列表加载这么慢?一共 20 条订单,SQL 日志里却有 41 条 SELECT 语句!”
星云电商的订单列表页性能问题已经持续了三个月。用户打开"我的订单"页面,需要等待 3-5 秒才能看到 20 条订单记录。DBA 抓了慢查询日志后发现:页面先执行了一条 SELECT * FROM orders WHERE user_id = 1 LIMIT 20,然后对每条订单又分别执行了一条 SELECT * FROM users WHERE id = ? 来查用户名。这就是经典的 N+1 查询问题(第 18 章会详细展开)。
而根本原因并不是懒加载问题——而是团队在初期根本没有使用 relationship() 映射,所有关联查询都是通过手写 SQL JOIN 完成的。因为不同开发人员手写 JOIN 时对表的关联关系和索引各不相同,最终每个接口的 JOIN 逻辑都不一致,维护成本极高。
另一个更严重的事故是数据完整性问题。一个订单因为在创建时没有检查用户是否存在,user_id 字段被写入了 99999(不存在的用户 ID),导致后续的统计报表中出现了大量"孤儿订单"。如果模型声明时配置了 ForeignKey 约束,数据库层就直接拦截了这种写入。
本章从关系映射的最基础用法开始——ForeignKey 约束、relationship() 导航属性、back_populates 双向关联、一对多(User.orders / Order.user)和一对一(User.profile)的声明与使用,并实战演示"创建订单并级联写入明细"的完整流程。
二、项目设计
场景:周五上午,大师约了小胖小白一起喝咖啡,讨论最近订单系统的重构方案。
大师:“小胖,你知道为什么订单列表慢吗?”
小胖:“我猜是没加索引?”
大师:“索引是原因之一,但更根本的原因是我们的代码里没有一个清晰的’关系模型’。你看这段代码:”
# 目前的写法:手写 JOIN
orders = conn.execute(text(
"SELECT o.*, u.username FROM orders o "
"JOIN users u ON o.user_id = u.id "
"WHERE o.user_id = :uid"
), {"uid": user_id}).fetchall()
大师:“每写一个新查询都要手写 JOIN。如果换了个人写,可能 JOIN 条件就是 o.user_id = u.user_id,或者忘了 LEFT JOIN 导致数据丢失。”
小胖:“我以前以为 ORM 的关系映射很复杂,所以一直没敢用……”
大师:“其实它比你想象的简单。关系映射的核心就两个概念:ForeignKey 和 relationship()。Foreign Key 是数据库层的约束(保证引用的行真实存在),relationship 是 Python 层的导航属性(让你可以用 order.user 而不是手写 JOIN 来访问关联对象)。”
小白:“技术映射:ForeignKey = 数据库层的身份证校验;relationship = Python 层的导航快捷方式。”
大师:“来看一个最简单的一对多例子:”
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
# 一对多:一个用户有多条订单
orders: Mapped[list["Order"]] = relationship(back_populates="user")
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
order_no: Mapped[str] = mapped_column(String(32))
# 外键:指向 users 表的主键
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
# 多对一:一条订单属于一个用户
user: Mapped["User"] = relationship(back_populates="orders")
小胖:“等等!为什么两边都有 relationship?不能只在一边写吗?”
大师:“这就是 back_populates 的作用。它告诉 SQLAlchemy:User.orders 和 Order.user 是互为镜像的关系。当你通过 user.orders.append(order) 添加一条订单,order.user 会自动设置为这个 user。反之亦然。这叫双向关联——两边自动保持同步。”
小白:“back_populates 和 backref 有什么区别?我见过同事用 backref。”
大师:“backref 是 1.x 的简化写法——你只在一边写 relationship(backref='user'),SQLAlchemy 自动在另一边创建 user 属性。但它的缺点是不显式——你看不到另一端到底是什么样子。2.0 推荐 back_populates,两边都显式声明,虽然多写几行代码,但可读性更高。”
# 1.x 风格:backref(不推荐)
class User(Base):
orders = relationship("Order", backref="user")
# 2.0 风格:back_populates(推荐)
class User(Base):
orders = relationship("Order", back_populates="user")
class Order(Base):
user = relationship("User", back_populates="orders")
小胖:“技术映射:back_populates = 双向关联的显式声明。那外键应该放在哪边?order 里有 user_id,那 user 里要不要有 order_id?”
大师:“外键放在’多’的一边。一个用户有多条订单,每条订单各指向一个用户——所以外键 user_id 放在 Order 表。User 表不需要存订单的外键。这是一对多的标准模式。”
小白:“那如果是一对一呢?比如每个用户有一个用户资料(Profile),User 和 Profile 是一对一的关系。”
大师:“一对一也是一对多的特例——在多的一边加 uselist=False:”
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
profile: Mapped["Profile"] = relationship(back_populates="user", uselist=False)
class Profile(Base):
__tablename__ = "profiles"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
user: Mapped["User"] = relationship(back_populates="profile")
小胖:“哇!unique=True 在外键上,加上 uselist=False 在 relationship 上——双重保证一对一?”
大师:“技术映射:unique + uselist=False = 一对一关系锁死。unique=True 从数据库层约束 user_id 唯一,uselist=False 从 Python 层约束返回单个对象而非列表。”
小白:“那集合类型呢?User.orders 用 list 还是 set?”
大师:“默认是 list(即 Mapped[list["Order"]])。如果你用 set,需要注意:重复添加相同对象不会报错(set 的去重特性),并且需要用 relationship(collection_class=set)。”
| 集合类型 | 行为 | 适用场景 |
|---|---|---|
list(默认) |
有序,允许重复 | 订单明细、时间线 |
set |
无序,去重 | 标签、角色 |
小胖:“那如果我不用 relationship 导航,直接用 Core 的 JOIN 写 SQL,行不行?”
大师:“当然可以。但在 ORM 层使用 relationship 有几个优势:一是对象图导航更自然(order.user.name 而不是写 JOIN);二是配合加载策略(第 18、19 章)可以优化 SQL 次数;三是级联写入——创建订单时自动创建明细。手写 JOIN 适合纯报表查询,业务对象操作用 relationship。”
三、项目实战
实战目标
建立用户-订单-订单明细的关系模型,实现"创建一笔订单并级联写入订单明细"的完整业务流程,并验证双向关联的自动同步行为。
步骤一:定义关系模型
"""ch10_relationships.py —— 一对多与一对多关系映射实战"""
from sqlalchemy import (
create_engine, String, Integer, Numeric, DateTime, Boolean,
ForeignKey, UniqueConstraint, Index, text, func, select,
)
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, relationship, Session, sessionmaker,
)
from datetime import datetime
from typing import Optional
engine = create_engine(
"postgresql+psycopg://nebula:nebula_dev@localhost:5432/order_center",
echo=True,
)
class Base(DeclarativeBase):
pass
# =============================================
# User —— 用户("一"的一方)
# =============================================
class User(Base):
__tablename__ = "rel_users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# —— 一对多:User → Order ——
orders: Mapped[list["Order"]] = relationship(back_populates="user")
def __repr__(self):
return f"<User({self.id}, {self.username})>"
# =============================================
# Order —— 订单("多"的一方,指向 User)
# =============================================
class Order(Base):
__tablename__ = "rel_orders"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_no: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("rel_users.id"), nullable=False, index=True)
total_amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# —— 多对一:Order → User ——
user: Mapped["User"] = relationship(back_populates="orders")
# —— 一对多:Order → OrderItem ——
items: Mapped[list["OrderItem"]] = relationship(
back_populates="order",
cascade="all, delete-orphan", # 级联写入 + 孤儿删除
)
def __repr__(self):
return f"<Order({self.order_no}, ¥{self.total_amount})>"
# =============================================
# OrderItem —— 订单明细("多"的一方,指向 Order)
# =============================================
class OrderItem(Base):
__tablename__ = "rel_order_items"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_id: Mapped[int] = mapped_column(ForeignKey("rel_orders.id"), nullable=False, index=True)
product_name: Mapped[str] = mapped_column(String(200), nullable=False)
unit_price: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
quantity: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("1"))
# —— 多对一:OrderItem → Order ——
order: Mapped["Order"] = relationship(back_populates="items")
@property
def subtotal(self) -> float:
return self.unit_price * self.quantity
def __repr__(self):
return f"<Item({self.product_name} x{self.quantity}, ¥{self.subtotal})>"
# =============================================
# Profile —— 用户资料(一对一)
# =============================================
class Profile(Base):
__tablename__ = "rel_profiles"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("rel_users.id"),
unique=True, # 数据库层一对一约束
nullable=False,
)
bio: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
avatar_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# —— 一对一:Profile → User ——
user: Mapped["User"] = relationship(back_populates="profile")
# 在 User 中添加 profile 关系(因为 Profile 定义在 User 之后,使用字符串引用)
User.profile: Mapped[Optional["Profile"]] = relationship(
"Profile", back_populates="user", uselist=False
)
Base.metadata.create_all(engine)
SessionFactory = sessionmaker(bind=engine, autocommit=False, autoflush=False)
步骤二:创建订单(级联写入明细)
# =============================================
# 级联创建:一笔订单 + 多条明细,一次 commit
# =============================================
def create_order_with_items(
username: str,
order_no: str,
items_data: list[dict], # [{"product_name": ..., "unit_price": ..., "quantity": ...}]
) -> Order:
"""创建订单并级联写入订单明细"""
with SessionFactory() as session:
# Step 1: 查找用户(新用户自动注册)
user = session.execute(
select(User).where(User.username == username)
).scalars().first()
if not user:
user = User(username=username, email=f"{username}@nebula.com")
session.add(user)
session.flush() # 获取 user.id
# Step 2: 创建订单
total_amount = sum(item["unit_price"] * item["quantity"] for item in items_data)
order = Order(
order_no=order_no,
user_id=user.id,
total_amount=total_amount,
)
# Step 3: 创建明细——通过 relationship 建立关联
for item_data in items_data:
item = OrderItem(
product_name=item_data["product_name"],
unit_price=item_data["unit_price"],
quantity=item_data["quantity"],
order=order, # 直接赋值 relationship 属性!
)
session.add(item)
session.add(order)
session.commit()
# 验证级联效果
print(f"订单 {order.order_no}: 金额 ¥{order.total_amount}, 共 {len(order.items)} 条明细")
for item in order.items:
print(f" → {item}")
return order
# 测试级联创建
print("=== 创建订单(级联写入)===")
order1 = create_order_with_items(
username="小胖",
order_no="ORD-2024-001",
items_data=[
{"product_name": "有机全麦面包", "unit_price": 15.80, "quantity": 2},
{"product_name": "低脂酸奶 100g", "unit_price": 8.50, "quantity": 3},
{"product_name": "蓝牙耳机 Pro", "unit_price": 299.00, "quantity": 1},
],
)
# 预期:一条 INSERT INTO orders + 三条 INSERT INTO order_items
print()
运行结果:
=== 创建订单(级联写入)===
[SQL] INSERT INTO rel_users (username, email, created_at)
VALUES ('小胖', '小胖@nebula.com', now())
[SQL] INSERT INTO rel_orders (order_no, user_id, total_amount, created_at)
VALUES ('ORD-2024-001', 1, 356.1, now())
[SQL] INSERT INTO rel_order_items (order_id, product_name, unit_price, quantity)
VALUES (1, '有机全麦面包', 15.80, 2)
[SQL] INSERT INTO rel_order_items (order_id, product_name, unit_price, quantity)
VALUES (1, '低脂酸奶 100g', 8.50, 3)
[SQL] INSERT INTO rel_order_items (order_id, product_name, unit_price, quantity)
VALUES (1, '蓝牙耳机 Pro', 299.00, 1)
订单 ORD-2024-001: 金额 ¥356.10, 共 3 条明细
→ <Item(有机全麦面包 x2, ¥31.60)>
→ <Item(低脂酸奶 100g x3, ¥25.50)>
→ <Item(蓝牙耳机 Pro x1, ¥299.00)>
步骤三:对象导航查询(替代手写 JOIN)
# =============================================
# 对象导航 vs 手写 JOIN
# =============================================
print("\n=== 对象导航查询 ===")
with SessionFactory() as session:
# 方式 1:先查用户,再通过 .orders 导航
print("--- 方式 1:user.orders 导航 ---")
user = session.execute(
select(User).where(User.username == "小胖")
).scalars().one()
print(f"用户 {user.username} 的订单:")
for order in user.orders: # 不需要手写 JOIN!
print(f" {order.order_no} | ¥{order.total_amount} | {order.status}")
for item in order.items:
print(f" └── {item}")
# 方式 2:先查订单,再通过 .user 反向导航
print("\n--- 方式 2:order.user 反向导航 ---")
orders = session.execute(
select(Order).where(Order.user.has(User.username == "小胖"))
).scalars().all()
for order in orders:
print(f"订单 {order.order_no} → 下单人: {order.user.username}")
# 注意:order.user 触发了额外一条 SELECT(用到了 User 的 Identity Map 缓存)
# 对比:手写 JOIN(不推荐日常使用,但性能场景可用)
print("\n--- 对比:手写 JOIN ---")
stmt = select(Order.order_no, User.username).select_from(
Order.join(User, Order.user_id == User.id)
).where(User.username == "小胖")
for row in session.execute(stmt):
print(f" {row.order_no} by {row.username}")
print()
步骤四:双向关联自动同步
# =============================================
# back_populates 双向同步验证
# =============================================
print("=== back_populates 双向同步 ===")
with SessionFactory() as session:
user = session.get(User, 1)
print(f"当前 user.orders 数量: {len(user.orders)}")
# 方式 A:从 Order 端建立关系
new_order = Order(
order_no="ORD-2024-002",
user_id=user.id,
total_amount=99.00,
)
new_order.user = user # 设置关系的一侧
# back_populates 自动同步了另一侧
print(f"new_order.user.username = {new_order.user.username}")
print(f"user.orders 是否包含 new_order? {new_order in user.orders}") # True!
# 方式 B:从 User 端建立关系
another_order = Order(
order_no="ORD-2024-003",
total_amount=199.00,
)
user.orders.append(another_order) # 添加到列表
# back_populates 自动同步了另一侧
print(f"another_order.user 已自动设置? {another_order.user is not None}") # True
print(f"another_order.user.username = {another_order.user.username}")
# 但注意:如果 user_id 没有正确设置呢?
# 因为 another_order 是通过 user.orders.append 添加的,
# 但 user_id 没有显式传值——
# SQLAlchemy 在 flush 时会自动从 user 对象的 id 推导 user_id
session.add_all([new_order, another_order])
session.flush()
print(f"flush 后: new_order.user_id={new_order.user_id}, another_order.user_id={another_order.user_id}")
# 两者都正确!
session.rollback() # 仅演示,回滚不保存
print()
步骤五:一对一 Profile 实战
# =============================================
# 一对一关系:User → Profile
# =============================================
print("=== 一对一关系:用户资料 ===")
with SessionFactory() as session:
user = session.get(User, 1)
# 创建资料
profile = Profile(
user=user,
bio="爱吃饭、爱写代码",
avatar_url="https://avatar.nebula.com/user1.png",
)
session.add(profile)
session.commit()
# 通过 user.profile 导航
print(f"user.profile.bio = {user.profile.bio}")
print(f"profile.user.username = {profile.user.username}")
完整代码清单
"""ch10_relationships_complete.py —— 关系映射完整示例"""
from sqlalchemy import create_engine, String, Integer, Numeric, DateTime, Boolean, ForeignKey, text, func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker, Session
from datetime import datetime
from typing import Optional, List
from order_center.config import DATABASE_URL
engine = create_engine(DATABASE_URL, echo=False)
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "rm_users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# 一对多
orders: Mapped[List["Order"]] = relationship(back_populates="user")
# 一对一
profile: Mapped[Optional["Profile"]] = relationship(back_populates="user", uselist=False)
class Profile(Base):
__tablename__ = "rm_profiles"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("rm_users.id"), unique=True, nullable=False)
bio: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
user: Mapped["User"] = relationship(back_populates="profile")
class Order(Base):
__tablename__ = "rm_orders"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_no: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("rm_users.id"), nullable=False, index=True)
total_amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship(back_populates="orders")
items: Mapped[List["OrderItem"]] = relationship(back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base):
__tablename__ = "rm_order_items"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
order_id: Mapped[int] = mapped_column(ForeignKey("rm_orders.id"), nullable=False, index=True)
product_name: Mapped[str] = mapped_column(String(200), nullable=False)
unit_price: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
quantity: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("1"))
order: Mapped["Order"] = relationship(back_populates="items")
Base.metadata.create_all(engine)
Factory = sessionmaker(bind=engine)
def create_order(username: str, order_no: str, items_data: List[dict]) -> Order:
with Factory() as session:
user = session.execute(select(User).where(User.username == username)).scalars().first()
if not user:
user = User(username=username, email=f"{username}@test.com")
session.add(user)
session.flush()
total = sum(i["unit_price"] * i["quantity"] for i in items_data)
order = Order(order_no=order_no, user_id=user.id, total_amount=total)
for d in items_data:
order.items.append(OrderItem(product_name=d["product_name"], unit_price=d["unit_price"], quantity=d["quantity"]))
session.add(order)
session.commit()
return order
if __name__ == "__main__":
o = create_order("test_user", "TEST-001", [
{"product_name": "商品A", "unit_price": 10, "quantity": 2},
{"product_name": "商品B", "unit_price": 20, "quantity": 1},
])
print(f"订单创建完成: {o.order_no}, {len(o.items)} 条明细")
可能遇到的坑及解决方法
Foreign Key引用尚未定义的表的名称
- 现象:
NoReferencedTableError: Foreign key could not find table 'users' - 原因:声明模型时被引用的表可能在当前文件中的定义顺序靠后,或者使用了错误的表名(注意大小写)。
- 解决:确认
ForeignKey("tablename")中的字符串与实际__tablename__一致。如在另一个模块中,确保已导入。
back_populates写错属性名导致静默失效
- 现象:双向关联不同步,
order.user为 None 但user.orders里有 order。 - 原因:
back_populates="userr"(多了一个 r),拼写错误不报错但匹配不上。 - 解决:用 IDE 的字符串引用检查功能;或者写单测验证双向同步行为。
cascade="all, delete-orphan"误删整树的子对象
- 现象:从
user.orders列表中移除一个 order,该 order 被物理删除。 - 根因:
delete-orphan会把"脱离父对象"的子对象自动删除。 - 解决:如果订单不能随意删除,去掉
delete-orphan,仅保留cascade="save-update, merge"。
- 集合类型使用不当
- 现象:用
set作为集合类型但重复添加时报IntegrityError。 - 解决:
relationship(collection_class=set)配合Mapped[set["Order"]]使用。
测试验证
# tests/test_ch10_relationships.py
import pytest
from sqlalchemy import create_engine, String, Integer, Numeric, ForeignKey, text, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker
from datetime import datetime
@pytest.fixture
def engine():
return create_engine("sqlite:///:memory:", echo=False)
@pytest.fixture
def models(engine):
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
orders: Mapped[list["Order"]] = relationship(back_populates="user")
profile: Mapped["Profile"] = relationship(back_populates="user", uselist=False)
class Profile(Base):
__tablename__ = "profiles"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
bio: Mapped[str] = mapped_column(String(200))
user: Mapped["User"] = relationship(back_populates="profile")
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
order_no: Mapped[str] = mapped_column(String(32))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
total: Mapped[float] = mapped_column(Numeric(12, 2))
user: Mapped["User"] = relationship(back_populates="orders")
items: Mapped[list["OrderItem"]] = relationship(back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base):
__tablename__ = "order_items"
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
product: Mapped[str] = mapped_column(String(100))
price: Mapped[float] = mapped_column(Numeric(12, 2))
qty: Mapped[int] = mapped_column(Integer)
order: Mapped["Order"] = relationship(back_populates="items")
Base.metadata.create_all(engine)
return User, Profile, Order, OrderItem
def test_one_to_many_bidirectional(models, engine):
"""验证一对多双向关联同步"""
User, _, Order, Item = models
Factory = sessionmaker(bind=engine)
with Factory() as s:
user = User(name="test")
order = Order(order_no="ORD-001", total=100)
order.user = user
s.add(order)
s.commit()
# 双向同步
assert order in user.orders
assert order.user == user
def test_one_to_one(models, engine):
"""验证一对一关系"""
User, Profile, _, _ = models
Factory = sessionmaker(bind=engine)
with Factory() as s:
user = User(name="test1")
profile = Profile(bio="hello")
profile.user = user
s.add(profile)
s.commit()
assert user.profile == profile
def test_cascade_create_order_with_items(models, engine):
"""验证级联创建订单和明细"""
User, _, Order, Item = models
Factory = sessionmaker(bind=engine)
with Factory() as s:
user = User(name="test2")
order = Order(order_no="ORD-002", total=200, user=user)
item1 = Item(product="A", price=100, qty=1, order=order)
item2 = Item(product="B", price=100, qty=1, order=order)
s.add(order)
s.commit()
assert len(order.items) == 2
assert order.items[0].product == "A"
def test_back_populates_sync(models, engine):
"""验证 back_populates 自动同步"""
User, _, Order, Item = models
Factory = sessionmaker(bind=engine)
with Factory() as s:
user = User(name="test3")
order = Order(order_no="ORD-003", total=300)
# 从 Order 端设置
order.user = user
assert order in user.orders # 自动同步到另一端
# 从 User 端追加
order2 = Order(order_no="ORD-004", total=400)
user.orders.append(order2)
assert order2.user == user # 自动同步到另一端
四、项目总结
优点与缺点
| 对比维度 | 手写 SQL JOIN | SQLAlchemy relationship |
|---|---|---|
| 可读性 | SQL 逻辑需阅读才能理解 | order.user.name 即见即所得 |
| 维护成本 | 每个查询手写 JOIN,散落各处 | 关系定义一次,处处复用 |
| 完整性 | 需手动保证外键一致 | ForeignKey 数据库约束 + relationship 逻辑关联 |
| 级联写入 | 需手动逐层 INSERT | cascade 配置,一次 add 搞定 |
| N+1 问题 | 容易写出来 | 配合加载策略(第18-19章)可根除 |
| 学习成本 | 低 | 中(需理解 back_populates、cascade、uselist) |
适用场景
- 有明确父子关系的数据模型:用户→订单→订单明细、部门→员工、项目→任务。
- 需要对象图的业务流程:如"创建订单"牵着"用户"和"明细"一起走。
- 需要避免手写 JOIN 的项目:用
relationship定义关系,让加载策略决定如何取数据。 - 需要数据完整性的业务:
ForeignKey确保引用完整性。 - 有级联操作需求的场景:cascade 配置简化级联写入和删除。
不推荐场景:
- 纯报表查询(不涉及对象操作)——直接 Core JOIN 更高效。
- 跨数据库关联——Foreign Key 跨库不生效,需在应用层处理。
- 极其简单的场景——如只有两张表且关系不变。
注意事项
ForeignKey的 ondelete 和 ORM cascade 是两层:ForeignKey(ondelete="CASCADE")是数据库层的级联删除,cascade="all, delete-orphan"是 ORM 层的级联逻辑。两者可以配合使用,但语义不同。relationship默认是懒加载:user.orders在你第一次访问时才会发 SQL。生产中需要根据加载场景选择合适的加载策略。- 避免循环引用:
back_populates用字符串引用避免了 Python 的 import 循环问题。 uselist=False不检查唯一性:它只在 Python 层限制返回单个对象,不保证数据库层唯一。需要配合ForeignKey(unique=True)使用。
常见踩坑经验
案例 1:从列表中 remove 后失踪
- 现象:
user.orders.remove(order)后,订单消失了但未发 DELETE。 - 根因:
remove只是把 order 的user_id设为 NULL(外键解除关联),不是物理删除。只有配置了cascade="all, delete-orphan"才会自动删除。 - 修复:如果业务需要真删除,用
session.delete(order),而不是从列表中 remove。
案例 2:自引用(父子表)的外键放错边
- 现象:
comment.parent_id指向了comments.id,但写入时 parent_id 始终为 NULL。 - 根因:因为 parent 还没 commit(没有 id),子对象插入时无法设置外键值。
- 修复:先 flush parent,再设置 child 的外键;或者配置正确的 cascade 顺序。
案例 3:JSON 序列化时循环引用
- 现象:
json.dumps(user.__dict__)因为user.orders[0].user == user而陷入无限循环。 - 根因:双向关联导致对象图形成闭环。
- 修复:定义 Pydantic schema 或
@property方法做安全序列化,只暴露需要的数据。
思考题
-
在你的订单系统中,
Order和OrderItem是一对多关系。如果用户修改了某条OrderItem的商品数量,你需要实时重新计算Order.total_amount。请设计两种实现方案:方案 A——Python 层(业务逻辑中手动重新计算),方案 B——数据库层(使用数据库计算函数或触发器)。分析两种方案的优缺点。 -
假设你的 User 有 100 条订单,代码里写了一段
for order in user.orders: print(order.total)。如果默认的懒加载策略是lazy='select'(默认值),这段代码实际会发出多少条 SQL 查询?如果改为lazy='selectin',又会发出多少条 SQL?这两种策略分别适合什么场景?
参考答案参见附录 E。
延伸阅读与资源
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
MongoDB 实战进阶与内核修炼
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析
更多推荐




所有评论(0)