feat(数据库): 新增第四阶段前两课教学内容
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,3 +14,6 @@ venv/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# 数据库课程的本地 TOML 配置包含远程数据库账号和密码,不应提交。
|
||||||
|
04_数据库/**/config.toml
|
||||||
|
|||||||
286
04_数据库/4_1_PostgreSQL与Psycopg入门/README.md
Normal file
286
04_数据库/4_1_PostgreSQL与Psycopg入门/README.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# 第 4-1 课:PostgreSQL 与 Psycopg 入门
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
你已经掌握 SQL、事务和 Java 数据库开发,因此本课不再从表、字段和增删改查讲起,而是集中回答一个问题:Python 程序怎样安全地连接 PostgreSQL 并执行 SQL?
|
||||||
|
|
||||||
|
Python 数据库 API(Database API,DB-API)规定了数据库驱动的通用操作方式。Psycopg 3 是 PostgreSQL 的 Python 驱动,本课会把它与 JDBC 逐项对照。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 解释 DB-API、Psycopg 和 PostgreSQL 的关系;
|
||||||
|
2. 使用本地 TOML 文件保存数据库连接配置;
|
||||||
|
3. 使用 `psycopg.connect()` 创建连接;
|
||||||
|
4. 使用游标执行只读 SQL 并取得结果;
|
||||||
|
5. 使用参数化查询传递数据;
|
||||||
|
6. 使用 `with` 自动释放连接和游标;
|
||||||
|
7. 对照 JDBC 理解 Python 数据库代码;
|
||||||
|
8. 识别连接失败、依赖缺失和参数占位符错误。
|
||||||
|
|
||||||
|
## 三、前置知识
|
||||||
|
|
||||||
|
本课默认已经掌握:
|
||||||
|
|
||||||
|
- PostgreSQL 数据库地址、端口、数据库、用户名和密码的含义;
|
||||||
|
- `SELECT` 基础语法;
|
||||||
|
- JDBC 的 `Connection`、`PreparedStatement` 和 `ResultSet`;
|
||||||
|
- Python 函数、异常、上下文管理器和文件读取基础。
|
||||||
|
|
||||||
|
本课只连接专用练习数据库。不要连接生产数据库,也不要使用具有创建用户、删除数据库等高权限的账号。
|
||||||
|
|
||||||
|
## 四、DB-API、Psycopg 与 JDBC
|
||||||
|
|
||||||
|
DB-API 是 Python 数据库驱动遵循的接口约定,不是一个需要单独安装的框架。不同数据库有不同驱动,但常见操作方式比较统一。
|
||||||
|
|
||||||
|
| Java/JDBC | Python/Psycopg | 作用 |
|
||||||
|
|---|---|---|
|
||||||
|
| PostgreSQL JDBC Driver | Psycopg 3 | 与 PostgreSQL 通信 |
|
||||||
|
| `DriverManager.getConnection()` | `psycopg.connect()` | 创建数据库连接 |
|
||||||
|
| `Connection` | `Connection` | 表示一次数据库会话 |
|
||||||
|
| `PreparedStatement` | `Cursor.execute(sql, params)` | 执行参数化 SQL |
|
||||||
|
| `ResultSet` | `Cursor` 和 `fetchone()` 等方法 | 读取查询结果 |
|
||||||
|
| `try-with-resources` | `with` | 自动释放资源 |
|
||||||
|
| `SQLException` | `psycopg.Error` | 表示数据库访问错误 |
|
||||||
|
|
||||||
|
Psycopg 的游标(Cursor)同时承担“执行 SQL”和“读取结果”的职责。它不是数据库界面中的鼠标光标。
|
||||||
|
|
||||||
|
## 五、准备远程练习数据库
|
||||||
|
|
||||||
|
建议为课程准备:
|
||||||
|
|
||||||
|
- 一个独立数据库,例如 `python_course`;
|
||||||
|
- 一个专用账号,例如 `python_student`;
|
||||||
|
- 只授予课程所需权限;
|
||||||
|
- 不与生产环境或其他重要测试数据共用。
|
||||||
|
|
||||||
|
本课不使用环境变量,也不要求把所有信息拼成数据库连接串,而是把连接参数分字段写入 TOML 配置:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[postgresql]
|
||||||
|
host = "数据库主机"
|
||||||
|
port = 5432
|
||||||
|
dbname = "数据库名"
|
||||||
|
user = "用户名"
|
||||||
|
password = "密码"
|
||||||
|
connect_timeout = 10
|
||||||
|
```
|
||||||
|
|
||||||
|
仓库中的 [config.example.toml](./config.example.toml) 只包含占位内容,可以提交 Git。实际配置写入同目录的 `config.toml`,该文件已经被项目 `.gitignore` 排除。
|
||||||
|
|
||||||
|
TOML(Tom's Obvious Minimal Language)是一种结构化配置格式。Python 3.11 及以上版本内置 `tomllib`,读取 TOML 不需要安装额外依赖,也不会修改操作系统或当前进程的环境变量。
|
||||||
|
|
||||||
|
## 六、安装 Psycopg 3
|
||||||
|
|
||||||
|
你当前使用 Conda 的 `base` 环境,可以直接安装 Psycopg:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda install -n base -c conda-forge "psycopg>=3,<4" psycopg-c
|
||||||
|
```
|
||||||
|
|
||||||
|
安装完成后验证:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -c "import psycopg; print(psycopg.__version__)"
|
||||||
|
```
|
||||||
|
|
||||||
|
如果以后改用 Python 虚拟环境,也可以使用 `requirements.txt` 安装。无论使用哪种方式,导入时都写 `import psycopg`,不是 `import psycopg3`。
|
||||||
|
|
||||||
|
## 七、创建本地 TOML 配置
|
||||||
|
|
||||||
|
进入本课目录,复制配置模板:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .\config.example.toml .\config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
然后只在本地 `config.toml` 中填写真实的主机、端口、数据库名、用户名和密码。程序通过 Python 文件自身的位置寻找配置,因此从项目根目录或课程目录启动都可以。
|
||||||
|
|
||||||
|
### 7.1 为什么选择 TOML
|
||||||
|
|
||||||
|
- Python 3.13 可以直接使用内置 `tomllib`;
|
||||||
|
- 字段和类型清楚,端口可以保持整数;
|
||||||
|
- 不需要污染环境变量;
|
||||||
|
- 比 XML 简洁,比 YAML 少一个第三方解析依赖;
|
||||||
|
- 配置节结构与 Java 项目的 YAML、Properties 配置思路相近。
|
||||||
|
|
||||||
|
### 7.2 代码硬编码可以怎么写
|
||||||
|
|
||||||
|
从技术上可以直接构造字典:
|
||||||
|
|
||||||
|
```python
|
||||||
|
database_config = {
|
||||||
|
"host": "数据库主机",
|
||||||
|
"port": 5432,
|
||||||
|
"dbname": "数据库名",
|
||||||
|
"user": "用户名",
|
||||||
|
"password": "密码",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这能帮助理解 `psycopg.connect()` 接收哪些参数,但真实密码一旦硬编码(Hard Coding)就可能进入 Git 历史。本课程标准示例使用 `config.toml`;如自行尝试硬编码,只能放在不提交的个人练习文件中。
|
||||||
|
|
||||||
|
## 八、完整示例
|
||||||
|
|
||||||
|
示例文件为 [connection_example.py](./connection_example.py)。核心结构如下:
|
||||||
|
|
||||||
|
```python
|
||||||
|
database_config = load_database_config(CONFIG_PATH)
|
||||||
|
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT current_database(), current_user, %s::text",
|
||||||
|
("Psycopg 连接成功",),
|
||||||
|
)
|
||||||
|
database_name, user_name, message = cursor.fetchone()
|
||||||
|
```
|
||||||
|
|
||||||
|
示例只读取当前数据库名和当前用户,不创建表、不修改数据。
|
||||||
|
|
||||||
|
### 8.1 为什么参数使用 `%s`
|
||||||
|
|
||||||
|
Psycopg 使用 `%s` 表示值参数,即使参数是整数也仍然使用 `%s`。参数值通过 `execute()` 的第二个参数单独传入:
|
||||||
|
|
||||||
|
```python
|
||||||
|
cursor.execute("SELECT %s::text", (message,))
|
||||||
|
```
|
||||||
|
|
||||||
|
不要使用 f-string、字符串拼接或 `%` 运算符把数据直接写进 SQL:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误示例:数据被直接拼进 SQL,可能产生 SQL 注入。
|
||||||
|
cursor.execute(f"SELECT '{message}'")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 单个参数为什么有逗号
|
||||||
|
|
||||||
|
```python
|
||||||
|
(message,)
|
||||||
|
```
|
||||||
|
|
||||||
|
这是只有一个元素的元组。写成 `(message)` 只是在字符串外加括号,不是元组。
|
||||||
|
|
||||||
|
### 8.3 with 做了什么
|
||||||
|
|
||||||
|
- 离开游标的 `with` 时关闭游标;
|
||||||
|
- 离开连接的 `with` 时结束事务并关闭连接;
|
||||||
|
- 正常离开连接块时提交当前事务;
|
||||||
|
- 块内出现异常时回滚当前事务。
|
||||||
|
|
||||||
|
本课执行的是只读查询,但仍要建立正确的资源和事务管理习惯。
|
||||||
|
|
||||||
|
## 九、运行方法
|
||||||
|
|
||||||
|
确认 `config.toml` 已经创建并填写完成,然后在项目根目录运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\04_数据库\4_1_PostgreSQL与Psycopg入门\connection_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
正常情况下会看到类似结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
连接成功。
|
||||||
|
当前数据库:python_course
|
||||||
|
当前用户:python_student
|
||||||
|
参数化查询结果:Psycopg 连接成功
|
||||||
|
```
|
||||||
|
|
||||||
|
数据库名和用户名应以你的远程练习环境为准。
|
||||||
|
|
||||||
|
## 十、关键执行顺序
|
||||||
|
|
||||||
|
1. `main()` 调用 `load_database_config(CONFIG_PATH)`;
|
||||||
|
2. `Path` 根据当前 Python 文件的位置定位 `config.toml`;
|
||||||
|
3. `tomllib.load()` 读取 `[postgresql]` 配置节;
|
||||||
|
4. 缺少文件、配置节或必填字段时主动抛出中文 `RuntimeError`;
|
||||||
|
5. `psycopg.connect(**database_config)` 把字典展开为连接参数;
|
||||||
|
6. `connection.cursor()` 创建游标;
|
||||||
|
7. `cursor.execute()` 将 SQL 和查询参数分别交给驱动;
|
||||||
|
8. `cursor.fetchone()` 读取一行结果;
|
||||||
|
9. 两层 `with` 依次释放游标和连接;
|
||||||
|
10. `main()` 输出结果,并分别处理配置异常和数据库异常。
|
||||||
|
|
||||||
|
## 十一、常见错误
|
||||||
|
|
||||||
|
### 11.1 `ModuleNotFoundError: No module named 'psycopg'`
|
||||||
|
|
||||||
|
含义:当前 Python 环境没有安装 Psycopg。
|
||||||
|
|
||||||
|
处理:激活 `.venv`,再使用本课的 `requirements.txt` 安装依赖。可以运行 `python -m pip show psycopg` 检查安装位置。
|
||||||
|
|
||||||
|
### 11.2 未找到 `config.toml`
|
||||||
|
|
||||||
|
含义:本课目录中还没有实际配置文件。
|
||||||
|
|
||||||
|
处理:把 `config.example.toml` 复制为 `config.toml` 并填写连接参数。不要直接把真实信息写进示例模板。
|
||||||
|
|
||||||
|
### 11.3 TOML 格式错误
|
||||||
|
|
||||||
|
含义:配置不符合 TOML 语法,例如字符串缺少引号或同一个键重复出现。
|
||||||
|
|
||||||
|
处理:对照 `config.example.toml` 检查配置节、等号、引号和字段名。
|
||||||
|
|
||||||
|
### 11.4 `connection refused` 或连接超时
|
||||||
|
|
||||||
|
含义:程序无法到达数据库地址和端口。
|
||||||
|
|
||||||
|
检查数据库服务、主机名、端口、防火墙、白名单和 VPN,不要先假设一定是密码错误。
|
||||||
|
|
||||||
|
### 11.5 `password authentication failed`
|
||||||
|
|
||||||
|
含义:服务器已经收到连接,但用户名或密码校验失败。
|
||||||
|
|
||||||
|
检查 `config.toml` 中的账号、密码和数据库名。分字段传参时无需手动拼接 URL,也避免了连接串中特殊字符编码问题。
|
||||||
|
|
||||||
|
### 11.6 `execute()` 参数写错
|
||||||
|
|
||||||
|
下面两种写法都不符合本课要求:
|
||||||
|
|
||||||
|
```python
|
||||||
|
cursor.execute("SELECT '%s'", (message,))
|
||||||
|
cursor.execute("SELECT %s", message)
|
||||||
|
```
|
||||||
|
|
||||||
|
占位符外不要加引号,参数序列只有一个值时要写成 `(message,)`。
|
||||||
|
|
||||||
|
### 11.7 使用 `fetchone()` 却没有处理空结果
|
||||||
|
|
||||||
|
`fetchone()` 在没有数据时可能返回 `None`。本课查询必然返回一行,所以可以直接解包;以后查询业务表时必须判断空结果。
|
||||||
|
|
||||||
|
## 十二、课堂练习
|
||||||
|
|
||||||
|
打开 [practice.py](./practice.py),按照注释完成练习。练习要求你独立完成:
|
||||||
|
|
||||||
|
1. 使用 `Path` 定位本地 `config.toml`;
|
||||||
|
2. 使用 `tomllib` 读取 PostgreSQL 配置;
|
||||||
|
3. 使用两层 `with` 管理连接和游标;
|
||||||
|
4. 执行包含两个参数的只读查询;
|
||||||
|
5. 使用 `fetchone()` 保存并输出结果;
|
||||||
|
6. 分别捕获配置读取异常和数据库访问异常。
|
||||||
|
|
||||||
|
练习仍然只执行只读 SQL,不创建、修改或删除远程数据。
|
||||||
|
|
||||||
|
## 十三、本课小结
|
||||||
|
|
||||||
|
- DB-API 是 Python 数据库驱动的通用接口约定;
|
||||||
|
- Psycopg 3 是 PostgreSQL 的 Python 驱动;
|
||||||
|
- Psycopg 的基础层次与 JDBC 相似;
|
||||||
|
- `Connection` 表示数据库会话,`Cursor` 负责执行 SQL 和读取结果;
|
||||||
|
- SQL 与参数必须分开传递;
|
||||||
|
- `with` 用于可靠地结束事务和释放资源;
|
||||||
|
- 数据库密码保存在被 Git 忽略的本地 TOML 配置中,不写入环境变量或源码。
|
||||||
|
|
||||||
|
## 十四、验收标准
|
||||||
|
|
||||||
|
- 能说明 DB-API、Psycopg 和 PostgreSQL 的关系;
|
||||||
|
- 能说出 Psycopg 与 JDBC 的主要对象对应关系;
|
||||||
|
- 能复制模板并通过本地 `config.toml` 配置连接参数;
|
||||||
|
- `connection_example.py` 可以连接练习数据库并输出三项查询结果;
|
||||||
|
- `practice.py` 使用参数化查询,没有拼接 SQL;
|
||||||
|
- 连接和游标都通过 `with` 管理;
|
||||||
|
- 能区分网络不可达、认证失败和依赖缺失;
|
||||||
|
- 程序没有读取、设置或修改环境变量;
|
||||||
|
- 未把 `config.toml` 或真实数据库连接信息写入 Git 跟踪文件。
|
||||||
10
04_数据库/4_1_PostgreSQL与Psycopg入门/config.example.toml
Normal file
10
04_数据库/4_1_PostgreSQL与Psycopg入门/config.example.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 复制本文件并重命名为 config.toml,再填写本地练习数据库信息。
|
||||||
|
# config.toml 已加入 .gitignore,不会被 Git 跟踪。
|
||||||
|
|
||||||
|
[postgresql]
|
||||||
|
host = "数据库主机"
|
||||||
|
port = 5432
|
||||||
|
dbname = "数据库名"
|
||||||
|
user = "用户名"
|
||||||
|
password = "密码"
|
||||||
|
connect_timeout = 10
|
||||||
86
04_数据库/4_1_PostgreSQL与Psycopg入门/connection_example.py
Normal file
86
04_数据库/4_1_PostgreSQL与Psycopg入门/connection_example.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"""第 4-1 课示例:从 TOML 配置读取参数并连接 PostgreSQL。"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
|
||||||
|
|
||||||
|
# 使用当前 Python 文件的位置定位配置,避免程序依赖 PowerShell 的工作目录。
|
||||||
|
CONFIG_PATH = Path(__file__).with_name("config.toml")
|
||||||
|
|
||||||
|
|
||||||
|
def load_database_config(config_path: Path) -> dict[str, str | int]:
|
||||||
|
"""读取并检查 TOML 中的 PostgreSQL 连接配置。"""
|
||||||
|
if not config_path.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
"未找到 config.toml,请复制 config.example.toml 并填写练习数据库配置。"
|
||||||
|
)
|
||||||
|
|
||||||
|
# tomllib 要求以二进制模式读取 TOML 文件,因此这里使用 "rb"。
|
||||||
|
with config_path.open("rb") as config_file:
|
||||||
|
config_data = tomllib.load(config_file)
|
||||||
|
|
||||||
|
postgresql_config = config_data.get("postgresql")
|
||||||
|
if not isinstance(postgresql_config, dict):
|
||||||
|
raise RuntimeError("config.toml 缺少 [postgresql] 配置节。")
|
||||||
|
|
||||||
|
required_names = ("host", "port", "dbname", "user", "password")
|
||||||
|
missing_names = [
|
||||||
|
name for name in required_names if postgresql_config.get(name) in (None, "")
|
||||||
|
]
|
||||||
|
if missing_names:
|
||||||
|
missing_text = "、".join(missing_names)
|
||||||
|
raise RuntimeError(f"config.toml 缺少数据库配置:{missing_text}。")
|
||||||
|
|
||||||
|
return postgresql_config
|
||||||
|
|
||||||
|
|
||||||
|
def query_connection_info(
|
||||||
|
database_config: dict[str, str | int],
|
||||||
|
) -> tuple[str, str, str]:
|
||||||
|
"""连接 PostgreSQL,执行只读参数化查询,并返回连接信息。"""
|
||||||
|
message = "Psycopg 连接成功"
|
||||||
|
|
||||||
|
# ** 会把字典中的键值展开为关键字参数,例如 host="数据库主机"。
|
||||||
|
# 连接信息来自本地 config.toml,不会写入环境变量。
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
# 第二层 with 管理游标。游标同时负责执行 SQL 和读取查询结果。
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# SQL 和参数必须分开传递,不能使用 f-string 拼接用户数据。
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT current_database(), current_user, %s::text",
|
||||||
|
(message,),
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
# 这条 PostgreSQL 查询必然返回一行。普通业务查询仍要考虑 None。
|
||||||
|
if result is None:
|
||||||
|
raise RuntimeError("数据库没有返回连接验证结果。")
|
||||||
|
|
||||||
|
database_name, user_name, returned_message = result
|
||||||
|
return database_name, user_name, returned_message
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""组织配置读取、数据库查询和结果输出。"""
|
||||||
|
try:
|
||||||
|
database_config = load_database_config(CONFIG_PATH)
|
||||||
|
database_name, user_name, message = query_connection_info(database_config)
|
||||||
|
except (OSError, tomllib.TOMLDecodeError, RuntimeError) as error:
|
||||||
|
# 这里处理文件读取、TOML 格式以及课程程序主动检查到的问题。
|
||||||
|
print(f"配置读取失败:{error}")
|
||||||
|
return
|
||||||
|
except psycopg.Error as error:
|
||||||
|
# Psycopg 的具体异常信息可以保留英文,前面补充中文场景说明。
|
||||||
|
print(f"数据库访问失败:{error}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("连接成功。")
|
||||||
|
print(f"当前数据库:{database_name}")
|
||||||
|
print(f"当前用户:{user_name}")
|
||||||
|
print(f"参数化查询结果:{message}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
93
04_数据库/4_1_PostgreSQL与Psycopg入门/practice.py
Normal file
93
04_数据库/4_1_PostgreSQL与Psycopg入门/practice.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 第 4-1 课练习:从 TOML 配置连接 PostgreSQL
|
||||||
|
#
|
||||||
|
# 本文件只提供题目,不包含导入、代码骨架、测试数据或参考答案。
|
||||||
|
# 请完成只读查询,不要创建表,也不要新增、修改或删除远程数据库中的数据。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:准备并定位配置文件
|
||||||
|
# 1. 导入 pathlib.Path、tomllib 和 psycopg。
|
||||||
|
# 2. 使用 Path(__file__).with_name("config.toml") 得到配置文件路径,
|
||||||
|
# 并保存为 CONFIG_PATH。
|
||||||
|
# 3. 复制 config.example.toml,重命名为 config.toml,再填写本地连接信息。
|
||||||
|
# 4. 不得修改环境变量,也不得把真实连接信息写入 Python 文件。
|
||||||
|
#
|
||||||
|
# 与 Java 对照:
|
||||||
|
# - config.toml 类似独立的 application.yml 或 properties 本地配置;
|
||||||
|
# - CONFIG_PATH 类似确定配置资源位置;
|
||||||
|
# - 本课手动读取配置,后续框架可能负责自动绑定配置对象。
|
||||||
|
|
||||||
|
# 第二部分:读取 TOML 配置
|
||||||
|
# 1. 定义 load_database_config(config_path) 函数。
|
||||||
|
# 2. config_path 不存在时抛出 RuntimeError,消息为:
|
||||||
|
# “未找到 config.toml,请先复制并填写配置文件。”。
|
||||||
|
# 3. 使用 config_path.open("rb") 和 with 打开文件。
|
||||||
|
# 4. 调用 tomllib.load(config_file),保存完整配置。
|
||||||
|
# 5. 使用配置节名称 postgresql 取得数据库配置字典。
|
||||||
|
# 6. 配置节不存在或不是字典时抛出:
|
||||||
|
# RuntimeError("config.toml 缺少 [postgresql] 配置节。")
|
||||||
|
# 7. 返回 postgresql 配置字典。
|
||||||
|
|
||||||
|
|
||||||
|
# 第三部分:执行参数化只读查询
|
||||||
|
# 1. 定义 query_lesson_info(database_config, lesson_name, lesson_number) 函数。
|
||||||
|
# 2. 调用 psycopg.connect(**database_config),并使用 with 管理 Connection。
|
||||||
|
# 3. 调用 connection.cursor(),并使用第二层 with 管理 Cursor。
|
||||||
|
# 4. 调用 cursor.execute() 执行:
|
||||||
|
# SELECT current_database(), %s::text, %s::integer
|
||||||
|
# 5. 把 lesson_name 和 lesson_number 组成二元素元组,作为 execute() 的第二个参数。
|
||||||
|
# 6. 不得使用 f-string、字符串拼接或 % 运算把参数拼进 SQL。
|
||||||
|
# 7. 调用 cursor.fetchone(),把返回值保存到 result。
|
||||||
|
# 8. 离开两层 with 后,如果 result 是 None,抛出:
|
||||||
|
# RuntimeError("数据库没有返回练习结果。")
|
||||||
|
# 9. 解包并返回 database_name、returned_name、returned_number。
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:组织 main() 正常流程
|
||||||
|
# 1. 定义 main()。
|
||||||
|
# 2. 调用 load_database_config(CONFIG_PATH),保存 database_config。
|
||||||
|
# 3. 调用:
|
||||||
|
# query_lesson_info(database_config, "PostgreSQL 与 Psycopg 入门", 1)
|
||||||
|
# 4. 保存返回的三个结果。
|
||||||
|
# 5. 分别使用 print() 输出:
|
||||||
|
# “当前数据库:实际数据库名”
|
||||||
|
# “课程名称:PostgreSQL 与 Psycopg 入门”
|
||||||
|
# “课程编号:1”
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:处理异常
|
||||||
|
# 1. 在 main() 中使用 try...except 包围配置读取和数据库查询。
|
||||||
|
# 2. 捕获 OSError、tomllib.TOMLDecodeError 和 RuntimeError,输出:
|
||||||
|
# “配置读取失败:{错误信息}”,然后结束 main()。
|
||||||
|
# 3. 捕获 psycopg.Error,输出“数据库访问失败:{错误信息}”,然后结束 main()。
|
||||||
|
# 4. 添加程序入口判断并调用 main()。
|
||||||
|
#
|
||||||
|
# 缺少配置文件时的预期输出:
|
||||||
|
# 配置读取失败:未找到 config.toml,请先复制并填写配置文件。
|
||||||
|
#
|
||||||
|
# 正确连接后的预期输出格式:
|
||||||
|
# 当前数据库:python_course
|
||||||
|
# 课程名称:PostgreSQL 与 Psycopg 入门
|
||||||
|
# 课程编号:1
|
||||||
|
#
|
||||||
|
# 注意:第一行的 python_course 只是示例,应以实际数据库名为准。
|
||||||
|
|
||||||
|
# 自查清单:
|
||||||
|
# 1. 是否通过当前 Python 文件位置定位 config.toml?
|
||||||
|
# 2. 是否没有读取、设置或修改任何环境变量?
|
||||||
|
# 3. 是否没有在 Python 文件中填写真实账号、密码和主机地址?
|
||||||
|
# 4. Connection 和 Cursor 是否都使用 with 管理?
|
||||||
|
# 5. 是否使用 **database_config 把配置字典传给 psycopg.connect()?
|
||||||
|
# 6. SQL 和参数是否通过 execute() 的两个参数分别传入?
|
||||||
|
# 7. 是否保存并检查了 fetchone() 的结果?
|
||||||
|
# 8. 是否分别处理配置异常和 psycopg.Error?
|
||||||
|
|
||||||
|
|
||||||
|
# 最终验收标准:
|
||||||
|
# 1. practice.py 可以通过 Python 语法检查;
|
||||||
|
# 2. 缺少 config.toml 时能输出指定的中文提示;
|
||||||
|
# 3. 配置正确时能连接 PostgreSQL 并输出数据库名、课程名和课程编号;
|
||||||
|
# 4. 查询全程只读,不产生远程数据库写入;
|
||||||
|
# 5. SQL 使用参数化查询,不存在字符串拼接;
|
||||||
|
# 6. 连接和游标能够可靠关闭;
|
||||||
|
# 7. 程序不读写环境变量;
|
||||||
|
# 8. config.toml 和真实连接信息没有进入 Git 跟踪文件。
|
||||||
3
04_数据库/4_1_PostgreSQL与Psycopg入门/requirements.txt
Normal file
3
04_数据库/4_1_PostgreSQL与Psycopg入门/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Psycopg 3 是 PostgreSQL 的 Python 数据库驱动。
|
||||||
|
# binary 额外依赖适合本地学习,可避免首次安装时配置 C 编译环境。
|
||||||
|
psycopg[binary]>=3,<4
|
||||||
420
04_数据库/4_2_Python数据库事务与数据访问层/README.md
Normal file
420
04_数据库/4_2_Python数据库事务与数据访问层/README.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# 第 4-2 课:Python 数据库事务与数据访问层
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
第一课已经完成 PostgreSQL 连接、游标、参数化查询和资源释放。本课先系统复习事务的核心概念,再学习 Python/Psycopg 中的事务边界,以及如何把 SQL 从业务逻辑中分离。数据库基础不会展开成完整 SQL 课程,但事务是后续 ORM 和 Web 业务正确性的基础,必须讲清楚。
|
||||||
|
|
||||||
|
本课会写入远程练习数据库:示例创建专用表 `course_bank_account`,只重置并操作 `COURSE-` 前缀数据;练习使用另一张专用表 `course_wallet_account`,只操作 `PRACTICE-` 前缀数据。不要连接生产数据库。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 说明事务是什么以及为什么需要事务;
|
||||||
|
2. 结合转账解释原子性、一致性、隔离性和持久性;
|
||||||
|
3. 区分提交、回滚、自动提交和事务失败状态;
|
||||||
|
4. 解释常见并发异常和事务隔离级别;
|
||||||
|
5. 解释 Psycopg 默认事务行为;
|
||||||
|
6. 使用连接上下文自动提交和回滚;
|
||||||
|
7. 理解为什么异常必须传播出事务上下文;
|
||||||
|
8. 使用 `executemany()` 批量执行同一条参数化 SQL;
|
||||||
|
9. 使用 `FOR UPDATE` 锁定待修改数据;
|
||||||
|
10. 使用 Repository 隔离 SQL;
|
||||||
|
11. 把业务规则放在 Service 中;
|
||||||
|
12. 让一次业务操作共享同一连接和事务;
|
||||||
|
13. 对照 JDBC、MyBatis 和 Spring 事务理解 Python实现。
|
||||||
|
|
||||||
|
## 三、事务是什么
|
||||||
|
|
||||||
|
事务(Transaction)是数据库中的一个工作单元:它包含一条或多条操作,这些操作应该作为一个不可分割的整体完成。
|
||||||
|
|
||||||
|
转账至少包含两条更新:
|
||||||
|
|
||||||
|
```text
|
||||||
|
账户A扣款200元
|
||||||
|
账户B入账200元
|
||||||
|
```
|
||||||
|
|
||||||
|
如果第一条成功、第二条失败,却保留了第一条结果,钱就凭空减少了。事务要求最终只能出现两种结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
全部成功 → 提交(COMMIT)
|
||||||
|
任一步失败 → 回滚(ROLLBACK),恢复到事务开始前
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的SQL概念是:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE account SET balance = balance - 200 WHERE account_no = 'A';
|
||||||
|
UPDATE account SET balance = balance + 200 WHERE account_no = 'B';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
```
|
||||||
|
|
||||||
|
中间发生错误时执行:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ROLLBACK;
|
||||||
|
```
|
||||||
|
|
||||||
|
使用Psycopg时通常不需要手写`BEGIN`。驱动会按连接状态自动开始事务,代码负责正确划定提交和回滚边界。
|
||||||
|
|
||||||
|
## 四、事务的ACID特性
|
||||||
|
|
||||||
|
ACID是事务需要满足的四类核心性质。
|
||||||
|
|
||||||
|
### 4.1 原子性(Atomicity)
|
||||||
|
|
||||||
|
事务内的操作要么全部成功,要么全部失败。转账中的扣款和入账不能只保留其中一步。
|
||||||
|
|
||||||
|
本课的失败示例会先扣除50元,再主动抛出异常。最终余额不变,就是在验证原子性。
|
||||||
|
|
||||||
|
### 4.2 一致性(Consistency)
|
||||||
|
|
||||||
|
事务执行前后,数据都必须满足数据库约束和业务规则。例如:
|
||||||
|
|
||||||
|
- 余额不能小于0;
|
||||||
|
- 转账前后两个账户的余额总额不应无故变化;
|
||||||
|
- 目标账户必须存在;
|
||||||
|
- 主键、非空和检查约束仍然成立。
|
||||||
|
|
||||||
|
一致性不是只靠数据库自动保证。数据库约束、事务、锁和Service中的业务校验要共同工作。
|
||||||
|
|
||||||
|
### 4.3 隔离性(Isolation)
|
||||||
|
|
||||||
|
多个事务并发执行时,一个事务不应随意看到另一个尚未完成事务的中间状态。
|
||||||
|
|
||||||
|
假设账户A余额为1000元,两个请求同时转出800元。如果二者都先读取到1000,再分别扣款,就可能发生超额转账。事务隔离级别和行锁用于控制这种并发影响。
|
||||||
|
|
||||||
|
隔离不等于所有事务完全串行。隔离越强,并发冲突通常越少,但等待和资源成本可能越高。
|
||||||
|
|
||||||
|
### 4.4 持久性(Durability)
|
||||||
|
|
||||||
|
事务成功提交后,结果应该持久保存。程序退出或连接关闭后,再使用新连接查询,仍然能够看到已提交的余额。
|
||||||
|
|
||||||
|
本课使用独立连接回查成功转账结果,就是在直观验证持久性。
|
||||||
|
|
||||||
|
## 五、提交、回滚与事务状态
|
||||||
|
|
||||||
|
### 5.1 提交
|
||||||
|
|
||||||
|
`COMMIT`确认事务中的修改。提交成功后,其他事务才能按照隔离规则观察到这些结果,当前事务也不能再整体撤销。
|
||||||
|
|
||||||
|
### 5.2 回滚
|
||||||
|
|
||||||
|
`ROLLBACK`取消当前事务中尚未提交的修改。它不是反向执行一条新的补偿SQL,而是让数据库放弃本次事务的未提交结果。
|
||||||
|
|
||||||
|
### 5.3 PostgreSQL事务失败状态
|
||||||
|
|
||||||
|
PostgreSQL事务中的某条SQL失败后,当前事务通常进入失败状态。即使后面的SQL本身正确,也会收到类似错误:
|
||||||
|
|
||||||
|
```text
|
||||||
|
current transaction is aborted, commands ignored until end of transaction block
|
||||||
|
```
|
||||||
|
|
||||||
|
中文含义是:当前事务已经失败,在事务结束前忽略后续命令。此时必须回滚,或让Psycopg的连接上下文因异常退出并自动回滚。
|
||||||
|
|
||||||
|
### 5.4 自动提交
|
||||||
|
|
||||||
|
自动提交(Autocommit)表示每条独立SQL完成后立即提交。它适合部分不需要多语句原子性的操作,但无法把扣款和入账自动组合成一个事务。
|
||||||
|
|
||||||
|
Psycopg默认`autocommit=False`。本课保持默认行为,不开启自动提交。
|
||||||
|
|
||||||
|
## 六、隔离级别与并发异常
|
||||||
|
|
||||||
|
常见并发异常如下:
|
||||||
|
|
||||||
|
| 并发现象 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 脏读(Dirty Read) | 读取到其他事务尚未提交的数据;对方回滚后,读到的数据从未真正成立 |
|
||||||
|
| 不可重复读(Non-repeatable Read) | 同一事务两次读取同一行,期间被其他事务提交修改,结果不同 |
|
||||||
|
| 幻读(Phantom Read) | 同一事务两次执行相同范围查询,结果行数量因其他事务提交而变化 |
|
||||||
|
| 丢失更新(Lost Update) | 两个事务基于同一旧值更新,后提交的结果覆盖前一个结果 |
|
||||||
|
|
||||||
|
SQL标准定义四个主要隔离级别:
|
||||||
|
|
||||||
|
| 隔离级别 | 基本含义 | PostgreSQL说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `READ UNCOMMITTED` | 理论上允许读取未提交数据 | PostgreSQL内部按`READ COMMITTED`处理 |
|
||||||
|
| `READ COMMITTED` | 每条语句读取执行开始前已提交的数据 | PostgreSQL默认级别 |
|
||||||
|
| `REPEATABLE READ` | 事务内多次查询基于稳定快照 | 提交时仍可能出现并发冲突 |
|
||||||
|
| `SERIALIZABLE` | 尽量表现得像事务串行执行 | 冲突时可能要求应用重试事务 |
|
||||||
|
|
||||||
|
隔离级别不能代替所有业务并发控制。本课在默认`READ COMMITTED`下使用`SELECT ... FOR UPDATE`锁住即将修改的账户行。
|
||||||
|
|
||||||
|
## 七、JDBC与Psycopg事务对照
|
||||||
|
|
||||||
|
| Java常见写法 | Psycopg写法 | 含义 |
|
||||||
|
|---|---|---|
|
||||||
|
| `connection.setAutoCommit(false)` | 默认连接首次操作自动进入事务 | 开始事务工作 |
|
||||||
|
| `connection.commit()` | 正常离开连接`with` | 提交 |
|
||||||
|
| `connection.rollback()` | 异常离开连接`with` | 回滚 |
|
||||||
|
| `try-with-resources` | `with psycopg.connect(...)` | 管理连接生命周期 |
|
||||||
|
| Mapper/DAO | Repository | 封装SQL和结果转换 |
|
||||||
|
| Service | Service | 组织业务规则 |
|
||||||
|
| `@Transactional` | 外层连接/事务上下文 | 划定事务边界 |
|
||||||
|
|
||||||
|
Python没有Spring默认提供的声明式`@Transactional`。直接使用Psycopg时,需要显式组织事务上下文;后续SQLAlchemy和FastAPI课程会进一步统一Session生命周期。
|
||||||
|
|
||||||
|
## 八、Psycopg默认事务行为
|
||||||
|
|
||||||
|
Psycopg遵循DB-API习惯:连接默认不是自动提交模式。包括`SELECT`在内的数据库操作通常都会启动事务。
|
||||||
|
|
||||||
|
```python
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
```
|
||||||
|
|
||||||
|
正常离开`with`时提交;块内异常传播出去时回滚;最后关闭连接。
|
||||||
|
|
||||||
|
### 8.1 异常不能在事务内部被吞掉
|
||||||
|
|
||||||
|
错误写法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
try:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
raise TransferError("后续失败")
|
||||||
|
except TransferError:
|
||||||
|
print("失败")
|
||||||
|
```
|
||||||
|
|
||||||
|
异常已经在`with`内部被捕获,连接上下文看到的是“正常结束”,可能提交前面的更新。
|
||||||
|
|
||||||
|
正确边界:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
raise TransferError("后续失败")
|
||||||
|
except TransferError as error:
|
||||||
|
print(error)
|
||||||
|
```
|
||||||
|
|
||||||
|
异常先离开`with`,触发回滚,然后才由外层处理。
|
||||||
|
|
||||||
|
### 8.2 手动提交和回滚
|
||||||
|
|
||||||
|
连接上下文适合“整个代码块就是一个事务”的情况,也可以显式控制:
|
||||||
|
|
||||||
|
```python
|
||||||
|
connection = psycopg.connect(**database_config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
connection.commit()
|
||||||
|
except Exception:
|
||||||
|
connection.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
这与JDBC手动事务非常接近。但只要业务边界能自然表达为代码块,优先使用`with`,可以减少遗漏回滚或关闭连接的风险。
|
||||||
|
|
||||||
|
### 8.3 连接with与事务with的区别
|
||||||
|
|
||||||
|
本课使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
它同时管理事务和连接生命周期。Psycopg还提供:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with connection.transaction():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
后者只划定一个事务或保存点范围,不负责创建连接。它适合长连接或连接池场景。本阶段后续课程结合连接池时再深入使用。
|
||||||
|
|
||||||
|
## 九、Repository与Service职责
|
||||||
|
|
||||||
|
本课采用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
main
|
||||||
|
↓ 创建连接并划定事务
|
||||||
|
Service
|
||||||
|
↓ 组织转账规则
|
||||||
|
Repository
|
||||||
|
↓ 执行参数化SQL
|
||||||
|
PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
Repository不应在每个方法中调用`commit()`,否则扣款刚提交、入账却失败时,外层已经无法回滚整个业务操作。
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AccountRepository:
|
||||||
|
def __init__(self, connection):
|
||||||
|
self.connection = connection
|
||||||
|
|
||||||
|
def change_balance(self, account_no, amount):
|
||||||
|
self.connection.execute(...)
|
||||||
|
# 这里不提交。
|
||||||
|
```
|
||||||
|
|
||||||
|
Service也不负责创建连接,它复用同一个Repository,从而保证多个SQL处于同一事务。
|
||||||
|
|
||||||
|
## 十、FOR UPDATE、锁与死锁
|
||||||
|
|
||||||
|
转账前先查询余额:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT balance
|
||||||
|
FROM course_bank_account
|
||||||
|
WHERE account_no = %s
|
||||||
|
FOR UPDATE
|
||||||
|
```
|
||||||
|
|
||||||
|
`FOR UPDATE`会锁定选中的行,直到事务提交或回滚。它能防止两个并发事务同时读取相同旧余额后分别扣款。
|
||||||
|
|
||||||
|
这种“先锁定,再判断和修改”的做法属于悲观锁:代码假设并发冲突可能发生,因此提前取得排他性的行锁。
|
||||||
|
|
||||||
|
锁会持续到事务提交或回滚。事务范围过大,会让其他请求等待更久,因此事务中不应夹杂耗时的网络请求、人工操作或无关计算。
|
||||||
|
|
||||||
|
### 10.1 死锁
|
||||||
|
|
||||||
|
如果事务一先锁A再锁B,事务二同时先锁B再锁A,双方可能互相等待。数据库会检测死锁并中止其中一个事务。
|
||||||
|
|
||||||
|
降低死锁风险的常用办法:
|
||||||
|
|
||||||
|
- 多个事务按照统一顺序锁定资源,例如始终按账号升序;
|
||||||
|
- 缩短事务时间;
|
||||||
|
- 只锁真正需要修改的行;
|
||||||
|
- 应用捕获死锁或序列化失败,并按策略重试整个事务。
|
||||||
|
|
||||||
|
## 十一、批量操作
|
||||||
|
|
||||||
|
多条数据执行相同SQL时,可以使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.executemany(
|
||||||
|
"INSERT INTO course_bank_account VALUES (%s, %s, %s)",
|
||||||
|
accounts,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
它比手工拼接多条SQL安全,也明确表达“同一语句、不同参数”。它不等于无限制地一次提交海量数据;生产中仍要根据数据量分批。
|
||||||
|
|
||||||
|
## 十二、完整示例
|
||||||
|
|
||||||
|
示例文件为[transaction_repository_example.py](./transaction_repository_example.py),包含:
|
||||||
|
|
||||||
|
- `AccountRepository`:建表、初始化、查询和更新;
|
||||||
|
- `TransferService`:金额检查、余额检查、扣款和入账;
|
||||||
|
- 成功事务:转账200元并自动提交;
|
||||||
|
- 失败事务:先扣50元再抛出异常,验证自动回滚;
|
||||||
|
- 独立连接回查:证明提交和回滚的最终状态。
|
||||||
|
|
||||||
|
## 十三、准备配置
|
||||||
|
|
||||||
|
在第二课目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .\config.example.toml .\config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
填写第一课使用的同一套专用练习数据库配置。`config.toml`已被项目`.gitignore`排除。
|
||||||
|
|
||||||
|
## 十四、运行方法与预期结果
|
||||||
|
|
||||||
|
激活已安装Psycopg的Conda环境:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda activate python-test
|
||||||
|
python .\transaction_repository_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
关键结果应为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
初始余额:
|
||||||
|
COURSE-A001|小明|余额:1000.00
|
||||||
|
COURSE-A002|小红|余额:500.00
|
||||||
|
成功转账 200 元后:
|
||||||
|
COURSE-A001|小明|余额:800.00
|
||||||
|
COURSE-A002|小红|余额:700.00
|
||||||
|
失败事务已回滚:模拟第二步失败,验证前一步更新会被回滚。
|
||||||
|
失败事务回滚后:
|
||||||
|
COURSE-A001|小明|余额:800.00
|
||||||
|
COURSE-A002|小红|余额:700.00
|
||||||
|
```
|
||||||
|
|
||||||
|
重复运行时,示例会先删除`COURSE-`前缀数据并重新初始化,因此结果保持一致。
|
||||||
|
|
||||||
|
## 十五、常见错误
|
||||||
|
|
||||||
|
### 15.1 Repository内部提交
|
||||||
|
|
||||||
|
这会破坏跨多个SQL的原子性。提交和回滚应由业务事务边界统一控制。
|
||||||
|
|
||||||
|
### 15.2 在with内部捕获业务异常
|
||||||
|
|
||||||
|
异常没有传播给连接上下文,可能导致错误提交。先让异常离开事务块,再在外层捕获。
|
||||||
|
|
||||||
|
### 15.3 失败后继续使用同一事务
|
||||||
|
|
||||||
|
PostgreSQL语句失败后,当前事务通常进入失败状态。在回滚前继续执行SQL,会收到`current transaction is aborted`一类错误。
|
||||||
|
|
||||||
|
### 15.4 使用float表示金额
|
||||||
|
|
||||||
|
二进制浮点数可能产生精度误差。课程金额使用`Decimal("200.00")`,数据库使用`NUMERIC(12, 2)`。
|
||||||
|
|
||||||
|
### 15.5 先查询再更新却没有锁
|
||||||
|
|
||||||
|
单用户测试可能正常,但并发时可能发生余额覆盖或超额扣款。本课使用`FOR UPDATE`锁定账户行。
|
||||||
|
|
||||||
|
### 15.6 清理范围过大
|
||||||
|
|
||||||
|
不要使用无条件`DELETE`或`TRUNCATE`。示例和练习只删除指定前缀的课程数据。
|
||||||
|
|
||||||
|
## 十六、课堂练习
|
||||||
|
|
||||||
|
打开[practice.py](./practice.py),实现独立的钱包转账练习。题目已经明确:
|
||||||
|
|
||||||
|
1. 配置读取;
|
||||||
|
2. Repository方法;
|
||||||
|
3. Service业务规则;
|
||||||
|
4. 批量初始化;
|
||||||
|
5. 成功事务;
|
||||||
|
6. 失败回滚;
|
||||||
|
7. 回查、输出和异常处理。
|
||||||
|
|
||||||
|
## 十七、本课小结
|
||||||
|
|
||||||
|
- 事务把多条数据库操作组织成一个工作单元;
|
||||||
|
- ACID分别是原子性、一致性、隔离性和持久性;
|
||||||
|
- 提交确认修改,回滚取消尚未提交的修改;
|
||||||
|
- PostgreSQL事务中的SQL失败后,通常必须先回滚才能继续;
|
||||||
|
- 隔离级别控制并发事务互相可见的范围;
|
||||||
|
- 事务边界应该覆盖完整业务操作;
|
||||||
|
- Repository封装SQL,但不擅自提交;
|
||||||
|
- Service组织业务规则,但复用外部连接;
|
||||||
|
- 异常必须先离开事务上下文才能触发自动回滚;
|
||||||
|
- `executemany()`适合相同SQL的多组参数;
|
||||||
|
- `FOR UPDATE`用于锁定即将修改的数据;
|
||||||
|
- 金额应使用`Decimal`与数据库`NUMERIC`。
|
||||||
|
|
||||||
|
## 十八、验收标准
|
||||||
|
|
||||||
|
- 能使用转账说明为什么需要事务;
|
||||||
|
- 能结合示例解释ACID四个特性;
|
||||||
|
- 能区分提交、回滚和自动提交;
|
||||||
|
- 能简要说明脏读、不可重复读、幻读和丢失更新;
|
||||||
|
- 能解释连接上下文何时提交、何时回滚;
|
||||||
|
- 能说明为什么Repository不能随意提交;
|
||||||
|
- 标准示例重复运行且结果一致;
|
||||||
|
- 练习的成功转账同时更新两个钱包;
|
||||||
|
- 模拟失败后第一条更新被完整回滚;
|
||||||
|
- SQL全部参数化;
|
||||||
|
- 只操作课程专用表和指定前缀数据;
|
||||||
|
- 未提交真实`config.toml`。
|
||||||
10
04_数据库/4_2_Python数据库事务与数据访问层/config.example.toml
Normal file
10
04_数据库/4_2_Python数据库事务与数据访问层/config.example.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 复制本文件并重命名为 config.toml,再填写本地练习数据库信息。
|
||||||
|
# config.toml 已加入项目 .gitignore,不会被 Git 跟踪。
|
||||||
|
|
||||||
|
[postgresql]
|
||||||
|
host = "数据库主机"
|
||||||
|
port = 5432
|
||||||
|
dbname = "数据库名"
|
||||||
|
user = "用户名"
|
||||||
|
password = "密码"
|
||||||
|
connect_timeout = 10
|
||||||
130
04_数据库/4_2_Python数据库事务与数据访问层/practice.py
Normal file
130
04_数据库/4_2_Python数据库事务与数据访问层/practice.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# 第 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。
|
||||||
3
04_数据库/4_2_Python数据库事务与数据访问层/requirements.txt
Normal file
3
04_数据库/4_2_Python数据库事务与数据访问层/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 第二课继续使用 Psycopg 3,不新增第三方框架。
|
||||||
|
# 若使用 Conda,可以在课程环境中安装 psycopg 或 psycopg-c。
|
||||||
|
psycopg>=3,<4
|
||||||
214
04_数据库/4_2_Python数据库事务与数据访问层/transaction_repository_example.py
Normal file
214
04_数据库/4_2_Python数据库事务与数据访问层/transaction_repository_example.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
"""第 4-2 课示例:使用事务和 Repository 完成安全转账。"""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
from psycopg import Connection
|
||||||
|
from psycopg.rows import dict_row
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_PATH = Path(__file__).with_name("config.toml")
|
||||||
|
|
||||||
|
|
||||||
|
class TransferError(Exception):
|
||||||
|
"""表示转账过程中可以预期的业务失败。"""
|
||||||
|
|
||||||
|
|
||||||
|
def load_database_config(config_path: Path) -> dict[str, str | int]:
|
||||||
|
"""读取第二课本地 TOML 数据库配置。"""
|
||||||
|
if not config_path.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
"未找到 config.toml,请复制 config.example.toml 并填写练习数据库配置。"
|
||||||
|
)
|
||||||
|
|
||||||
|
with config_path.open("rb") as config_file:
|
||||||
|
config_data = tomllib.load(config_file)
|
||||||
|
|
||||||
|
database_config = config_data.get("postgresql")
|
||||||
|
if not isinstance(database_config, dict):
|
||||||
|
raise RuntimeError("config.toml 缺少 [postgresql] 配置节。")
|
||||||
|
|
||||||
|
return database_config
|
||||||
|
|
||||||
|
|
||||||
|
class AccountRepository:
|
||||||
|
"""封装账户表 SQL,但不自行提交或回滚事务。"""
|
||||||
|
|
||||||
|
def __init__(self, connection: Connection) -> None:
|
||||||
|
self.connection = connection
|
||||||
|
|
||||||
|
def create_table(self) -> None:
|
||||||
|
"""创建本课专用表;表已存在时保持不变。"""
|
||||||
|
self.connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS course_bank_account (
|
||||||
|
account_no VARCHAR(30) PRIMARY KEY,
|
||||||
|
owner_name VARCHAR(50) NOT NULL,
|
||||||
|
balance NUMERIC(12, 2) NOT NULL CHECK (balance >= 0)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def reset_course_accounts(self) -> None:
|
||||||
|
"""只清理 COURSE- 前缀的课程数据,避免影响其他记录。"""
|
||||||
|
self.connection.execute(
|
||||||
|
"DELETE FROM course_bank_account WHERE account_no LIKE %s",
|
||||||
|
("COURSE-%",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_accounts(self, accounts: list[tuple[str, str, Decimal]]) -> None:
|
||||||
|
"""使用 executemany() 批量新增课程账户。"""
|
||||||
|
# executemany() 是 Cursor 的方法,因此显式创建并关闭游标。
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO course_bank_account (account_no, owner_name, balance)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
""",
|
||||||
|
accounts,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_balance_for_update(self, account_no: str) -> Decimal:
|
||||||
|
"""查询并锁定账户,防止并发事务同时修改同一余额。"""
|
||||||
|
result = self.connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT balance
|
||||||
|
FROM course_bank_account
|
||||||
|
WHERE account_no = %s
|
||||||
|
FOR UPDATE
|
||||||
|
""",
|
||||||
|
(account_no,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
raise TransferError(f"账户不存在:{account_no}")
|
||||||
|
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
def change_balance(self, account_no: str, amount: Decimal) -> None:
|
||||||
|
"""使用数据库加法更新余额,并检查目标账户是否存在。"""
|
||||||
|
cursor = self.connection.execute(
|
||||||
|
"""
|
||||||
|
UPDATE course_bank_account
|
||||||
|
SET balance = balance + %s
|
||||||
|
WHERE account_no = %s
|
||||||
|
""",
|
||||||
|
(amount, account_no),
|
||||||
|
)
|
||||||
|
if cursor.rowcount != 1:
|
||||||
|
raise TransferError(f"账户不存在:{account_no}")
|
||||||
|
|
||||||
|
def find_course_accounts(self) -> list[dict[str, object]]:
|
||||||
|
"""按账号查询课程账户,并以字典行返回。"""
|
||||||
|
cursor = self.connection.cursor(row_factory=dict_row)
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT account_no, owner_name, balance
|
||||||
|
FROM course_bank_account
|
||||||
|
WHERE account_no LIKE %s
|
||||||
|
ORDER BY account_no
|
||||||
|
""",
|
||||||
|
("COURSE-%",),
|
||||||
|
)
|
||||||
|
return list(cursor.fetchall())
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TransferService:
|
||||||
|
"""组织转账业务规则;事务由调用它的连接上下文统一管理。"""
|
||||||
|
|
||||||
|
def __init__(self, repository: AccountRepository) -> None:
|
||||||
|
self.repository = repository
|
||||||
|
|
||||||
|
def transfer(self, source_no: str, target_no: str, amount: Decimal) -> None:
|
||||||
|
"""在同一事务中完成扣款与入账。"""
|
||||||
|
if amount <= 0:
|
||||||
|
raise TransferError("转账金额必须大于 0。")
|
||||||
|
|
||||||
|
source_balance = self.repository.get_balance_for_update(source_no)
|
||||||
|
self.repository.get_balance_for_update(target_no)
|
||||||
|
|
||||||
|
if source_balance < amount:
|
||||||
|
raise TransferError("账户余额不足。")
|
||||||
|
|
||||||
|
self.repository.change_balance(source_no, -amount)
|
||||||
|
self.repository.change_balance(target_no, amount)
|
||||||
|
|
||||||
|
|
||||||
|
def print_accounts(title: str, accounts: list[dict[str, object]]) -> None:
|
||||||
|
"""输出当前课程账户余额。"""
|
||||||
|
print(title)
|
||||||
|
for account in accounts:
|
||||||
|
print(
|
||||||
|
f"{account['account_no']}|{account['owner_name']}|"
|
||||||
|
f"余额:{account['balance']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_data(database_config: dict[str, str | int]) -> None:
|
||||||
|
"""创建专用表并重置本课固定数据。"""
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
repository = AccountRepository(connection)
|
||||||
|
repository.create_table()
|
||||||
|
repository.reset_course_accounts()
|
||||||
|
repository.add_accounts(
|
||||||
|
[
|
||||||
|
("COURSE-A001", "小明", Decimal("1000.00")),
|
||||||
|
("COURSE-A002", "小红", Decimal("500.00")),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_successful_transfer(database_config: dict[str, str | int]) -> None:
|
||||||
|
"""演示正常离开连接上下文时自动提交事务。"""
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
service = TransferService(AccountRepository(connection))
|
||||||
|
service.transfer("COURSE-A001", "COURSE-A002", Decimal("200.00"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_failed_transfer(database_config: dict[str, str | int]) -> None:
|
||||||
|
"""演示异常离开连接上下文时自动回滚整个事务。"""
|
||||||
|
try:
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
repository = AccountRepository(connection)
|
||||||
|
|
||||||
|
# 先执行一条成功更新,再主动触发业务异常。
|
||||||
|
# 外层 with 会回滚,因此这 50 元扣款不会保留下来。
|
||||||
|
repository.change_balance("COURSE-A001", Decimal("-50.00"))
|
||||||
|
raise TransferError("模拟第二步失败,验证前一步更新会被回滚。")
|
||||||
|
except TransferError as error:
|
||||||
|
print(f"失败事务已回滚:{error}")
|
||||||
|
|
||||||
|
|
||||||
|
def query_accounts(
|
||||||
|
database_config: dict[str, str | int],
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
"""使用独立连接回查已经提交的数据。"""
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
return AccountRepository(connection).find_course_accounts()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""依次演示初始化、提交、回滚和回查。"""
|
||||||
|
try:
|
||||||
|
database_config = load_database_config(CONFIG_PATH)
|
||||||
|
prepare_data(database_config)
|
||||||
|
print_accounts("初始余额:", query_accounts(database_config))
|
||||||
|
|
||||||
|
run_successful_transfer(database_config)
|
||||||
|
print_accounts("成功转账 200 元后:", query_accounts(database_config))
|
||||||
|
|
||||||
|
run_failed_transfer(database_config)
|
||||||
|
print_accounts("失败事务回滚后:", query_accounts(database_config))
|
||||||
|
except (OSError, tomllib.TOMLDecodeError, RuntimeError) as error:
|
||||||
|
print(f"配置读取失败:{error}")
|
||||||
|
except psycopg.Error as error:
|
||||||
|
print(f"数据库访问失败:{error}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
51
README.md
51
README.md
@@ -69,16 +69,24 @@
|
|||||||
|
|
||||||
### 第四阶段:数据库编程
|
### 第四阶段:数据库编程
|
||||||
|
|
||||||
- 数据库和关系型数据库的基本概念;
|
本阶段采用精简路线。学习者已经具备数据库、SQL 和 Java 数据库开发基础,因此不再单独讲解关系型数据库、增删改查、关联查询、索引和事务等通用知识,而是重点学习 Python 数据库编程方式以及它与 JDBC、MyBatis、MyBatis-Plus、JPA/Hibernate 的差异。
|
||||||
- SQL 基础语法;
|
|
||||||
- 表、字段、主键和外键;
|
本阶段统一使用 PostgreSQL,不同时维护 MySQL 和 PostgreSQL 两套示例。涉及常见数据库差异时,通过补充说明对照 PostgreSQL 与 MySQL,不影响后续将知识迁移到 MySQL。
|
||||||
- 数据的新增、查询、修改和删除;
|
|
||||||
- 条件查询、排序、分组和关联查询;
|
本阶段共五课:
|
||||||
- 索引与事务基础;
|
|
||||||
- Python 连接 MySQL 或 PostgreSQL;
|
1. `4_1_PostgreSQL与Psycopg入门`:连接远程 PostgreSQL,认识 Python 数据库 API(Database API,DB-API),使用 Psycopg 3 执行参数化 SQL,并与 JDBC 的连接、语句和结果集进行对照;
|
||||||
- 参数化查询与 SQL 注入防护;
|
2. `4_2_Python数据库事务与数据访问层`:学习提交、回滚、异常处理、批量操作,以及数据访问对象(Data Access Object,DAO)和 Repository 分层;
|
||||||
- SQLAlchemy 对象关系映射;
|
3. `4_3_SQLAlchemy基础`:学习 SQLAlchemy 2.x 的 Engine、Session、声明式模型和基本增删改查,并与 JPA/Hibernate、MyBatis-Plus 进行对照;
|
||||||
- 数据库综合项目:持久化图书管理系统。
|
4. `4_4_SQLAlchemy关系映射与工程实践`:学习一对多、多对多、级联操作、加载策略、N+1 查询问题和业务分层;
|
||||||
|
5. `4_5_数据库综合项目`:使用 PostgreSQL 和 SQLAlchemy 2.x,把第三阶段的内存版图书管理系统升级为持久化版本。
|
||||||
|
|
||||||
|
本阶段采用两层数据库操作路线:
|
||||||
|
|
||||||
|
- Psycopg 3 对应 JDBC 驱动层,用于理解连接、游标、参数化 SQL、查询结果和事务;
|
||||||
|
- SQLAlchemy 2.x 提供 SQL 工具和对象关系映射(Object Relational Mapping,ORM)能力,其中 ORM 的定位更接近 JPA/Hibernate,并具备部分与 MyBatis-Plus 相似的常规增删改查体验。
|
||||||
|
|
||||||
|
数据库连接地址、用户名和密码通过本地 `config.toml` 提供,不写入环境变量。仓库只提交脱敏的 `config.example.toml`,真实配置文件必须由 `.gitignore` 排除。课程只使用专用练习数据库和受限账号,不连接生产数据库。
|
||||||
|
|
||||||
### 第五阶段:Web 开发基础
|
### 第五阶段:Web 开发基础
|
||||||
|
|
||||||
@@ -192,6 +200,11 @@ Python/
|
|||||||
│ ├── 3_4_Python特殊方法与数据类/
|
│ ├── 3_4_Python特殊方法与数据类/
|
||||||
│ └── 3_5_面向对象综合项目/
|
│ └── 3_5_面向对象综合项目/
|
||||||
├── 04_数据库/
|
├── 04_数据库/
|
||||||
|
│ ├── 4_1_PostgreSQL与Psycopg入门/
|
||||||
|
│ ├── 4_2_Python数据库事务与数据访问层/
|
||||||
|
│ ├── 4_3_SQLAlchemy基础/
|
||||||
|
│ ├── 4_4_SQLAlchemy关系映射与工程实践/
|
||||||
|
│ └── 4_5_数据库综合项目/
|
||||||
├── 05_web基础/
|
├── 05_web基础/
|
||||||
├── 06_fastapi/
|
├── 06_fastapi/
|
||||||
├── 07_django/
|
├── 07_django/
|
||||||
@@ -202,13 +215,13 @@ Python/
|
|||||||
|
|
||||||
## 当前学习进度
|
## 当前学习进度
|
||||||
|
|
||||||
- 当前阶段:第三阶段——面向对象编程。
|
- 当前阶段:第四阶段——数据库编程。
|
||||||
- 当前课程:`3_5_面向对象综合项目`。
|
- 当前课程:`4_2_Python数据库事务与数据访问层`。
|
||||||
- 当前状态:第三阶段已完成并通过综合项目验收。
|
- 当前状态:第四阶段前两课已完成,并通过 PostgreSQL 连接、成功提交和异常回滚验证。
|
||||||
- 已完成课程:第一阶段 `1_1_hello_world` 至 `1_14_python基础综合项目`、第二阶段 `2_1_模块与包` 至 `2_9_python进阶综合项目`,以及第三阶段 `3_1` 至 `3_5`。
|
- 已完成课程:第一阶段 `1_1_hello_world` 至 `1_14_python基础综合项目`、第二阶段 `2_1_模块与包` 至 `2_9_python进阶综合项目`、第三阶段 `3_1` 至 `3_5`,以及第四阶段 `4_1`、`4_2`。
|
||||||
- 学习中的课程:无。
|
- 学习中的课程:无。
|
||||||
- 已创建课程目录:第一阶段全部课程、第二阶段全部课程,以及第三阶段 `3_1` 至 `3_5`。
|
- 已创建课程目录:第一阶段全部课程、第二阶段全部课程、第三阶段 `3_1` 至 `3_5`,以及第四阶段 `4_1`、`4_2`。
|
||||||
- 下一步:进入第四阶段数据库编程,从数据库与关系型数据库基本概念开始。
|
- 下一步:进入 `4_3_SQLAlchemy基础`,学习 Engine、Session、声明式模型和 ORM 增删改查。
|
||||||
|
|
||||||
## 建议环境
|
## 建议环境
|
||||||
|
|
||||||
@@ -216,7 +229,9 @@ Python/
|
|||||||
- Python:学习开始时选择当前稳定版本;
|
- Python:学习开始时选择当前稳定版本;
|
||||||
- 编辑器:PyCharm 或 Visual Studio Code;
|
- 编辑器:PyCharm 或 Visual Studio Code;
|
||||||
- 命令行:PowerShell;
|
- 命令行:PowerShell;
|
||||||
- 数据库:前期可使用 SQLite,后期学习 MySQL 或 PostgreSQL;
|
- 数据库:第四阶段统一使用专用的远程 PostgreSQL 练习数据库;
|
||||||
|
- Python 数据库驱动:Psycopg 3;
|
||||||
|
- 对象关系映射工具:SQLAlchemy 2.x;
|
||||||
- 浏览器:Chrome、Edge 或其他现代浏览器。
|
- 浏览器:Chrome、Edge 或其他现代浏览器。
|
||||||
|
|
||||||
具体版本和安装步骤将在第一课中核对并讲解,避免因为版本变化使用过时的安装方式。
|
具体版本和安装步骤将在第一课中核对并讲解,避免因为版本变化使用过时的安装方式。
|
||||||
@@ -232,4 +247,4 @@ Python/
|
|||||||
5. 使用鸭子类型接入通知器;
|
5. 使用鸭子类型接入通知器;
|
||||||
6. 验证正常流程和主要异常流程。
|
6. 验证正常流程和主要异常流程。
|
||||||
|
|
||||||
下一步进入第四阶段数据库编程,将逐步学习 SQL,并把内存版图书管理系统升级为持久化版本。
|
第四阶段前两课已经完成,能够使用本地 TOML 配置、Psycopg 3 和参数化 SQL 安全访问远程 PostgreSQL,并通过转账场景掌握事务提交、异常回滚、批量操作以及 Repository/Service 分层。下一步进入 SQLAlchemy 2.x 基础。
|
||||||
|
|||||||
Reference in New Issue
Block a user