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