Files
PythonLearn/04_数据库/4_3_SQLAlchemy基础/README.md
T

471 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第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`。