Files
PythonLearn/04_数据库/4_2_Python数据库事务与数据访问层/practice.py

131 lines
6.4 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-2 课练习:使用事务完成安全转账
#
# 本文件只提供题目,不包含导入、代码骨架、测试数据或参考答案。
# 本练习会创建 course_wallet_account 表,并只操作 PRACTICE- 前缀的数据。
# 请勿把表名改成现有业务表,也不要删除不属于本练习的数据。
# 第一部分:准备配置和异常
# 1. 导入 Decimal、Path、tomllib 和 psycopg。
# 2. 从 psycopg 导入 Connection。
# 3. 使用 Path(__file__).with_name("config.toml") 定义 CONFIG_PATH。
# 4. 定义 WalletError(Exception),类体只写 pass。
# 5. 实现 load_database_config(config_path),要求与第一课相同:
# - 文件不存在时抛出明确的 RuntimeError
# - 使用 tomllib 读取 [postgresql]
# - 配置节不是字典时抛出明确的 RuntimeError
# - 返回数据库配置字典。
# 第二部分:定义 WalletRepository
# 1. __init__(self, connection) 保存 Connection但不在 Repository 中创建连接。
# 2. create_table() 执行以下建表逻辑:
# - 表名 course_wallet_account
# - wallet_no VARCHAR(30) 主键;
# - owner_name VARCHAR(50) 非空;
# - balance NUMERIC(12, 2) 非空并且大于等于 0
# - 使用 CREATE TABLE IF NOT EXISTS。
# 3. reset_practice_wallets() 只删除 wallet_no LIKE 'PRACTICE-%' 的记录,
# 模式字符串必须作为参数传入,不得拼接 SQL。
# 4. add_wallets(wallets) 使用 with connection.cursor() 创建游标,
# 再调用 cursor.executemany() 批量新增。
# 5. find_balance_for_update(wallet_no) 使用参数化 SQL 和 FOR UPDATE
# - 找不到时抛出 WalletError("钱包不存在:{wallet_no}")
# - 找到时返回 Decimal 余额。
# 6. change_balance(wallet_no, amount) 使用 balance = balance + %s 更新余额;
# - cursor.rowcount 不等于 1 时抛出钱包不存在异常;
# - 本方法不调用 commit() 或 rollback()。
# 7. find_practice_wallets() 查询 PRACTICE- 前缀记录,按 wallet_no 排序并返回结果。
#
# Repository 边界提醒:
# - Repository 负责 SQL 和结果转换;
# - 不要在 add_wallets()、change_balance() 内提交事务;
# - 多个 Repository 操作需要由外层业务事务统一提交或回滚。
# 第三部分:定义 WalletTransferService
# 1. __init__(self, repository) 保存 WalletRepository。
# 2. transfer(self, source_no, target_no, amount) 按顺序执行:
# - amount <= 0 时抛出 WalletError("转账金额必须大于 0。")
# - 调用 find_balance_for_update(source_no),保存 source_balance
# - 调用 find_balance_for_update(target_no),确认目标钱包存在并锁定;
# - source_balance < amount 时抛出 WalletError("钱包余额不足。")
# - 调用 change_balance(source_no, -amount)
# - 调用 change_balance(target_no, amount)。
# 3. transfer() 不调用 commit() 或 rollback()。
# 第四部分:准备练习数据
# 1. 定义 prepare_data(database_config)。
# 2. 使用 with psycopg.connect(**database_config) as connection 管理事务。
# 3. 创建 WalletRepository(connection)。
# 4. 依次调用 create_table() 和 reset_practice_wallets()。
# 5. 调用 add_wallets() 批量新增:
# - ("PRACTICE-W001", "张三", Decimal("800.00"))
# - ("PRACTICE-W002", "李四", Decimal("300.00"))
# 6. 正常离开 with让初始化事务自动提交。
# 第五部分:完成成功事务
# 1. 定义 run_successful_transfer(database_config)。
# 2. 使用一个连接上下文创建 Repository 和 Service。
# 3. 调用:
# service.transfer("PRACTICE-W001", "PRACTICE-W002", Decimal("150.00"))
# 4. 正常离开 with不要手工调用 commit()。
# 5. 成功后两个余额应分别为 650.00 和 450.00。
# 第六部分:完成失败事务并验证回滚
# 1. 定义 run_failed_transfer(database_config)。
# 2. 在 try 中创建连接上下文和 Repository。
# 3. 先调用:
# repository.change_balance("PRACTICE-W001", Decimal("-50.00"))
# 4. 紧接着抛出 WalletError("模拟入账失败。")。
# 5. 在连接上下文外捕获 WalletError并输出
# “失败事务已回滚:模拟入账失败。”
# 6. 不要在 except 前捕获并吞掉异常,否则连接上下文无法自动回滚。
# 7. 回查后余额必须仍为 650.00 和 450.00,而不是 600.00 和 450.00。
# 第七部分:查询与 main() 输出
# 1. 定义 query_wallets(database_config),使用独立连接查询并返回练习钱包。
# 2. 定义 print_wallets(title, wallets),按以下格式逐行输出:
# “PRACTICE-W001张三余额800.00”。
# 3. main() 依次调用:
# - load_database_config(CONFIG_PATH)
# - prepare_data(database_config)
# - print_wallets("初始余额:", query_wallets(database_config))
# - run_successful_transfer(database_config)
# - print_wallets("成功转账 150 元后:", query_wallets(database_config))
# - run_failed_transfer(database_config)
# - print_wallets("失败事务回滚后:", query_wallets(database_config))。
# 4. 分别捕获配置异常和 psycopg.Error并输出中文场景说明。
# 5. 添加程序入口判断并调用 main()。
# 预期关键输出:
# 初始余额:
# PRACTICE-W001张三余额800.00
# PRACTICE-W002李四余额300.00
# 成功转账 150 元后:
# PRACTICE-W001张三余额650.00
# PRACTICE-W002李四余额450.00
# 失败事务已回滚:模拟入账失败。
# 失败事务回滚后:
# PRACTICE-W001张三余额650.00
# PRACTICE-W002李四余额450.00
# 自查清单:
# 1. Repository 是否复用外部传入的 Connection
# 2. Repository 和 Service 是否都没有自行提交事务?
# 3. 扣款和入账是否处于同一个连接上下文?
# 4. 失败异常是否离开连接 with 后才被捕获?
# 5. 是否使用 FOR UPDATE 锁定待修改账户?
# 6. 批量初始化是否使用 executemany()
# 7. 所有值是否通过参数化查询传入?
# 8. 是否只清理 PRACTICE- 前缀的练习数据?
# 最终验收标准:
# 1. practice.py 通过语法检查并能重复运行;
# 2. 初始化、成功转账和失败回滚的余额符合预期;
# 3. 失败事务没有保留第一条扣款更新;
# 4. Repository 只负责数据访问Service 负责业务规则;
# 5. 外层连接上下文控制事务提交和回滚;
# 6. SQL 全部参数化,不拼接业务数据;
# 7. 只操作本课专用表和 PRACTICE- 前缀数据;
# 8. config.toml 与真实连接信息没有进入 Git。