471 lines
13 KiB
Markdown
471 lines
13 KiB
Markdown
# 第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`。
|