代码语言

知识点思维导图

28 个知识节点

Python(16) - 数据库操作

读完后,你应能完成以下任务:

  • 绘制“Python(16) - 数据库操作 / 先给前端的「最小 SQL 集」”的关键对象与数据流,解释“ORM 是把 SQL 包了一层,但你至少得知道它在背后生成什么。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Python(16) - 数据库操作 / 先给锚点:ORM ≈ Prisma / TypeORM”设计正常与异常输入,验证“关键边界(最重要):Prisma 里你 await prisma.user.create(...) 一行就直接写库了。”,输出首个偏差位置与回归测试结果。
  • 实现“Python(16) - 数据库操作 / 三个地基对象:Engine、Model、Session”的最小代码或配置,检验“学习期用 SQLite(零部署、单文件),上生产再换 MySQL/PG,模型代码一行不用改——这正是 ORM 屏蔽数据库差异的价值。”,输出命令、结果与 Diff,并说明不适用边界。

接口收到数据后总得存进数据库。前端时代你大概率没手写过 SQL,而是用 Prisma / TypeORM 这类 ORM:定义一个 model,然后 prisma.user.create(...) 就完事。Python 后端最主流的 ORM 叫 SQLAlchemy,思路一模一样——把数据表映射成 class,把每行数据映射成对象,增删改查全用 Python 代码表达,不手写 SQL。本篇帮你把 Prisma 的直觉平移过来,同时讲清一个前端 ORM 里很弱、但 SQLAlchemy 里极其核心的概念:Session(会话/工作单元)

一、先给前端的「最小 SQL 集」

ORM 是把 SQL 包了一层,但你至少得知道它在背后生成什么。下面四句覆盖 90% 日常操作,看懂即可,不用背:

-- 查:从 users 表取 name 等于 'Tom' 的行
SELECT * FROM users WHERE name = 'Tom';

-- 增:插入一行
INSERT INTO users (name, email) VALUES ('Tom', 't@x.com');

-- 改:把 id=1 的行的 name 改掉
UPDATE users SET name = 'Tom2' WHERE id = 1;

-- 删:删掉 id=1 的行
DELETE FROM users WHERE id = 1;

记住对应关系即可:查=SELECT、增=INSERT、改=UPDATE、删=DELETE。下面 SQLAlchemy 的每个操作,本质都是帮你生成上面这几句。


二、先给锚点:ORM ≈ Prisma / TypeORM

如果你用过 Prisma,下面这段几乎是逐行对应:

// Prisma(前端熟悉的 ORM)
// schema 里定义 model
model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String?
}

// 业务代码里直接用对象操作,不写 SQL
const user = await prisma.user.create({ data: { name: "Tom" } })
const found = await prisma.user.findMany({ where: { name: "Tom" } })

对照表先建立直觉:

Prisma / TypeORM SQLAlchemy 说明
model User {} class User(Base) 一个模型 = 一张表
@id primary_key=True 主键
String?(可空) Mapped[str | None] 可空列
prisma.user.create session.add(User(...)) 新增
findMany / findUnique select(User) / session.get 查询
自动连接池 / client engine + Session 连接与会话

关键边界(最重要):Prisma 里你 await prisma.user.create(...) 一行就直接写库了。SQLAlchemy 不是——你对对象的增删改,是先攒在一个叫 Session 的"暂存区"里,最后统一 commit() 才真正落库。这个 Session 概念是前端 ORM 里几乎没有、但 SQLAlchemy 里绕不开的核心,第三节专门讲。先记住:SQLAlchemy = 模型映射(像 Prisma) + 显式的工作单元(Session)


三、三个地基对象:Engine、Model、Session

SQLAlchemy 有三个你必须分清的角色,用前端类比一句话各自定位:

对象 作用 前端类比
Engine 数据库连接的总入口,管连接池 new Pool() / 数据库连接配置
Model(模型类) 表 ↔ 类、行 ↔ 对象的映射定义 Prisma 的 model 定义
Session 一次"对话",暂存改动、最后提交 一个事务 / 一批待保存的草稿

3.1 Engine:连一次,全局复用

连接串换数据库只改前缀:SQLite 用 sqlite:///文件名;MySQL 用 mysql+pymysql://user:pwd@host/dbname;PostgreSQL 用 postgresql+psycopg://...。学习期用 SQLite(零部署、单文件),上生产再换 MySQL/PG,模型代码一行不用改——这正是 ORM 屏蔽数据库差异的价值。

3.2 Session:所有增删改查都在它上面进行

记住这条主线:Engine 建一次全局共享;Session 每个请求/每段业务开一个,用完就关。在 FastAPI 里,Session 通常通过依赖注入按请求创建(见第 18 篇 Depends)。


四、核心心智模型:Session 是"工作单元",commit 才落库

这是前端过来最该停下来理解的一节。Prisma 每个方法都是"说了就直接执行",而 SQLAlchemy 的 Session 像 Git 的暂存区

  • session.add(obj)git add:把改动放进暂存区,还没写库
  • session.commit()git commit:把这一批改动一次性真正写入数据库
  • session.rollback()git checkout .:放弃这一批改动

为什么要这么设计(WHY):把"多次改动攒成一批一次提交",天然就是事务——要么全成功,要么全回滚。比如"扣库存 + 创建订单"两步,放在一个 Session 里 commit,中途失败就整体回滚,不会出现扣了库存却没下单的脏数据。Prisma 要用 prisma.$transaction([...]) 显式包起来,而 SQLAlchemy 的 Session 本身就是一个事务单元,这是它更"重"但也更强的地方。

边界提醒:别每次操作都 new 一个 Session,也别全局共享一个 Session。正确姿势是"一段业务/一个请求一个 Session"。Session 不是线程安全的,全局共享会出并发问题。


五、增删改查(CRUD)完整对照

下面四个操作是日常 95% 的工作,每个都给出等价的 Prisma 写法并排看。

5.1 增(Create)

// Prisma 对比:一行搞定,没有 add/commit 两步
const user = await prisma.user.create({ data: { name, email } })

5.2 查(Read)—— 2.0 的 select 写法

SQLAlchemy 2.0 统一用 select() 构造查询,再用 session.scalars() 执行:

// Prisma 对比
const user = await prisma.user.findUnique({ where: { id: uid } })
const list = await prisma.user.findMany({ where: { name } })

小坑:过滤条件写的是 User.name == name(用 ==),不是 Python 的关键字。SQLAlchemy 重载了 ==,让 User.name == name 这个表达式生成 SQL 片段 name = ?,而不是返回布尔值。这点和 Prisma 用对象 { where: { name } } 风格不同,初看会有点怪,习惯就好。

5.3 改(Update)

最直观的方式:先查出对象,直接改属性,commit。Session 会自动追踪哪些字段变了(这叫"脏检查"):

// Prisma 对比
const user = await prisma.user.update({ where: { id: uid }, data: { name: newName } })

这就是 ORM 的"魔法":你只是改了个 Python 对象的属性,commit 时 SQLAlchemy 对比对象的新旧值,发现 name 变了,自动生成 UPDATE users SET name=? WHERE id=?。前端 ORM 里没有这种"改对象=改库"的隐式追踪,这是 Session 跟踪对象带来的能力。

5.4 删(Delete)

// Prisma 对比
await prisma.user.delete({ where: { id: uid } })

六、表关系:一对多(外键 + relationship)

真实业务里表是有关系的:一个用户有多篇文章。SQLAlchemy 用 ForeignKey(外键,数据库层面的关联)+ relationship(Python 层面的对象导航)两件套:

// Prisma 对比:relation 字段 + 外键标量字段,思路完全一致
model User { id Int @id; posts Post[] }
model Post { id Int @id; authorId Int; author User @relation(fields: [authorId], references: [id]) }

定义好后,访问关联数据就像访问普通属性:

N+1 查询坑(前端用 ORM 也会踩):上面 for post in user.posts 默认是"用到才查"(懒加载)。如果你在循环里对一批 user 逐个访问 .posts,会变成 1 次查用户 + N 次查文章,共 N+1 条 SQL,量大时很慢。解决办法是查询时显式预加载:

from sqlalchemy.orm import selectinload
# selectinload:一次性把关联的 posts 也批量查出来,避免逐个再查
stmt = select(User).options(selectinload(User.posts))
users = session.scalars(stmt).all()

这等价于 Prisma 的 include: { posts: true }。把 echo=True 打开看 SQL 条数,是验证有没有踩 N+1 的最直接办法。


七、放进 FastAPI:按请求开 Session

把 Session 接到 FastAPI 接口上,标准做法是用依赖注入(Depends 见第 18 篇),保证每个请求一个独立 Session、请求结束自动关闭

这里 db: Session = Depends(get_db) 就是 FastAPI 的依赖注入:你不用自己 Session(engine),框架按请求帮你建好、用完帮你关。这套"每请求一个会话"是后端铁律——和前端 React 里"每个组件一份 state、卸载时清理"的生命周期直觉一脉相承。


八、总结

  • 先给前端的「最小 SQL 集」:ORM 是把 SQL 包了一层,但你至少得知道它在背后生成什么。
  • 先给锚点:ORM ≈ Prisma / TypeORM:关键边界(最重要):Prisma 里你 await prisma.user.create(...) 一行就直接写库了。
  • 三个地基对象:Engine、Model、Session:学习期用 SQLite(零部署、单文件),上生产再换 MySQL/PG,模型代码一行不用改——这正是 ORM 屏蔽数据库差异的价值。
  • 核心心智模型:Session 是"工作单元",commit 才落库:这是前端过来最该停下来理解的一节。
  • 增删改查(CRUD)完整对照:小坑:过滤条件写的是 User.name == name(用 ==),不是 Python 的关键字。
  • 表关系:一对多(外键 + relationship):真实业务里表是有关系的:一个用户有多篇文章。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“数据库操作”中,需要同时满足“先给前端的「最小 SQL 集」”与“先给锚点:ORM ≈ Prisma / TypeORM”。给定正文约束“ORM 是把 SQL 包了一层,但你至少得知道它在背后生成什么。”,哪些判断保持了原有处理机制?多选
2“数据库操作”出现偏差:“在“数据库操作 / 三个地基对象:Engine、Model、Session”中,即使不满足“SQLAlchemy 有三个你必须分清的角色,用前端类比一句话各自定位”,结果与副作用仍会保持不变。”已成为实际行为。围绕“三个地基对象:Engine、Model、Session”与“Engine:连一次,全局复用”,哪些判断能定位被改变的职责或边界?多选
3评审“数据库操作”方案时,验收条件包含“在 FastAPI 里,Session 通常通过依赖注入按请求创建(见第 18 篇 Depends)。”。关于“Session:所有增删改查都在它上面进行”与“核心心智模型:Session 是"工作单元",commit 才落库”的哪些决策符合正文机制?多选