# 第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`。