Skip to content

第 9 章 映射类的查询与修改 ​

从一个生活场景说起 ​

上一章我们把记录变成了"档案卡"(对象)。这一章学习如何找到一张卡、修改它、或者把它销毁。

ORM 的修改有一种很"科幻"的玩法,想象你有一张智能档案卡:

  1. 你从档案柜(数据库)里取出"张三"这张卡;
  2. 用笔把卡上的年龄从 20 改成 21——只是在卡上改,档案柜里的还没动;
  3. 把卡还给管理员并说一句"存档"(commit);
  4. 管理员自己对比出"哦,年龄这一栏变了",只更新数据库里对应的那一列。

你只管在卡片上涂改,"哪些变了、怎么同步回数据库"全是 Session 自动搞定的——这就是 ORM 的**脏检测(Dirty Tracking)**机制。

核心概念 ​

ORM 查询返回的是对象而不是裸的行数据。2.0 中的查询套路固定为:

python
session.scalars(select(模型类).where(...)).结果方法()
  • select(User):构建"查 User 表"的语句;
  • session.scalars(...):执行并直接拆包成"对象迭代器"(省去手动从 Row 里取);
  • .all() / .first():决定取多少条。

示例代码(逐行讲解) ​

python
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):

python
session.commit()
print(user.name)   # 这里会悄悄发一条 SELECT 重新加载!

设计意图:commit 后数据库里的数据可能被别的程序改过,SQLAlchemy 不敢让你用旧数据,于是下次访问时自动重新查询。优点是永远读到新鲜数据,代价是多一次 SQL。如果这个行为困扰你,可以在创建 Session 工厂时关闭:

python
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 章详解):

python
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;
  • 三个"查一条"方法对异常的处理不同,按业务语义选择。