知识点思维导图
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):真实业务里表是有关系的:一个用户有多篇文章。
学完自测
选择所有正确答案;提交后逐项核对判断依据。