feat(数据库): 完善第四阶段SQLAlchemy课程与综合项目

This commit is contained in:
zhiye.sun
2026-08-20 15:45:21 +08:00
parent a28c3b3168
commit bf1f7042a9
18 changed files with 3039 additions and 29 deletions
+470
View File
@@ -0,0 +1,470 @@
# 第4-3课:SQLAlchemy基础
## 一、本课定位
前两课直接使用Psycopg,已经理解连接、游标、参数化SQL、事务和Repository分层。本课开始使用SQLAlchemy 2.x,将生产项目常用的连接池、SQL工具和对象关系映射(Object Relational Mapping,ORM)引入课程。
本课不会隐藏底层原理。SQLAlchemy最终仍通过Psycopg连接PostgreSQL:
```text
业务代码
↓
SQLAlchemy ORM和Session
↓
SQLAlchemy Engine与连接池
↓
Psycopg
↓
PostgreSQL
```
本课示例会创建`course_orm_book`表,只重置`ORM-`前缀课程数据;练习创建`course_orm_product`表,只操作`ORM-P-`前缀数据。不要连接生产数据库。
## 二、本课目标
完成本课后,你将能够:
1. 解释SQLAlchemy Core和ORM的关系;
2. 使用`Engine`统一管理数据库方言和连接池;
3. 使用声明式模型映射Python类与数据库表;
4. 使用`Session`管理ORM对象和事务;
5. 使用SQLAlchemy 2.x的`select()`查询对象;
6. 完成ORM新增、查询、修改和删除;
7. 区分`flush()`、`commit()`、`rollback()`和`close()`;
8. 理解Session的工作单元和身份映射;
9. 对照JDBC、MyBatis-Plus和JPA/Hibernate理解SQLAlchemy。
## 三、SQLAlchemy解决什么问题
直接使用Psycopg时,需要自行处理:
- 创建数据库连接;
- 复用或关闭连接;
- 手写SQL;
- 把查询元组转换为对象;
- 跟踪对象修改;
- 组织提交和回滚。
SQLAlchemy提供两个主要层次:
| 层次 | 作用 |
|---|---|
| SQLAlchemy Core | Engine、连接池、SQL表达式、表元数据、方言适配 |
| SQLAlchemy ORM | 类表映射、Session、对象查询、关系和工作单元 |
ORM建立在Core之上。使用ORM并不意味着不再需要理解SQL、事务和索引。
## 四、与Java技术体系对照
| Java常见技术 | SQLAlchemy中的相近概念 | 说明 |
|---|---|---|
| JDBC Driver | Psycopg | PostgreSQL底层驱动 |
| `DataSource`和HikariCP | `Engine`和连接池 | 管理连接获取、复用和归还 |
| JPA实体 | 声明式ORM模型 | 类和表之间的映射 |
| `EntityManager` | `Session` | 管理持久化对象和事务 |
| Persistence Context | Session身份映射 | 同一Session内按主键维护对象身份 |
| Dirty Checking | Session变更跟踪 | 修改对象属性后生成UPDATE |
| JPQL/Criteria | `select()`表达式 | 用Python表达式构造查询 |
| `@Transactional` | `Session.begin()`上下文 | 正常提交、异常回滚 |
SQLAlchemy ORM总体更接近JPA/Hibernate。它也能简化常规增删改查,使用体验部分接近MyBatis-Plus,但不是以Mapper接口和SQL模板为中心。
## 五、Engine和连接池
创建Engine:
```python
engine = create_engine(
database_url,
connect_args={"connect_timeout": 10},
pool_size=5,
max_overflow=5,
pool_pre_ping=True,
echo=False,
)
```
Engine不是一条固定连接,而是数据库访问入口。它组合了:
- 数据库URL;
- PostgreSQL方言;
- Psycopg驱动;
- 连接池;
- SQL执行和事件机制。
### 5.1 常用连接池参数
| 参数 | 含义 |
|---|---|
| `pool_size=5` | 池中长期保留的连接数量上限 |
| `max_overflow=5` | 池满时允许临时增加的连接数 |
| `pool_pre_ping=True` | 取出连接时先检查连接是否仍可用 |
| `pool_timeout` | 连接池耗尽时最多等待多少秒 |
| `pool_recycle` | 连接存活超过指定秒数后回收更新 |
`connect_args`会把驱动专用参数交给Psycopg。本课将TOML中的`connect_timeout`传入,避免网络异常时无限等待。
`pool_size=5`不代表程序启动时立即创建5条连接。Engine通常按需创建连接。
生产应用通常在启动时创建一个Engine,不应在每个Repository方法或循环中重复`create_engine()`。
### 5.2 为什么使用URL.create()
本课使用:
```python
database_url = URL.create(
drivername="postgresql+psycopg",
username=database_config["user"],
password=database_config["password"],
host=database_config["host"],
port=database_config["port"],
database=database_config["dbname"],
)
```
`postgresql+psycopg`表示:
```text
数据库方言:PostgreSQL
DB-API驱动:Psycopg 3
```
结构化创建URL可以避免手工拼接连接串,也不用自己处理密码中的`@`、`:`等特殊字符。
## 六、声明式模型
先定义共同基类:
```python
class Base(DeclarativeBase):
pass
```
再定义映射模型:
```python
class Book(Base):
__tablename__ = "course_orm_book"
id: Mapped[int] = mapped_column(primary_key=True)
isbn: Mapped[str] = mapped_column(String(30), unique=True, nullable=False)
title: Mapped[str] = mapped_column(String(100), nullable=False)
```
拆开理解:
- `Book`是可以正常创建和使用的Python类;
- `__tablename__`指定数据库表名;
- `Mapped[str]`说明ORM属性映射的Python类型;
- `mapped_column()`描述数据库列和约束;
- `Base.metadata`收集所有模型的表元数据。
调用:
```python
Base.metadata.create_all(engine)
```
会创建缺失的表,但它不是完整的数据库迁移工具,不能可靠地把已有表自动升级成新结构。后续FastAPI阶段会学习数据库迁移。
## 七、Session是什么
Session不是数据库连接本身,也不是线程安全的全局单例。它主要负责:
- 从Engine申请连接;
- 开始和结束数据库事务;
- 保存当前持久化上下文中的ORM对象;
- 跟踪新增、修改和删除;
- 在合适时机把变化同步到数据库;
- 提交或回滚事务;
- 把连接归还连接池。
创建Session工厂:
```python
session_factory = sessionmaker(
engine,
expire_on_commit=False,
)
```
`sessionmaker`类似统一配置后的Session工厂。生产代码让每个请求或业务任务创建自己的Session,而不是让多个并发请求共享同一个Session。
### 7.1 身份映射
Session内部维护身份映射(Identity Map)。在同一个Session中,以相同主键加载同一行时,通常会得到同一个Python对象实例:
```python
first = session.get(Book, 1)
second = session.get(Book, 1)
print(first is second) # 通常为True
```
这与JPA持久化上下文中的实体身份概念相近。
### 7.2 工作单元
工作单元(Unit of Work)表示Session收集一组对象变化,再统一生成SQL:
```python
book = session.scalar(select(Book).where(Book.isbn == "ORM-001"))
book.price = Decimal("72.00")
```
这里只修改了Python属性,没有手写`UPDATE`。Session会识别变化,在`flush()`或提交时发送UPDATE。
## 八、Session事务写法
写操作推荐使用:
```python
with session_factory.begin() as session:
session.add(book)
```
其行为是:
```text
创建Session
→ 开始事务
→ 执行业务
→ 正常结束时flush并commit
→ 异常时rollback
→ 关闭Session
→ 连接归还连接池
```
只读查询可以使用:
```python
with session_factory() as session:
books = session.scalars(select(Book)).all()
```
离开Session的`with`会关闭Session并释放连接资源,但不会替你提交尚未提交的写操作。不要把“关闭Session”误认为“提交事务”。
## 九、flush与commit的区别
### 9.1 flush
```python
session.add(book)
session.flush()
print(book.id)
```
`flush()`把Session内待处理变化发送给数据库,例如执行INSERT并取得数据库生成的主键。但是:
- 当前事务仍未提交;
- 其他事务通常还看不到结果;
- 后续发生异常仍可以回滚。
### 9.2 commit
`commit()`先执行必要的`flush()`,然后提交数据库事务。提交成功后,本事务的修改成为持久结果。
### 9.3 rollback
`rollback()`撤销当前事务中未提交的数据库变化,并调整Session中的对象状态。事务失败后必须回滚或关闭Session,才能安全开始后续工作。
一句话记忆:
```text
flush:把变化发给数据库,但还可以回滚。
commit:确认事务结果,完成持久化。
```
## 十、ORM增删改查
### 10.1 新增
```python
book = Book(isbn="ORM-001", title="Python数据库编程", ...)
session.add(book)
```
多个对象使用:
```python
session.add_all([first_book, second_book])
```
### 10.2 查询
SQLAlchemy 2.x使用`select()`:
```python
statement = (
select(Book)
.where(Book.isbn.like("ORM-%"))
.order_by(Book.isbn)
)
books = session.scalars(statement).all()
```
不要在本课程中使用旧式:
```python
session.query(Book).filter(...)
```
`session.scalars()`适合只需要ORM对象的查询。`session.execute()`返回更通用的结果行。
### 10.3 修改
```python
book = session.scalar(select(Book).where(Book.isbn == "ORM-001"))
book.price = Decimal("72.00")
```
Session跟踪属性变化,在flush时生成UPDATE。
### 10.4 删除
```python
session.delete(book)
```
对象会被标记为删除,DELETE在flush时发送。
## 十一、expire_on_commit
SQLAlchemy默认`expire_on_commit=True`。事务提交后,Session中的对象属性会被标记为过期;下一次访问时,Session可能重新查询数据库获取最新值。
本课使用:
```python
sessionmaker(engine, expire_on_commit=False)
```
这样提交后对象仍可读取已经加载的值,适合当前命令行示例,也常用于Web响应层。但它不代表对象永远是数据库最新状态;如果其他事务修改了数据,需要重新查询或刷新。
## 十二、完整示例
运行[sqlalchemy_crud_example.py](./sqlalchemy_crud_example.py),会依次演示:
1. 创建Engine和连接池;
2. 创建Session工厂;
3. 根据模型创建缺失表;
4. 重置并新增课程图书;
5. `flush()`后读取数据库生成的ID;
6. 使用`select()`查询;
7. 修改对象属性并删除对象;
8. 主动抛出异常验证事务回滚;
9. 使用新Session回查最终数据。
## 十三、安装与配置
激活课程环境,在本课目录安装:
```powershell
python -m pip install -r .\requirements.txt
```
如果使用Conda管理SQLAlchemy:
```powershell
conda install -c conda-forge sqlalchemy
```
Psycopg已经在前两课安装完成。然后复制配置:
```powershell
Copy-Item .\config.example.toml .\config.toml
```
填写专用练习数据库信息。真实`config.toml`已被项目`.gitignore`排除。
## 十四、运行方法与预期结果
```powershell
python .\sqlalchemy_crud_example.py
```
关键输出类似:
```text
flush后第一本书ID:实际ID
新增后:
ORM-001|Python数据库编程|作者:小明|价格:68.00
ORM-002|SQLAlchemy实践|作者:小红|价格:88.00
修改并删除后:
ORM-001|Python数据库编程|作者:小明|价格:72.00
失败事务已回滚:模拟后续业务失败。
失败事务回滚后:
ORM-001|Python数据库编程|作者:小明|价格:72.00
```
数据库生成的ID不要求固定。重复运行会先清理`ORM-`前缀示例数据。
## 十五、常见错误
### 15.1 每个方法都创建Engine
Engine应当是应用级长生命周期对象。反复创建Engine会反复创建连接池,失去连接复用价值。
### 15.2 多个请求共享同一个Session
Session不是供多个线程或并发任务共享的全局对象。常见原则是每个线程一个Session,异步场景每个任务一个AsyncSession。
### 15.3 关闭Session却没有提交
```python
with session_factory() as session:
session.add(book)
```
离开时Session关闭,未提交写入会被回滚。写事务使用`session_factory.begin()`或明确调用`session.commit()`。
### 15.4 把flush当成commit
`flush()`只把SQL发送到当前事务,后续异常仍会回滚。
### 15.5 提交后访问过期对象
默认配置下,提交会使对象属性过期。Session已关闭后访问需要重新加载的属性,可能出现对象已脱离Session的错误。本课通过`expire_on_commit=False`降低入门干扰。
### 15.6 使用旧式Session.query()
当前课程统一使用SQLAlchemy 2.x的`select()`、`Session.scalar()`和`Session.scalars()`。
### 15.7 把create_all当作迁移工具
`create_all()`适合创建缺失表,不负责完整版本化迁移。生产项目修改表结构通常使用Alembic等迁移工具。
## 十六、课堂练习
打开[practice.py](./practice.py),完成商品ORM练习。题目按以下顺序组织:
1. 创建配置和声明式基类;
2. 定义Product模型;
3. 创建并初始化课程数据;
4. 使用`select()`查询;
5. 修改和删除ORM对象;
6. 验证`flush()`后的异常回滚;
7. 组织Engine、Session工厂和完整输出。
## 十七、本课小结
- Engine统一管理方言、驱动和连接池;
- Engine不是一条固定数据库连接;
- 声明式模型把Python类映射到数据库表;
- Session管理事务、身份映射和工作单元;
- `select()`是SQLAlchemy 2.x查询入口;
- 修改ORM对象属性后,Session能够跟踪变化;
- `flush()`发送SQL但不提交,`commit()`确认事务;
- Session的生命周期应位于业务函数之外;
- SQLAlchemy ORM更接近JPA/Hibernate,而不是MyBatis-Plus的直接复制。
## 十八、验收标准
- 能说明Psycopg、Engine、连接池和Session的调用层次;
- 能说明Engine为什么通常只创建一次;
- 能使用声明式模型完成类表映射;
- 能使用SQLAlchemy 2.x方式完成增删改查;
- 能解释身份映射和工作单元;
- 能区分`flush()`、`commit()`、`rollback()`和`close()`;
- 标准示例和练习的失败事务均能正确回滚;
- 未提交真实`config.toml`。