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
@@ -0,0 +1,162 @@
# 第4-3课练习:使用SQLAlchemy 2.x管理课程商品
#
# 本文件只提供题目,不包含导入、代码骨架、测试数据或参考答案。
# 练习会创建course_orm_product表,并只操作ORM-P-前缀的数据。
# 请勿改用现有业务表,也不要删除不属于本练习的数据。
# 第一部分:导入、配置与声明式基类
# 1. 导入Decimal、Path和tomllib。
# 2. 从sqlalchemy导入Numeric、String、URL、create_engine、delete和select。
# 3. 从sqlalchemy.orm导入DeclarativeBase、Mapped、Session、mapped_column和sessionmaker。
# 4. 使用Path(__file__).with_name("config.toml")定义CONFIG_PATH。
# 5. 定义Base(DeclarativeBase),类体中不添加业务字段。
# 6. 实现load_database_config(config_path),读取并返回[postgresql]配置字典。
# 7. 实现create_database_url(database_config),必须调用URL.create()并指定:
# - drivername="postgresql+psycopg";
# - username、password、host、port、database分别来自配置;
# - 返回URL对象,不手工拼接包含密码的字符串。
# 第二部分:定义Product ORM模型
# 1. 定义Product(Base)。
# 2. 设置__tablename__ = "course_orm_product"。
# 3. 使用Mapped和mapped_column()声明:
# - id:int主键,由数据库生成;
# - code:最长30字符,唯一且非空;
# - name:最长100字符且非空;
# - price:NUMERIC(10, 2)且非空;
# - stock:int且非空。
# 4. 定义__repr__,至少包含id、code、name和stock,返回字符串供调试使用。
#
# Java对照提醒:
# - Product既是普通Python类,也是数据库映射模型;
# - Mapped类似声明持久化属性的类型;
# - mapped_column()描述列约束,不等于Java字段的setter。
# 第三部分:创建并初始化课程数据
# 1. 定义reset_practice_data(session),调用session.execute()执行:
# delete(Product).where(Product.code.like("ORM-P-%"))。
# 2. 定义add_products(session),创建并保存以下两个Product:
# - code="ORM-P-001",name="机械键盘",price=Decimal("399.00"),stock=10;
# - code="ORM-P-002",name="无线鼠标",price=Decimal("199.00"),stock=20。
# 3. 调用session.add_all(products)。
# 4. 调用session.flush(),然后读取第一件商品的id并输出:
# “flush后第一件商品ID:实际ID”。
# 5. 不在add_products()中调用commit()。
# 第四部分:实现查询
# 1. 定义find_products(session),创建SQLAlchemy 2.x查询语句:
# select(Product).where(Product.code.like("ORM-P-%")).order_by(Product.code)
# 2. 调用session.scalars(statement).all()取得Product对象。
# 3. 返回Product对象列表;没有数据时返回空列表,不返回None。
# 4. 不使用旧式session.query()。
# 第五部分:实现修改和删除
# 1. 定义update_product(session):
# - 调用session.scalar(select(Product).where(Product.code == "ORM-P-001"));
# - 找不到时抛出RuntimeError("没有找到商品ORM-P-001。");
# - 找到后直接把stock属性修改为8;
# - 不手写UPDATE SQL,也不在方法中提交。
# 2. 定义delete_product(session):
# - 查询code为ORM-P-002的Product;
# - 找不到时抛出RuntimeError("没有找到商品ORM-P-002。");
# - 调用session.delete(product);
# - 不手写DELETE SQL,也不在方法中提交。
# 第六部分:验证事务回滚
# 1. 定义demonstrate_rollback(session_factory)。
# 2. 在try中使用with session_factory.begin() as session管理事务。
# 3. 查询ORM-P-001,把stock修改为0。
# 4. 调用session.flush(),让UPDATE先发送给数据库。
# 5. 紧接着抛出RuntimeError("模拟库存业务失败。")。
# 6. 在事务with外捕获RuntimeError并输出:
# “失败事务已回滚:模拟库存业务失败。”
# 7. 最终回查时stock必须仍为8,而不是0。
# 第七部分:输出和main()流程
# 1. 定义print_products(title, products),先print(title),再逐个输出:
# “ORM-P-001|机械键盘|价格:399.00|库存:10”。
# 2. main()依次执行:
# - 读取TOML配置并创建URL;
# - 调用create_engine(),除database_url外还要传入:
# connect_args={"connect_timeout": 配置值或默认值10}、pool_size=5、
# max_overflow=5、pool_pre_ping=True、echo=False;
# - 调用sessionmaker(engine, expire_on_commit=False)创建Session工厂;
# - 调用Base.metadata.create_all(engine)创建缺失的课程表;
# - 使用with session_factory.begin() as session重置并新增商品;
# - 使用with session_factory() as session查询并输出“新增后:”;
# - 使用新的session_factory.begin()事务修改和删除;
# - 使用只读Session查询并输出“修改并删除后:”;
# - 调用demonstrate_rollback(session_factory);
# - 最后查询并输出“失败事务回滚后:”。
# 3. 在程序边界分别处理配置错误和数据库访问错误。
# 4. 添加程序入口判断并调用main()。
# 预期关键输出:
# flush后第一件商品ID:实际ID
# 新增后:
# ORM-P-001|机械键盘|价格:399.00|库存:10
# ORM-P-002|无线鼠标|价格:199.00|库存:20
# 修改并删除后:
# ORM-P-001|机械键盘|价格:399.00|库存:8
# 失败事务已回滚:模拟库存业务失败。
# 失败事务回滚后:
# ORM-P-001|机械键盘|价格:399.00|库存:8
# 自查清单:
# 1. 是否使用SQLAlchemy 2.x的DeclarativeBase、Mapped和mapped_column()?
# 2. Engine是否只创建一次,并启用了pool_pre_ping?
# 3. 是否使用sessionmaker统一创建Session?
# 4. 是否使用select()和session.scalars(),而不是session.query()?
# 5. 修改属性后是否由Session自动识别变化?
# 6. Repository式函数中是否没有擅自提交事务?
# 7. flush后是否能取得数据库生成的主键,但事务仍可回滚?
# 8. 失败事务回滚后库存是否仍为8?
# 9. 是否只清理ORM-P-前缀的练习数据?
# 最终验收标准:
# 1. practice.py通过语法检查并能重复运行;
# 2. 模型字段与数据库表映射正确;
# 3. 新增、查询、修改和删除结果符合预期;
# 4. Session事务成功时提交、异常时回滚;
# 5. 能解释Engine、连接池和Session的职责;
# 6. 能解释flush()与commit()的区别;
# 7. 不使用SQLAlchemy 1.x旧式查询写法;
# 8. config.toml与真实数据库信息没有进入Git。