第4-3课:SQLAlchemy基础
一、本课定位
前两课直接使用Psycopg,已经理解连接、游标、参数化SQL、事务和Repository分层。本课开始使用SQLAlchemy 2.x,将生产项目常用的连接池、SQL工具和对象关系映射(Object Relational Mapping,ORM)引入课程。
本课不会隐藏底层原理。SQLAlchemy最终仍通过Psycopg连接PostgreSQL:
业务代码
↓
SQLAlchemy ORM和Session
↓
SQLAlchemy Engine与连接池
↓
Psycopg
↓
PostgreSQL
本课示例会创建course_orm_book表,只重置ORM-前缀课程数据;练习创建course_orm_product表,只操作ORM-P-前缀数据。不要连接生产数据库。
二、本课目标
完成本课后,你将能够:
- 解释SQLAlchemy Core和ORM的关系;
- 使用
Engine统一管理数据库方言和连接池; - 使用声明式模型映射Python类与数据库表;
- 使用
Session管理ORM对象和事务; - 使用SQLAlchemy 2.x的
select()查询对象; - 完成ORM新增、查询、修改和删除;
- 区分
flush()、commit()、rollback()和close(); - 理解Session的工作单元和身份映射;
- 对照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:
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()
本课使用:
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表示:
数据库方言:PostgreSQL
DB-API驱动:Psycopg 3
结构化创建URL可以避免手工拼接连接串,也不用自己处理密码中的@、:等特殊字符。
六、声明式模型
先定义共同基类:
class Base(DeclarativeBase):
pass
再定义映射模型:
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收集所有模型的表元数据。
调用:
Base.metadata.create_all(engine)
会创建缺失的表,但它不是完整的数据库迁移工具,不能可靠地把已有表自动升级成新结构。后续FastAPI阶段会学习数据库迁移。
七、Session是什么
Session不是数据库连接本身,也不是线程安全的全局单例。它主要负责:
- 从Engine申请连接;
- 开始和结束数据库事务;
- 保存当前持久化上下文中的ORM对象;
- 跟踪新增、修改和删除;
- 在合适时机把变化同步到数据库;
- 提交或回滚事务;
- 把连接归还连接池。
创建Session工厂:
session_factory = sessionmaker(
engine,
expire_on_commit=False,
)
sessionmaker类似统一配置后的Session工厂。生产代码让每个请求或业务任务创建自己的Session,而不是让多个并发请求共享同一个Session。
7.1 身份映射
Session内部维护身份映射(Identity Map)。在同一个Session中,以相同主键加载同一行时,通常会得到同一个Python对象实例:
first = session.get(Book, 1)
second = session.get(Book, 1)
print(first is second) # 通常为True
这与JPA持久化上下文中的实体身份概念相近。
7.2 工作单元
工作单元(Unit of Work)表示Session收集一组对象变化,再统一生成SQL:
book = session.scalar(select(Book).where(Book.isbn == "ORM-001"))
book.price = Decimal("72.00")
这里只修改了Python属性,没有手写UPDATE。Session会识别变化,在flush()或提交时发送UPDATE。
八、Session事务写法
写操作推荐使用:
with session_factory.begin() as session:
session.add(book)
其行为是:
创建Session
→ 开始事务
→ 执行业务
→ 正常结束时flush并commit
→ 异常时rollback
→ 关闭Session
→ 连接归还连接池
只读查询可以使用:
with session_factory() as session:
books = session.scalars(select(Book)).all()
离开Session的with会关闭Session并释放连接资源,但不会替你提交尚未提交的写操作。不要把“关闭Session”误认为“提交事务”。
九、flush与commit的区别
9.1 flush
session.add(book)
session.flush()
print(book.id)
flush()把Session内待处理变化发送给数据库,例如执行INSERT并取得数据库生成的主键。但是:
- 当前事务仍未提交;
- 其他事务通常还看不到结果;
- 后续发生异常仍可以回滚。
9.2 commit
commit()先执行必要的flush(),然后提交数据库事务。提交成功后,本事务的修改成为持久结果。
9.3 rollback
rollback()撤销当前事务中未提交的数据库变化,并调整Session中的对象状态。事务失败后必须回滚或关闭Session,才能安全开始后续工作。
一句话记忆:
flush:把变化发给数据库,但还可以回滚。
commit:确认事务结果,完成持久化。
十、ORM增删改查
10.1 新增
book = Book(isbn="ORM-001", title="Python数据库编程", ...)
session.add(book)
多个对象使用:
session.add_all([first_book, second_book])
10.2 查询
SQLAlchemy 2.x使用select():
statement = (
select(Book)
.where(Book.isbn.like("ORM-%"))
.order_by(Book.isbn)
)
books = session.scalars(statement).all()
不要在本课程中使用旧式:
session.query(Book).filter(...)
session.scalars()适合只需要ORM对象的查询。session.execute()返回更通用的结果行。
10.3 修改
book = session.scalar(select(Book).where(Book.isbn == "ORM-001"))
book.price = Decimal("72.00")
Session跟踪属性变化,在flush时生成UPDATE。
10.4 删除
session.delete(book)
对象会被标记为删除,DELETE在flush时发送。
十一、expire_on_commit
SQLAlchemy默认expire_on_commit=True。事务提交后,Session中的对象属性会被标记为过期;下一次访问时,Session可能重新查询数据库获取最新值。
本课使用:
sessionmaker(engine, expire_on_commit=False)
这样提交后对象仍可读取已经加载的值,适合当前命令行示例,也常用于Web响应层。但它不代表对象永远是数据库最新状态;如果其他事务修改了数据,需要重新查询或刷新。
十二、完整示例
运行sqlalchemy_crud_example.py,会依次演示:
- 创建Engine和连接池;
- 创建Session工厂;
- 根据模型创建缺失表;
- 重置并新增课程图书;
flush()后读取数据库生成的ID;- 使用
select()查询; - 修改对象属性并删除对象;
- 主动抛出异常验证事务回滚;
- 使用新Session回查最终数据。
十三、安装与配置
激活课程环境,在本课目录安装:
python -m pip install -r .\requirements.txt
如果使用Conda管理SQLAlchemy:
conda install -c conda-forge sqlalchemy
Psycopg已经在前两课安装完成。然后复制配置:
Copy-Item .\config.example.toml .\config.toml
填写专用练习数据库信息。真实config.toml已被项目.gitignore排除。
十四、运行方法与预期结果
python .\sqlalchemy_crud_example.py
关键输出类似:
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却没有提交
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,完成商品ORM练习。题目按以下顺序组织:
- 创建配置和声明式基类;
- 定义Product模型;
- 创建并初始化课程数据;
- 使用
select()查询; - 修改和删除ORM对象;
- 验证
flush()后的异常回滚; - 组织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。