Files
PythonLearn/04_数据库/4_5_数据库综合项目/practice.py
T

179 lines
8.8 KiB
Python
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-5课练习:数据库综合项目——库存订单管理
#
# 本文件只提供题目,不包含导入、代码骨架、测试数据代码或参考答案。
# 练习会创建course_shop_product、course_shop_order和course_shop_order_item三张表,
# 并只操作DBP-商品前缀及DBO-订单前缀的数据。
# 请勿改用现有业务表,也不要删除不属于本练习的数据。
# 第一部分:导入、配置与基础类型
# 1. 导入dataclass、Decimal、Path和tomllib。
# 2. 从sqlalchemy导入CheckConstraint、ForeignKey、Numeric、String、URL、
# create_engine、delete、func和select。
# 3. 从sqlalchemy.exc导入SQLAlchemyError。
# 4. 从sqlalchemy.orm导入DeclarativeBase、Mapped、Session、mapped_column、
# relationship、selectinload和sessionmaker。
# 5. 使用Path(__file__).with_name("config.toml")定义CONFIG_PATH。
# 6. 定义Base(DeclarativeBase)。
# 7. 定义OrderError(Exception),用于表达商品不存在、库存不足等业务失败。
# 8. 实现load_database_config(config_path),读取[postgresql]配置。
# 9. 实现create_database_url(database_config),使用URL.create()创建连接地址。
# 第二部分:定义三个ORM模型
# 1. 定义Product(Base),表名course_shop_product:
# - id:int主键;
# - product_code:最长30字符,唯一且非空;
# - product_name:最长100字符且非空;
# - price:NUMERIC(10, 2)且非空;
# - stock:int且非空;
# - 使用CheckConstraint保证price和stock都大于等于0;
# - items:与OrderItem建立双向一对多关系。
# 2. 定义Order(Base),表名course_shop_order:
# - id:int主键;
# - order_no:最长30字符,唯一且非空;
# - customer_name:最长100字符且非空;
# - total_amount:NUMERIC(12, 2)且非空;
# - status:最长20字符且非空;
# - items:与OrderItem建立双向一对多关系;
# - 配置cascade="all, delete-orphan"。
# 3. 定义OrderItem(Base),表名course_shop_order_item:
# - id:int主键;
# - order_id:外键引用course_shop_order.id,非空;
# - product_id:外键引用course_shop_product.id,非空;
# - quantity:int且非空,使用CheckConstraint保证大于0;
# - unit_price:NUMERIC(10, 2)且非空;
# - order:与Order.items互为双向关系;
# - product:与Product.items互为双向关系。
# 第三部分:定义DTO
# 1. 使用@dataclass(frozen=True)定义OrderDetailDTO。
# 2. DTO包含order_no、customer_name、product_code、product_name、quantity、
# unit_price、line_amount和status。
# 3. DTO不继承Base,不承担数据库持久化职责。
# 4. line_amount由查询结果中的unit_price乘以quantity得到。
# 第四部分:实现ProductRepository
# 1. 构造方法接收并保存外部传入的Session。
# 2. find_by_code_for_update(product_code):
# - 使用select(Product).where(...)查询商品;
# - 调用with_for_update()锁定商品行;
# - 找不到时抛出OrderError("商品不存在:{product_code}");
# - 返回Product对象。
# 3. find_practice_products()查询DBP-前缀商品并按商品编号排序。
# 4. Repository中不得创建Session,不得调用commit()或rollback()。
# 第五部分:实现OrderRepository
# 1. 构造方法接收并保存外部传入的Session。
# 2. exists_by_order_no(order_no)判断订单编号是否已经存在。
# 3. add(order)调用session.add(order),但不提交事务。
# 4. find_order_details()使用显式join查询订单、明细和商品:
# - 只查询DBO-前缀订单;
# - 只选择DTO所需字段;
# - 按订单编号和明细ID排序;
# - 把结果转换成OrderDetailDTO列表。
# 5. count_orders_by_status()使用func.count()和group_by()统计各状态订单数。
# 第六部分:实现OrderService下单业务
# 1. 构造方法接收ProductRepository和OrderRepository。
# 2. 定义place_order(order_no, customer_name, requests),其中requests是
# “商品编号、购买数量”组成的列表。
# 3. 订单编号已存在时抛出OrderError("订单已存在:{order_no}")。
# 4. requests为空时抛出OrderError("订单至少需要一项商品。")。
# 5. 逐项处理购买请求:
# - 数量小于等于0时抛出OrderError("购买数量必须大于0。");
# - 调用find_by_code_for_update()查询并锁定商品;
# - 库存不足时抛出OrderError("商品库存不足:{product_code}");
# - 商品库存减去购买数量;
# - 使用商品当前价格创建OrderItem;
# - 累加订单总金额。
# 6. 创建status="CREATED"的Order并关联全部OrderItem。
# 7. 调用OrderRepository.add(order),不在Service中提交事务。
# 第七部分:准备数据和验证事务
# 1. 定义reset_and_add_products(session):
# - 先删除DBO-前缀订单对应的订单明细;
# - 再删除DBO-前缀订单;
# - 最后删除DBP-前缀商品;
# - 新增DBP-001机械键盘,价格399.00,库存10;
# - 新增DBP-002无线鼠标,价格199.00,库存5;
# - 全过程不调用commit()。
# 2. 定义run_successful_order(session_factory):
# - 使用with session_factory.begin() as session管理事务;
# - 创建两个Repository和OrderService;
# - 创建订单DBO-001,客户张三,购买2个DBP-001和1个DBP-002;
# - 正常离开with,让事务自动提交;
# - 成功后键盘库存为8,鼠标库存为4,订单金额为997.00。
# 3. 定义run_failed_order(session_factory):
# - 在try中使用with session_factory.begin() as session;
# - 创建订单DBO-002,先购买1个DBP-001,再购买99个DBP-002;
# - 第二项因库存不足抛出OrderError;
# - 在事务with外捕获OrderError并输出失败信息;
# - 整个订单事务必须回滚,键盘库存仍为8,且DBO-002不能存在。
# 第八部分:输出和main()流程
# 1. 定义print_products(title, products),输出商品编号、名称、价格和库存。
# 2. 定义print_order_details(details),输出DTO中的订单和明细信息。
# 3. 定义print_order_counts(counts),输出“状态|订单数量:数字”。
# 4. main()依次执行:
# - 读取TOML配置并创建数据库URL;
# - 创建一次Engine并启用pool_pre_ping;
# - 使用sessionmaker(engine, expire_on_commit=False)创建Session工厂;
# - 调用Base.metadata.create_all(engine);
# - 在一个事务中重置数据并新增练习商品;
# - 输出初始库存;
# - 执行成功订单并输出扣减后的库存;
# - 执行失败订单并输出回滚后的库存;
# - 查询并输出订单DTO和状态统计;
# - 分类捕获配置异常、OrderError和SQLAlchemyError;
# - 在finally中调用engine.dispose()。
# 5. 添加程序入口判断并调用main()。
#
# 预期关键输出:
# 初始库存:
# DBP-001|机械键盘|价格:399.00|库存:10
# DBP-002|无线鼠标|价格:199.00|库存:5
# 成功订单提交后:
# DBP-001|机械键盘|价格:399.00|库存:8
# DBP-002|无线鼠标|价格:199.00|库存:4
# 失败订单已回滚:商品库存不足:DBP-002
# 失败订单回滚后:
# DBP-001|机械键盘|价格:399.00|库存:8
# DBP-002|无线鼠标|价格:199.00|库存:4
# 订单明细DTO:
# DBO-001|张三|DBP-001|机械键盘|数量:2|单价:399.00|小计:798.00|CREATED
# DBO-001|张三|DBP-002|无线鼠标|数量:1|单价:199.00|小计:199.00|CREATED
# 订单状态统计:
# CREATED|订单数量:1
# 自查清单:
# 1. 三个ORM模型是否建立了真实外键和双向对象关系?
# 2. 金额是否全部使用Decimal和NUMERIC,而不是float?
# 3. 商品查询是否使用FOR UPDATE锁定待扣减库存的记录?
# 4. Repository和Service是否都没有自行提交事务?
# 5. 一张订单的全部库存扣减和订单新增是否处于同一个事务?
# 6. 失败订单中第一项库存扣减是否也被回滚?
# 7. 订单明细查询是否使用join()并转换成DTO?
# 8. 状态统计是否由数据库完成count()和group_by()?
# 9. 程序是否只清理DBP-和DBO-前缀的练习数据?
# 10. 配置是否来自被Git忽略的本地config.toml?
# 最终验收标准:
# 1. practice.py通过语法检查并能连续运行两次;
# 2. 三张表的字段、约束、外键和关系映射正确;
# 3. 成功订单正确保存订单、明细并扣减库存;
# 4. 失败订单完全回滚,不保存订单且不改变任何库存;
# 5. DTO联表查询结果和订单状态统计符合预期;
# 6. Repository负责持久化,Service负责业务规则,调用方负责事务;
# 7. SQLAlchemy查询使用2.x写法,不使用session.query();
# 8. 所有练习数据与现有数据安全隔离;
# 9. config.toml与真实连接信息没有进入Git。