第 9 章 映射类的查询与修改
从一个生活场景说起
上一章我们把记录变成了"档案卡"(对象)。这一章学习如何找到一张卡、修改它、或者把它销毁。
ORM 的修改有一种很"科幻"的玩法,想象你有一张智能档案卡:
- 你从档案柜(数据库)里取出"张三"这张卡;
- 用笔把卡上的年龄从 20 改成 21——只是在卡上改,档案柜里的还没动;
- 把卡还给管理员并说一句"存档"(commit);
- 管理员自己对比出"哦,年龄这一栏变了",只更新数据库里对应的那一列。
你只管在卡片上涂改,"哪些变了、怎么同步回数据库"全是 Session 自动搞定的——这就是 ORM 的**脏检测(Dirty Tracking)**机制。
核心概念
ORM 查询返回的是对象而不是裸的行数据。2.0 中的查询套路固定为:
session.scalars(select(模型类).where(...)).结果方法()select(User):构建"查 User 表"的语句;session.scalars(...):执行并直接拆包成"对象迭代器"(省去手动从 Row 里取);.all()/.first():决定取多少条。
示例代码(逐行讲解)
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
engine = create_engine(
"mysql+pymysql://root:123456@localhost:3306/demo_db?charset=utf8mb4",
echo=True,
)
with Session(engine) as session:
# ① 查询所有用户:SELECT * FROM user
users = session.scalars(select(User)).all()
# ② 条件查询:SELECT * FROM user WHERE name = '张三'
user = session.scalars(
select(User).where(User.name == "张三")
).first()
# ③ 按主键查询(快捷方法):SELECT * FROM user WHERE id = 1
user = session.get(User, 1)
# ④ 修改:直接改对象属性,然后 commit —— 没有写任何 UPDATE 语句!
if user:
user.age = 21
session.commit()
# ⑤ 删除对象
if user:
session.delete(user)
session.commit()修改部分最值得细品:user.age = 21 只是一次普通的 Python 赋值,数据库毫无动静;直到 session.commit(),Session 才检查出"age 被改过",生成 UPDATE user SET age=21 WHERE id=1 发给 MySQL。
关键知识点
| 操作 | 写法 |
|---|---|
| 查多条 | session.scalars(select(User).where(...)).all() |
| 查一条 | .first() / .one() / .one_or_none() |
| 按主键查 | session.get(User, 主键值) |
| 修改 | 改属性 → commit() |
| 删除 | session.delete(obj) → commit() |
深入理解:三个"查一条"方法的区别
它们长得像,脾气完全不同:
| 方法 | 查不到 | 查到多条 |
|---|---|---|
.first() | 返回 None | 返回第一条(不报错) |
.one_or_none() | 返回 None | 抛异常 |
.one() | 抛异常 | 抛异常 |
选择建议:
- "可能有也可能没有"的场景(如按昵称找人)→
.first()或.one_or_none(); - "按业务逻辑必然唯一"的场景(如按唯一邮箱查用户)→
.one(),查不到说明数据有问题,让它抛异常暴露出来。
深入理解:commit 后的"过期"现象
默认配置下,commit() 之后对象的属性会被标记为过期(expired):
session.commit()
print(user.name) # 这里会悄悄发一条 SELECT 重新加载!设计意图:commit 后数据库里的数据可能被别的程序改过,SQLAlchemy 不敢让你用旧数据,于是下次访问时自动重新查询。优点是永远读到新鲜数据,代价是多一次 SQL。如果这个行为困扰你,可以在创建 Session 工厂时关闭:
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)进阶内容
1. session.get() 的缓存福利。 session.get(User, 1) 不仅会查数据库,还会先看当前 Session 里是否已经加载过这个对象——加载过就直接复用,不发 SQL。同一个 Session 内重复 get 同一个主键,第二次零开销。
2. 批量修改用 SQL 级操作。 "给所有用户年龄加 1"这种需求,逐对象改再 commit 会产生 N 条 UPDATE。更好的方式是直接执行一条 UPDATE(第 15 章详解):
from sqlalchemy import update
session.execute(update(User).values(age=User.age + 1))
session.commit()3. MySQL 的可重复读隔离级别。 MySQL 默认隔离级别是 REPEATABLE READ:在一个事务里反复查询同一条数据,读到的是事务开始时的"快照"。这意味着长事务中你可能看不到其他程序刚刚提交的修改——又一个"事务要短"的理由(第 16 章展开)。
注意事项
- ORM 的修改是"脏检测"机制:Session 会自动发现哪些对象被改过,commit 时只更新变化的部分。
- 对象在
commit()后属性默认会"过期",下次访问时自动重新查询数据库(可通过expire_on_commit=False关闭)。 session.get()查不到返回None,不会报错。.one()查不到或查到多条都会抛异常;.one_or_none()查不到返回None,多条抛异常;.first()永远不抛异常——按业务需要选择。- MySQL 默认隔离级别是可重复读(REPEATABLE READ),长事务中反复查询可能读到"快照"旧数据,注意事务要短。
- 删除对象后该对象在 Session 中仍可访问,但 commit 后数据库里已不存在——不要拿着已删除的对象继续操作。
本章小结
- ORM 查询固定套路:
session.scalars(select(Model).where(...)).all()/first(); - 按主键查用
session.get(),快且有缓存; - 修改 = 改属性 + commit,脏检测自动生成 UPDATE;
- 三个"查一条"方法对异常的处理不同,按业务语义选择。