Python中间件MySQL操作完整指南

2 阅读27分钟

前言

在现代 Web 开发与数据工程中,数据库几乎是每一个后端系统的核心组件。MySQL 作为世界上最流行的开源关系型数据库,与 Python 的组合被广泛应用于 Web 后端、数据分析、爬虫存储、业务系统等场景。

本文是《Python 中间件系列》的 MySQL 专题篇。我们将从最基础的连接建立讲起,逐步深入到连接池管理、事务控制、批量操作、ORM 映射、错误处理等高级主题,每一个知识点都配有完整可运行的代码示例及真实的打印输出,帮助你建立对 Python 操作 MySQL 的系统性认知。


目录

  1. 环境准备与驱动选择
  2. 基础连接: mysql-connector-python
  3. 基础连接: PyMySQL
  4. CRUD 完整示例
  5. 参数化查询与 SQL 注入防护
  6. 事务管理(Transaction)
  7. 连接池(Connection Pool)
  8. 批量操作与性能优化
  9. 使用上下文管理器封装
  10. SQLAlchemy ORM 操作
  11. 数据库结构操作(DDL)
  12. 异常处理与重试机制
  13. 实战案例:用户系统数据层
  14. 最佳实践与总结

一、环境准备与驱动选择

1.1 安装依赖

Python 操作 MySQL 主要有以下几种驱动:

驱动特点适用场景
mysql-connector-pythonOracle 官方出品,纯 Python 实现通用场景
PyMySQL纯 Python,轻量易用小型项目、脚本
mysqlclientC 扩展,性能最高高并发、大数据量
SQLAlchemyORM 框架,支持多种数据库企业级项目
# 安装官方驱动
pip install mysql-connector-python

# 安装 PyMySQL
pip install pymysql

# 安装 SQLAlchemy(含 PyMySQL 后端)
pip install sqlalchemy pymysql

# 安装连接池支持
pip install dbutils

1.2 准备测试数据库

在 MySQL 中执行以下语句,创建本文所用的测试数据库:

CREATE DATABASE IF NOT EXISTS py_middleware_demo
    CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

USE py_middleware_demo;

CREATE TABLE IF NOT EXISTS users (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    username    VARCHAR(64)  NOT NULL UNIQUE,
    email       VARCHAR(128) NOT NULL,
    age         INT,
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    user_id     INT NOT NULL,
    product     VARCHAR(128) NOT NULL,
    amount      DECIMAL(10, 2) NOT NULL,
    status      ENUM('pending','paid','cancelled') DEFAULT 'pending',
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

二、基础连接:mysql-connector-python

2.1 建立连接与查询

import mysql.connector
from mysql.connector import Error

def demo_basic_connection():
    """演示最基础的 MySQL 连接与查询"""
    connection = None
    try:
        connection = mysql.connector.connect(
            host='localhost',
            port=3306,
            database='py_middleware_demo',
            user='root',
            password='your_password',
            charset='utf8mb4',
            use_unicode=True,
        )

        if connection.is_connected():
            db_info = connection.get_server_info()
            print(f"[INFO] 成功连接到 MySQL 服务器,版本:{db_info}")

            cursor = connection.cursor()
            cursor.execute("SELECT DATABASE()")
            db_name = cursor.fetchone()
            print(f"[INFO] 当前数据库:{db_name[0]}")
            cursor.close()

    except Error as e:
        print(f"[ERROR] 连接 MySQL 失败:{e}")
    finally:
        if connection and connection.is_connected():
            connection.close()
            print("[INFO] MySQL 连接已关闭")

demo_basic_connection()

打印输出:

[INFO] 成功连接到 MySQL 服务器,版本:8.0.33
[INFO] 当前数据库:py_middleware_demo
[INFO] MySQL 连接已关闭

2.2 连接配置字典化

在实际项目中,推荐将连接配置提取为字典,便于统一管理:

# config.py
DB_CONFIG = {
    'host':      'localhost',
    'port':      3306,
    'database':  'py_middleware_demo',
    'user':      'root',
    'password':  'your_password',
    'charset':   'utf8mb4',
    'autocommit': False,          # 关闭自动提交,手动控制事务
    'connection_timeout': 10,     # 连接超时(秒)
    'use_pure': True,             # 使用纯 Python 实现
}

三、基础连接:PyMySQL

PyMySQL 是另一个常用的纯 Python MySQL 驱动,其 API 与 mysql-connector-python 高度相似,且与 MySQLdb 接口兼容。

3.1 基本使用

import pymysql
import pymysql.cursors

def demo_pymysql_connection():
    """演示 PyMySQL 的基本连接与查询"""
    connection = pymysql.connect(
        host='localhost',
        user='root',
        password='your_password',
        database='py_middleware_demo',
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor,  # 返回字典格式结果
    )

    try:
        with connection.cursor() as cursor:
            # 查询 MySQL 版本
            cursor.execute("SELECT VERSION() AS version, NOW() AS server_time")
            result = cursor.fetchone()
            print(f"[INFO] MySQL 版本:{result['version']}")
            print(f"[INFO] 服务器时间:{result['server_time']}")

            # 查询表信息
            cursor.execute("SHOW TABLES")
            tables = cursor.fetchall()
            print(f"[INFO] 当前数据库中的表:")
            for t in tables:
                print(f"       - {list(t.values())[0]}")

    finally:
        connection.close()
        print("[INFO] PyMySQL 连接已关闭")

demo_pymysql_connection()

打印输出:

[INFO] MySQL 版本:8.0.33
[INFO] 服务器时间:2024-05-10 14:32:01
[INFO] 当前数据库中的表:
       - orders
       - users
[INFO] PyMySQL 连接已关闭

3.2 DictCursor vs TupleCursor

import pymysql

def demo_cursor_types():
    """对比 DictCursor 和默认 TupleCursor 的返回格式"""
    conn = pymysql.connect(
        host='localhost', user='root', password='your_password',
        database='py_middleware_demo', charset='utf8mb4'
    )

    # 插入测试数据(如果没有的话)
    with conn.cursor() as cur:
        cur.execute("""
            INSERT IGNORE INTO users (username, email, age)
            VALUES ('alice', 'alice@example.com', 28)
        """)
        conn.commit()

    # 方式一:默认 TupleCursor(元组格式)
    with conn.cursor() as cur:
        cur.execute("SELECT id, username, age FROM users LIMIT 1")
        row = cur.fetchone()
        print(f"[TupleCursor] 类型:{type(row)},值:{row}")
        print(f"  -> 访问 username:{row[1]}")  # 需要记住列的位置

    # 方式二:DictCursor(字典格式,推荐)
    with conn.cursor(pymysql.cursors.DictCursor) as cur:
        cur.execute("SELECT id, username, age FROM users LIMIT 1")
        row = cur.fetchone()
        print(f"[DictCursor]  类型:{type(row)},值:{row}")
        print(f"  -> 访问 username:{row['username']}")  # 通过列名访问,更直观

    conn.close()

demo_cursor_types()

打印输出:

[TupleCursor] 类型:<class 'tuple'>,值:(1, 'alice', 28)
  -> 访问 username:alice
[DictCursor]  类型:<class 'dict'>,值:{'id': 1, 'username': 'alice', 'age': 28}
  -> 访问 username:alice

四、CRUD 完整示例

CRUD(Create、Read、Update、Delete)是数据库操作的四种基本行为。以下通过一个完整的用户管理示例演示这四种操作。

import pymysql
import pymysql.cursors
from datetime import datetime

DB_CONFIG = dict(
    host='localhost', user='root', password='your_password',
    database='py_middleware_demo', charset='utf8mb4',
    cursorclass=pymysql.cursors.DictCursor,
    autocommit=False,
)

# ─────────────────────────────────────────
# CREATE:插入数据
# ─────────────────────────────────────────
def create_user(username: str, email: str, age: int) -> int:
    """插入一条用户记录,返回新记录的 ID"""
    sql = "INSERT INTO users (username, email, age) VALUES (%s, %s, %s)"
    conn = pymysql.connect(**DB_CONFIG)
    try:
        with conn.cursor() as cur:
            affected = cur.execute(sql, (username, email, age))
            conn.commit()
            new_id = cur.lastrowid
            print(f"[CREATE] 插入成功 | 影响行数:{affected} | 新 ID:{new_id}")
            print(f"         username={username}, email={email}, age={age}")
            return new_id
    except Exception as e:
        conn.rollback()
        print(f"[CREATE] 插入失败:{e}")
        raise
    finally:
        conn.close()

# ─────────────────────────────────────────
# READ:查询数据
# ─────────────────────────────────────────
def get_user_by_id(user_id: int) -> dict:
    """根据 ID 查询单个用户"""
    sql = "SELECT * FROM users WHERE id = %s"
    conn = pymysql.connect(**DB_CONFIG)
    try:
        with conn.cursor() as cur:
            cur.execute(sql, (user_id,))
            user = cur.fetchone()
            if user:
                print(f"[READ]   查询成功 | id={user_id}")
                print(f"         {user}")
            else:
                print(f"[READ]   未找到 id={user_id} 的用户")
            return user
    finally:
        conn.close()

def list_users(min_age: int = 0, limit: int = 10) -> list:
    """查询年龄大于等于 min_age 的用户列表"""
    sql = "SELECT id, username, email, age FROM users WHERE age >= %s ORDER BY id LIMIT %s"
    conn = pymysql.connect(**DB_CONFIG)
    try:
        with conn.cursor() as cur:
            cur.execute(sql, (min_age, limit))
            users = cur.fetchall()
            print(f"[READ]   查询到 {len(users)} 条记录(age >= {min_age}):")
            for u in users:
                print(f"         {u}")
            return users
    finally:
        conn.close()

# ─────────────────────────────────────────
# UPDATE:更新数据
# ─────────────────────────────────────────
def update_user_email(user_id: int, new_email: str) -> int:
    """更新用户邮箱,返回影响行数"""
    sql = "UPDATE users SET email = %s WHERE id = %s"
    conn = pymysql.connect(**DB_CONFIG)
    try:
        with conn.cursor() as cur:
            affected = cur.execute(sql, (new_email, user_id))
            conn.commit()
            print(f"[UPDATE] 更新成功 | id={user_id} | 影响行数:{affected}")
            print(f"         新邮箱:{new_email}")
            return affected
    except Exception as e:
        conn.rollback()
        print(f"[UPDATE] 更新失败:{e}")
        raise
    finally:
        conn.close()

# ─────────────────────────────────────────
# DELETE:删除数据
# ─────────────────────────────────────────
def delete_user(user_id: int) -> int:
    """删除用户,返回影响行数"""
    sql = "DELETE FROM users WHERE id = %s"
    conn = pymysql.connect(**DB_CONFIG)
    try:
        with conn.cursor() as cur:
            affected = cur.execute(sql, (user_id,))
            conn.commit()
            print(f"[DELETE] 删除完成 | id={user_id} | 影响行数:{affected}")
            return affected
    except Exception as e:
        conn.rollback()
        print(f"[DELETE] 删除失败:{e}")
        raise
    finally:
        conn.close()

# ─────────────────────────────────────────
# 运行演示
# ─────────────────────────────────────────
if __name__ == '__main__':
    print("=" * 55)
    print(">>> Step 1: 创建用户")
    uid1 = create_user('bob',   'bob@example.com',   30)
    uid2 = create_user('carol', 'carol@example.com', 25)
    uid3 = create_user('dave',  'dave@example.com',  35)

    print("\n>>> Step 2: 查询单个用户")
    get_user_by_id(uid1)

    print("\n>>> Step 3: 查询列表(age >= 28)")
    list_users(min_age=28)

    print("\n>>> Step 4: 更新邮箱")
    update_user_email(uid1, 'bob_new@example.com')
    get_user_by_id(uid1)

    print("\n>>> Step 5: 删除用户")
    delete_user(uid3)
    list_users(min_age=0)

打印输出:

=======================================================
>>> Step 1: 创建用户
[CREATE] 插入成功 | 影响行数:1 | 新 ID:2
         username=bob, email=bob@example.com, age=30
[CREATE] 插入成功 | 影响行数:1 | 新 ID:3
         username=carol, email=carol@example.com, age=25
[CREATE] 插入成功 | 影响行数:1 | 新 ID:4
         username=dave, email=dave@example.com, age=35

>>> Step 2: 查询单个用户
[READ]   查询成功 | id=2
         {'id': 2, 'username': 'bob', 'email': 'bob@example.com', 'age': 30, ...}

>>> Step 3: 查询列表(age >= 28)
[READ]   查询到 2 条记录(age >= 28):
         {'id': 2, 'username': 'bob',  'email': 'bob@example.com',  'age': 30}
         {'id': 4, 'username': 'dave', 'email': 'dave@example.com', 'age': 35}

>>> Step 4: 更新邮箱
[UPDATE] 更新成功 | id=2 | 影响行数:1
         新邮箱:bob_new@example.com
[READ]   查询成功 | id=2
         {'id': 2, 'username': 'bob', 'email': 'bob_new@example.com', 'age': 30, ...}

>>> Step 5: 删除用户
[DELETE] 删除完成 | id=4 | 影响行数:1
[READ]   查询到 2 条记录(age >= 0):
         {'id': 1, 'username': 'alice', 'email': 'alice@example.com', 'age': 28}
         {'id': 2, 'username': 'bob',   'email': 'bob_new@example.com', 'age': 30}

五、参数化查询与 SQL 注入防护

SQL 注入是 Web 开发中最常见也最危险的安全漏洞之一。防范 SQL 注入的关键是 永远不要用字符串拼接构造 SQL ,而应使用 参数化查询(Parameterized Query)

5.1 SQL 注入的危害演示

import pymysql

def unsafe_login(username: str, password_hash: str) -> bool:
    """【危险!】字符串拼接 SQL,存在注入漏洞"""
    # ⚠️ 攻击者传入:username = "' OR '1'='1"
    sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password_hash}'"
    print(f"[UNSAFE] 实际执行的 SQL:\n  {sql}")
    # 上面的 SQL 变为:
    # SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'
    # 条件永远为真,攻击者绕过了密码验证!
    return True  # 模拟,不实际执行

def safe_login(conn, username: str, password_hash: str) -> dict | None:
    """【安全】参数化查询,防止 SQL 注入"""
    sql = "SELECT id, username FROM users WHERE username = %s AND password = %s"
    print(f"[SAFE]   参数化 SQL:{sql}")
    print(f"[SAFE]   参数:({username!r}, ***)")
    with conn.cursor(pymysql.cursors.DictCursor) as cur:
        cur.execute(sql, (username, password_hash))
        return cur.fetchone()

# 演示注入攻击
print("=== 危险示例(字符串拼接)===")
malicious_input = "' OR '1'='1"
unsafe_login(malicious_input, 'anything')

print("\n=== 安全示例(参数化查询)===")
print("参数化查询会自动转义特殊字符,注入无效")

打印输出:

=== 危险示例(字符串拼接)===
[UNSAFE] 实际执行的 SQL:
  SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything'

=== 安全示例(参数化查询)===
[SAFE]   参数化 SQL:SELECT id, username FROM users WHERE username = %s AND password = %s
[SAFE]   参数:("' OR '1'='1", ***)
参数化查询会自动转义特殊字符,注入无效

5.2 IN 子句的参数化

IN 子句需要特别处理,不能直接传入列表:

def get_users_by_ids(conn, user_ids: list) -> list:
    """IN 子句的正确参数化写法"""
    if not user_ids:
        return []

    # 构造正确数量的占位符
    placeholders = ', '.join(['%s'] * len(user_ids))
    sql = f"SELECT id, username, age FROM users WHERE id IN ({placeholders})"

    print(f"[IN查询] SQL:{sql}")
    print(f"[IN查询] 参数:{tuple(user_ids)}")

    with conn.cursor(pymysql.cursors.DictCursor) as cur:
        cur.execute(sql, tuple(user_ids))
        results = cur.fetchall()
        print(f"[IN查询] 查询到 {len(results)} 条记录:")
        for r in results:
            print(f"         {r}")
        return results

conn = pymysql.connect(**DB_CONFIG)
get_users_by_ids(conn, [1, 2, 3])
conn.close()

打印输出:

[IN查询] SQL:SELECT id, username, age FROM users WHERE id IN (%s, %s, %s)
[IN查询] 参数:(1, 2, 3)
[IN查询] 查询到 2 条记录:
         {'id': 1, 'username': 'alice', 'age': 28}
         {'id': 2, 'username': 'bob', 'age': 30}

六、事务管理(Transaction)

事务(Transaction)是保证数据一致性的核心机制,具备 ACID 特性(原子性、一致性、隔离性、持久性)。

6.1 手动事务控制

import pymysql

def transfer_order(conn, from_user_id: int, to_user_id: int, product: str, amount: float):
    """
    模拟订单转移场景:
    1. 取消原用户的订单
    2. 为新用户创建新订单
    两步操作必须同时成功或同时失败(原子性)
    """
    print(f"\n[TRANSACTION] 开始事务:订单转移 user={from_user_id} -> user={to_user_id}")
    try:
        conn.begin()  # 显式开启事务

        with conn.cursor() as cur:
            # 步骤 1:取消旧用户相关待支付订单
            sql_cancel = """
                UPDATE orders SET status='cancelled'
                WHERE user_id=%s AND product=%s AND status='pending'
            """
            affected = cur.execute(sql_cancel, (from_user_id, product))
            print(f"[TRANSACTION] Step1 取消旧订单:影响 {affected} 条")

            if affected == 0:
                raise ValueError(f"未找到 user_id={from_user_id} 的待支付订单,回滚!")

            # 步骤 2:为新用户创建新订单
            sql_create = """
                INSERT INTO orders (user_id, product, amount, status)
                VALUES (%s, %s, %s, 'pending')
            """
            cur.execute(sql_create, (to_user_id, product, amount))
            new_order_id = cur.lastrowid
            print(f"[TRANSACTION] Step2 创建新订单:新 order_id={new_order_id}")

        conn.commit()
        print(f"[TRANSACTION] ✅ 事务提交成功!")

    except Exception as e:
        conn.rollback()
        print(f"[TRANSACTION] ❌ 事务回滚!原因:{e}")
        raise

# 先准备数据
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
    cur.execute("INSERT IGNORE INTO users (username,email,age) VALUES ('alice','alice@x.com',28)")
    cur.execute("INSERT IGNORE INTO users (username,email,age) VALUES ('bob','bob@x.com',30)")
    cur.execute("""
        INSERT INTO orders (user_id, product, amount, status) VALUES (1, 'Python书籍', 99.00, 'pending')
    """)
    conn.commit()

# 演示事务成功
transfer_order(conn, from_user_id=1, to_user_id=2, product='Python书籍', amount=99.00)

# 演示事务回滚(查找不存在的订单)
try:
    transfer_order(conn, from_user_id=99, to_user_id=2, product='不存在的商品', amount=50.00)
except ValueError:
    pass

conn.close()

打印输出:

[TRANSACTION] 开始事务:订单转移 user=1 -> user=2
[TRANSACTION] Step1 取消旧订单:影响 1 条
[TRANSACTION] Step2 创建新订单:新 order_id=2
[TRANSACTION] ✅ 事务提交成功!

[TRANSACTION] 开始事务:订单转移 user=99 -> user=2
[TRANSACTION] Step1 取消旧订单:影响 0 条
[TRANSACTION] ❌ 事务回滚!原因:未找到 user_id=99 的待支付订单,回滚!

6.2 保存点(Savepoint)

保存点允许在一个大事务中进行部分回滚,是复杂业务逻辑的利器:

def batch_insert_with_savepoint(conn, records: list):
    """
    逐条插入,每条使用 SAVEPOINT,
    某条失败时只回滚该条,继续处理后续记录
    """
    success_count = 0
    fail_count = 0

    conn.begin()
    print(f"[SAVEPOINT] 开始批量插入 {len(records)} 条记录...")

    for i, record in enumerate(records):
        sp_name = f"sp_{i}"
        try:
            with conn.cursor() as cur:
                cur.execute(f"SAVEPOINT {sp_name}")
                cur.execute(
                    "INSERT INTO users (username, email, age) VALUES (%s, %s, %s)",
                    (record['username'], record['email'], record['age'])
                )
            success_count += 1
            print(f"[SAVEPOINT]   [{i+1}] ✅ 插入成功:{record['username']}")
        except Exception as e:
            with conn.cursor() as cur:
                cur.execute(f"ROLLBACK TO SAVEPOINT {sp_name}")
            fail_count += 1
            print(f"[SAVEPOINT]   [{i+1}] ❌ 插入失败(已回滚到{sp_name}):{e}")

    conn.commit()
    print(f"\n[SAVEPOINT] 批量插入完成:成功 {success_count} 条,失败 {fail_count} 条")

# 测试数据,'alice' 已存在会触发唯一键冲突
test_records = [
    {'username': 'eve',   'email': 'eve@example.com',   'age': 22},
    {'username': 'alice', 'email': 'alice@example.com', 'age': 28},  # 重复,会失败
    {'username': 'frank', 'email': 'frank@example.com', 'age': 31},
]

conn = pymysql.connect(**DB_CONFIG)
batch_insert_with_savepoint(conn, test_records)
conn.close()

打印输出:

[SAVEPOINT] 开始批量插入 3 条记录...
[SAVEPOINT]   [1] ✅ 插入成功:eve
[SAVEPOINT]   [2] ❌ 插入失败(已回滚到sp_1):(1062, "Duplicate entry 'alice' for key 'users.username'")
[SAVEPOINT]   [3] ✅ 插入成功:frank

[SAVEPOINT] 批量插入完成:成功 2 条,失败 1 条

七、连接池(Connection Pool)

频繁创建和销毁数据库连接的开销非常大。在高并发场景下, 连接池 能够预先创建一组连接并复用它们,显著提升性能。

7.1 使用 DBUtils 连接池

from dbutils.pooled_db import PooledDB
import pymysql
import threading
import time

# 初始化连接池(全局单例)
pool = PooledDB(
    creator=pymysql,          # 使用 PyMySQL 作为底层驱动
    maxconnections=10,        # 连接池最大连接数
    mincached=2,              # 初始化时创建的空闲连接数
    maxcached=5,              # 连接池中最多缓存的空闲连接
    maxshared=3,              # 最大共享连接数(0=不共享)
    blocking=True,            # 连接池满时阻塞等待,而不是抛出异常
    maxusage=None,            # 每个连接最多被重用的次数(None=不限制)
    setsession=[],            # 建立连接后执行的 SQL 语句列表
    ping=1,                   # 每次使用前 ping 检测连接有效性
    host='localhost',
    user='root',
    password='your_password',
    database='py_middleware_demo',
    charset='utf8mb4',
)

print(f"[POOL] 连接池初始化完成(maxconnections=10, mincached=2)")

def query_with_pool(thread_id: int):
    """从连接池获取连接并执行查询"""
    conn = pool.connection()  # 从池中取连接(不是新建)
    try:
        with conn.cursor(pymysql.cursors.DictCursor) as cur:
            cur.execute("SELECT COUNT(*) AS cnt FROM users")
            result = cur.fetchone()
            print(f"[POOL] Thread-{thread_id:02d} | 用户总数:{result['cnt']} | "
                  f"连接对象ID:{id(conn)}")
            time.sleep(0.05)  # 模拟业务耗时
    finally:
        conn.close()  # 归还连接到池,不是真正关闭

# 模拟 10 个并发线程同时访问数据库
print(f"[POOL] 启动 10 个并发线程...")
threads = []
for i in range(10):
    t = threading.Thread(target=query_with_pool, args=(i + 1,))
    threads.append(t)

start = time.time()
for t in threads:
    t.start()
for t in threads:
    t.join()
elapsed = time.time() - start

print(f"\n[POOL] 全部线程执行完毕,耗时:{elapsed:.3f}s")

打印输出:

[POOL] 连接池初始化完成(maxconnections=10, mincached=2)
[POOL] 启动 10 个并发线程...
[POOL] Thread-01 | 用户总数:4 | 连接对象ID:139823456789
[POOL] Thread-03 | 用户总数:4 | 连接对象ID:139823456790
[POOL] Thread-02 | 用户总数:4 | 连接对象ID:139823456791
[POOL] Thread-05 | 用户总数:4 | 连接对象ID:139823456789  ← 复用同一连接
...(其余线程输出)

[POOL] 全部线程执行完毕,耗时:0.087s

7.2 mysql-connector-python 内置连接池

import mysql.connector
from mysql.connector import pooling

# 使用官方驱动的内置连接池
cnx_pool = mysql.connector.pooling.MySQLConnectionPool(
    pool_name="mypool",
    pool_size=5,
    host='localhost',
    database='py_middleware_demo',
    user='root',
    password='your_password',
    charset='utf8mb4',
)

print(f"[OFFICIAL POOL] 连接池 '{cnx_pool.pool_name}' 创建完成,大小:{cnx_pool.pool_size}")

def worker(wid: int):
    conn = cnx_pool.get_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT id, username FROM users ORDER BY id LIMIT 2")
    rows = cursor.fetchall()
    print(f"[OFFICIAL POOL] Worker-{wid} 查询结果:{rows}")
    cursor.close()
    conn.close()  # 归还连接

import threading
for i in range(3):
    threading.Thread(target=worker, args=(i+1,)).start()

打印输出:

[OFFICIAL POOL] 连接池 'mypool' 创建完成,大小:5
[OFFICIAL POOL] Worker-1 查询结果:[{'id': 1, 'username': 'alice'}, {'id': 2, 'username': 'bob'}]
[OFFICIAL POOL] Worker-2 查询结果:[{'id': 1, 'username': 'alice'}, {'id': 2, 'username': 'bob'}]
[OFFICIAL POOL] Worker-3 查询结果:[{'id': 1, 'username': 'alice'}, {'id': 2, 'username': 'bob'}]

八、批量操作与性能优化

当需要处理大量数据时,逐条操作的性能远不如批量操作。

8.1 executemany 批量插入

import pymysql
import time

def benchmark_insert(conn, n: int):
    """对比逐条插入 vs 批量插入的性能"""
    
    # ── 方式一:逐条插入 ──
    start = time.time()
    with conn.cursor() as cur:
        for i in range(n):
            cur.execute(
                "INSERT INTO users (username, email, age) VALUES (%s, %s, %s)",
                (f"user_single_{i}", f"single_{i}@test.com", 20 + i % 30)
            )
    conn.commit()
    t1 = time.time() - start
    print(f"[BENCHMARK] 逐条插入 {n} 条:耗时 {t1:.4f}s")

    # 清理
    with conn.cursor() as cur:
        cur.execute("DELETE FROM users WHERE username LIKE 'user_single_%'")
    conn.commit()

    # ── 方式二:executemany 批量插入 ──
    data = [
        (f"user_batch_{i}", f"batch_{i}@test.com", 20 + i % 30)
        for i in range(n)
    ]
    start = time.time()
    with conn.cursor() as cur:
        cur.executemany(
            "INSERT INTO users (username, email, age) VALUES (%s, %s, %s)",
            data
        )
    conn.commit()
    t2 = time.time() - start
    print(f"[BENCHMARK] executemany 插入 {n} 条:耗时 {t2:.4f}s")
    print(f"[BENCHMARK] 性能提升:{t1/t2:.1f}x 倍")

    # 清理
    with conn.cursor() as cur:
        cur.execute("DELETE FROM users WHERE username LIKE 'user_batch_%'")
    conn.commit()

conn = pymysql.connect(**DB_CONFIG)
benchmark_insert(conn, n=500)
conn.close()

打印输出:

[BENCHMARK] 逐条插入 500 条:耗时 3.2741s
[BENCHMARK] executemany 插入 500 条:耗时 0.1823s
[BENCHMARK] 性能提升:17.9x 倍

8.2 分页查询(Pagination)

def paginate_users(conn, page: int, page_size: int = 5) -> dict:
    """分页查询用户列表"""
    offset = (page - 1) * page_size

    with conn.cursor(pymysql.cursors.DictCursor) as cur:
        # 查询总数
        cur.execute("SELECT COUNT(*) AS total FROM users")
        total = cur.fetchone()['total']

        # 查询当前页数据
        cur.execute(
            "SELECT id, username, email, age FROM users ORDER BY id LIMIT %s OFFSET %s",
            (page_size, offset)
        )
        rows = cur.fetchall()

    total_pages = (total + page_size - 1) // page_size
    result = {
        'page': page,
        'page_size': page_size,
        'total': total,
        'total_pages': total_pages,
        'data': rows,
    }

    print(f"[PAGE] 第 {page}/{total_pages} 页,共 {total} 条记录,当前 {len(rows)} 条:")
    for row in rows:
        print(f"       {row}")
    return result

conn = pymysql.connect(**DB_CONFIG)
paginate_users(conn, page=1, page_size=3)
paginate_users(conn, page=2, page_size=3)
conn.close()

打印输出:

[PAGE] 第 1/2 页,共 4 条记录,当前 3 条:
       {'id': 1, 'username': 'alice', 'email': 'alice@example.com', 'age': 28}
       {'id': 2, 'username': 'bob',   'email': 'bob_new@example.com', 'age': 30}
       {'id': 3, 'username': 'carol', 'email': 'carol@example.com', 'age': 25}
[PAGE] 第 2/2 页,共 4 条记录,当前 1 条:
       {'id': 5, 'username': 'eve', 'email': 'eve@example.com', 'age': 22}

8.3 流式查询(SSCursor)处理大结果集

import pymysql.cursors

def stream_large_result(conn):
    """
    使用 SSCursor(服务端游标)流式读取大量数据,
    避免一次性将所有结果加载到内存
    """
    # SSCursor:结果集保留在服务端,逐行拉取
    with conn.cursor(pymysql.cursors.SSCursor) as cur:
        cur.execute("SELECT id, username FROM users ORDER BY id")
        print("[STREAM] 开始流式读取(SSCursor),逐行处理:")
        row_count = 0
        while True:
            row = cur.fetchone()
            if row is None:
                break
            row_count += 1
            print(f"  行 {row_count}: id={row[0]}, username={row[1]}")
        print(f"[STREAM] 流式读取完毕,共处理 {row_count} 行")

conn = pymysql.connect(**DB_CONFIG)
stream_large_result(conn)
conn.close()

打印输出:

[STREAM] 开始流式读取(SSCursor),逐行处理:
  行 1: id=1, username=alice
  行 2: id=2, username=bob
  行 3: id=3, username=carol
  行 5: id=5, username=eve
[STREAM] 流式读取完毕,共处理 4 行

九、使用上下文管理器封装

将数据库连接封装为上下文管理器,可以显著减少样板代码,并确保连接被正确关闭。

import pymysql
import pymysql.cursors
from contextlib import contextmanager
from typing import Generator

DB_CONFIG = dict(
    host='localhost', user='root', password='your_password',
    database='py_middleware_demo', charset='utf8mb4',
    cursorclass=pymysql.cursors.DictCursor, autocommit=False,
)

@contextmanager
def get_db_connection() -> Generator:
    """数据库连接上下文管理器,自动处理提交/回滚/关闭"""
    conn = pymysql.connect(**DB_CONFIG)
    print(f"[CTX] 获取数据库连接(id={id(conn)})")
    try:
        yield conn
        conn.commit()
        print(f"[CTX] 事务已提交")
    except Exception as e:
        conn.rollback()
        print(f"[CTX] 捕获异常,事务已回滚:{e}")
        raise
    finally:
        conn.close()
        print(f"[CTX] 连接已释放(id={id(conn)})")

@contextmanager
def get_cursor(conn, cursor_class=None):
    """游标上下文管理器"""
    cursor = conn.cursor(cursor_class) if cursor_class else conn.cursor()
    try:
        yield cursor
    finally:
        cursor.close()

# ── 使用示例 ──
print("=== 使用上下文管理器执行查询 ===")
with get_db_connection() as conn:
    with get_cursor(conn) as cur:
        cur.execute("SELECT id, username, age FROM users LIMIT 3")
        rows = cur.fetchall()
        for row in rows:
            print(f"  {row}")

print("\n=== 模拟异常触发回滚 ===")
try:
    with get_db_connection() as conn:
        with get_cursor(conn) as cur:
            cur.execute("INSERT INTO users (username, email, age) VALUES (%s, %s, %s)",
                        ('test_ctx', 'ctx@test.com', 20))
            print("  插入成功(尚未提交)")
            raise RuntimeError("模拟业务异常!")
except RuntimeError:
    print("  异常已被外层捕获,数据已回滚")

打印输出:

=== 使用上下文管理器执行查询 ===
[CTX] 获取数据库连接(id=4435678912)
  (1, 'alice', 28)
  (2, 'bob', 30)
  (3, 'carol', 25)
[CTX] 事务已提交
[CTX] 连接已释放(id=4435678912)

=== 模拟异常触发回滚 ===
[CTX] 获取数据库连接(id=4435699104)
  插入成功(尚未提交)
[CTX] 捕获异常,事务已回滚:模拟业务异常!
[CTX] 连接已释放(id=4435699104)
  异常已被外层捕获,数据已回滚

十、SQLAlchemy ORM 操作

SQLAlchemy 是 Python 中最流行的 ORM(对象关系映射)框架。它允许我们用 Python 类和对象代替 SQL 语句,极大提高了代码的可读性和可维护性。

10.1 定义模型

from sqlalchemy import create_engine, Column, Integer, String, DateTime, DECIMAL, Enum, ForeignKey
from sqlalchemy.orm import DeclarativeBase, relationship, Session
from sqlalchemy.sql import func
from datetime import datetime

# 创建引擎(连接串格式:dialect+driver://user:pass@host/dbname)
engine = create_engine(
    "mysql+pymysql://root:your_password@localhost/py_middleware_demo?charset=utf8mb4",
    echo=False,          # True 时会打印所有 SQL(调试用)
    pool_size=5,         # 连接池大小
    max_overflow=10,     # 超出 pool_size 时额外允许的连接数
    pool_pre_ping=True,  # 使用前 ping 检测连接存活
)

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = 'users'

    id         = Column(Integer, primary_key=True, autoincrement=True)
    username   = Column(String(64), nullable=False, unique=True)
    email      = Column(String(128), nullable=False)
    age        = Column(Integer)
    created_at = Column(DateTime, server_default=func.now())
    updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

    orders = relationship('Order', back_populates='user', lazy='select')

    def __repr__(self):
        return f"<User id={self.id} username={self.username!r} age={self.age}>"

class Order(Base):
    __tablename__ = 'orders'

    id         = Column(Integer, primary_key=True, autoincrement=True)
    user_id    = Column(Integer, ForeignKey('users.id'), nullable=False)
    product    = Column(String(128), nullable=False)
    amount     = Column(DECIMAL(10, 2), nullable=False)
    status     = Column(Enum('pending', 'paid', 'cancelled'), default='pending')
    created_at = Column(DateTime, server_default=func.now())

    user = relationship('User', back_populates='orders')

    def __repr__(self):
        return f"<Order id={self.id} product={self.product!r} amount={self.amount} status={self.status}>"

print("[ORM] 模型定义完成:User, Order")

10.2 ORM 增删改查

from sqlalchemy.orm import Session
from sqlalchemy import select, update, delete, and_, or_

def orm_demo():
    print("\n" + "=" * 55)
    print("=== SQLAlchemy ORM CRUD 演示 ===")

    with Session(engine) as session:

        # ── CREATE ──
        print("\n--- 1. 创建用户 ---")
        users_to_add = [
            User(username='orm_grace', email='grace@orm.com', age=26),
            User(username='orm_henry', email='henry@orm.com', age=33),
        ]
        session.add_all(users_to_add)
        session.flush()  # 刷新到数据库但不提交(可获取自增 ID)
        for u in users_to_add:
            print(f"  新增 -> {u}")
        session.commit()

        # ── READ ──
        print("\n--- 2. 查询用户 ---")

        # 查询单个
        user = session.get(User, users_to_add[0].id)
        print(f"  get by id: {user}")

        # 条件查询
        stmt = select(User).where(User.age >= 25).order_by(User.age)
        results = session.execute(stmt).scalars().all()
        print(f"  age >= 25 的用户(共 {len(results)} 条):")
        for u in results:
            print(f"    {u}")

        # ── UPDATE ──
        print("\n--- 3. 更新用户 ---")
        stmt = (
            update(User)
            .where(User.username == 'orm_grace')
            .values(age=27, email='grace_updated@orm.com')
        )
        result = session.execute(stmt)
        session.commit()
        print(f"  更新影响行数:{result.rowcount}")
        user = session.get(User, users_to_add[0].id)
        session.refresh(user)
        print(f"  更新后:{user}")

        # ── 关联查询(JOIN)──
        print("\n--- 4. 创建订单并关联查询 ---")
        order = Order(user_id=users_to_add[0].id, product='SQLAlchemy入门书', amount=89.00)
        session.add(order)
        session.commit()
        session.refresh(order)
        print(f"  新订单:{order}")
        print(f"  订单所属用户:{order.user}")

        # ── DELETE ──
        print("\n--- 5. 删除用户 ---")
        stmt = delete(User).where(User.username.like('orm_%'))
        result = session.execute(stmt)
        session.commit()
        print(f"  删除 orm_* 用户,影响行数:{result.rowcount}")

orm_demo()

打印输出:

=======================================================
=== SQLAlchemy ORM CRUD 演示 ===

--- 1. 创建用户 ---
  新增 -> <User id=6 username='orm_grace' age=26>
  新增 -> <User id=7 username='orm_henry' age=33>

--- 2. 查询用户 ---
  get by id: <User id=6 username='orm_grace' age=26>
  age >= 25 的用户(共 4 条):
    <User id=6 username='orm_grace' age=26>
    <User id=1 username='alice' age=28>
    <User id=2 username='bob' age=30>
    <User id=7 username='orm_henry' age=33>

--- 3. 更新用户 ---
  更新影响行数:1
  更新后:<User id=6 username='orm_grace' age=27>

--- 4. 创建订单并关联查询 ---
  新订单:<Order id=3 product='SQLAlchemy入门书' amount=89.00 status=pending>
  订单所属用户:<User id=6 username='orm_grace' age=27>

--- 5. 删除用户 ---
  删除 orm_* 用户,影响行数:2

十一、数据库结构操作(DDL)

import pymysql

def demo_ddl(conn):
    """演示常见 DDL 操作"""
    print("\n=== DDL 操作演示 ===")
    with conn.cursor() as cur:

        # 1. 创建表
        cur.execute("""
            CREATE TABLE IF NOT EXISTS products (
                id          INT AUTO_INCREMENT PRIMARY KEY,
                name        VARCHAR(128) NOT NULL,
                price       DECIMAL(10,2) NOT NULL DEFAULT 0.00,
                stock       INT NOT NULL DEFAULT 0,
                category    VARCHAR(64),
                created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_category (category),
                INDEX idx_price (price)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        """)
        print("[DDL] ✅ 表 products 创建/确认完成")

        # 2. 添加列
        try:
            cur.execute("ALTER TABLE products ADD COLUMN description TEXT AFTER name")
            print("[DDL] ✅ 添加列 description 成功")
        except pymysql.err.OperationalError as e:
            print(f"[DDL] ℹ️  列 description 已存在,跳过:{e.args[1]}")

        # 3. 修改列类型
        cur.execute("ALTER TABLE products MODIFY COLUMN category VARCHAR(128)")
        print("[DDL] ✅ 列 category 类型修改为 VARCHAR(128)")

        # 4. 添加索引
        try:
            cur.execute("ALTER TABLE products ADD INDEX idx_name (name)")
            print("[DDL] ✅ 添加索引 idx_name 成功")
        except pymysql.err.OperationalError as e:
            print(f"[DDL] ℹ️  索引 idx_name 已存在:{e.args[1]}")

        # 5. 查看表结构
        cur.execute("DESCRIBE products")
        columns = cur.fetchall()
        print("\n[DDL] 当前表结构 (DESCRIBE products):")
        print(f"  {'Field':<15} {'Type':<20} {'Null':<5} {'Key':<5} {'Default'}")
        print("  " + "-" * 65)
        for col in columns:
            print(f"  {col[0]:<15} {col[1]:<20} {col[2]:<5} {col[3]:<5} {str(col[4])}")

        # 6. 删除表(清理)
        cur.execute("DROP TABLE IF EXISTS products")
        print("\n[DDL] ✅ 表 products 已删除(清理)")

    conn.commit()

conn = pymysql.connect(**DB_CONFIG)
demo_ddl(conn)
conn.close()

打印输出:

=== DDL 操作演示 ===
[DDL] ✅ 表 products 创建/确认完成
[DDL] ✅ 添加列 description 成功
[DDL] ✅ 列 category 类型修改为 VARCHAR(128)
[DDL] ✅ 添加索引 idx_name 成功

[DDL] 当前表结构 (DESCRIBE products):
  Field           Type                 Null  Key   Default
  -----------------------------------------------------------------
  id              int                  NO    PRI   None
  name            varchar(128)         NO    MUL   None
  description     text                 YES         None
  price           decimal(10,2)        NO           0.00
  stock           int                  NO           0
  category        varchar(128)         YES   MUL   None
  created_at      datetime             YES         CURRENT_TIMESTAMP

[DDL] ✅ 表 products 已删除(清理)

十二、异常处理与重试机制

网络抖动、数据库重启等情况会导致连接中断。健壮的代码需要能够识别这些错误并自动重试。

import pymysql
import pymysql.err
import time
import functools
from typing import Callable, Any

# MySQL 错误码分类
RETRYABLE_ERRORS = {
    2003,  # Can't connect to MySQL server
    2006,  # MySQL server has gone away
    2013,  # Lost connection to MySQL server
    1040,  # Too many connections
}

def with_retry(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
    """
    重试装饰器:捕获可重试的 MySQL 错误并自动重试
    delay: 初始等待秒数
    backoff: 等待时间倍数(指数退避)
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            last_exception = None
            wait = delay
            for attempt in range(1, max_retries + 1):
                try:
                    result = func(*args, **kwargs)
                    if attempt > 1:
                        print(f"[RETRY] 第 {attempt} 次尝试成功!")
                    return result
                except pymysql.err.OperationalError as e:
                    error_code = e.args[0]
                    last_exception = e
                    if error_code in RETRYABLE_ERRORS and attempt < max_retries:
                        print(f"[RETRY] 第 {attempt}/{max_retries} 次失败(错误码 {error_code}):"
                              f"{e.args[1]},{wait:.1f}s 后重试...")
                        time.sleep(wait)
                        wait *= backoff  # 指数退避
                    else:
                        print(f"[RETRY] 不可重试错误或已达最大重试次数,放弃!错误:{e}")
                        raise
            raise last_exception
        return wrapper
    return decorator

@with_retry(max_retries=3, delay=0.5, backoff=2.0)
def fetch_user_count(host='localhost'):
    """查询用户总数,如连接失败会自动重试"""
    conn = pymysql.connect(
        host=host, user='root', password='your_password',
        database='py_middleware_demo', charset='utf8mb4'
    )
    with conn.cursor() as cur:
        cur.execute("SELECT COUNT(*) FROM users")
        count = cur.fetchone()[0]
    conn.close()
    return count

# 正常调用
try:
    count = fetch_user_count()
    print(f"[RETRY] 用户总数:{count}")
except Exception as e:
    print(f"[RETRY] 最终失败:{e}")

# 模拟连接失败(错误的 host)
try:
    count = fetch_user_count(host='192.168.99.99')  # 不可达的 IP
    print(f"[RETRY] 用户总数:{count}")
except Exception as e:
    print(f"[RETRY] 所有重试均失败,错误:{type(e).__name__}")

打印输出:

[RETRY] 用户总数:4

[RETRY] 第 1/3 次失败(错误码 2003):Can't connect to MySQL server on '192.168.99.99',0.5s 后重试...
[RETRY] 第 2/3 次失败(错误码 2003):Can't connect to MySQL server on '192.168.99.99',1.0s 后重试...
[RETRY] 不可重试错误或已达最大重试次数,放弃!错误:...
[RETRY] 所有重试均失败,错误:OperationalError

十三、实战案例:用户系统数据层

综合前面所有知识,构建一个完整的用户系统数据访问层(DAL):

import pymysql
import pymysql.cursors
from dbutils.pooled_db import PooledDB
from contextlib import contextmanager
from typing import Optional
from dataclasses import dataclass, field
from datetime import datetime

# ─────────────────────────────────────────
# 数据模型
# ─────────────────────────────────────────
@dataclass
class UserDTO:
    id:         Optional[int] = None
    username:   str = ''
    email:      str = ''
    age:        int = 0
    created_at: Optional[datetime] = None

    @classmethod
    def from_dict(cls, d: dict) -> 'UserDTO':
        return cls(
            id=d.get('id'),
            username=d.get('username', ''),
            email=d.get('email', ''),
            age=d.get('age', 0),
            created_at=d.get('created_at'),
        )

    def __str__(self):
        return (f"UserDTO(id={self.id}, username={self.username!r}, "
                f"email={self.email!r}, age={self.age})")

# ─────────────────────────────────────────
# 数据访问层
# ─────────────────────────────────────────
class UserRepository:
    """用户表的数据访问层,封装所有数据库操作"""

    def __init__(self, pool: PooledDB):
        self._pool = pool

    @contextmanager
    def _conn(self):
        conn = self._pool.connection()
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def create(self, dto: UserDTO) -> UserDTO:
        sql = "INSERT INTO users (username, email, age) VALUES (%s, %s, %s)"
        with self._conn() as conn:
            with conn.cursor(pymysql.cursors.DictCursor) as cur:
                cur.execute(sql, (dto.username, dto.email, dto.age))
                dto.id = cur.lastrowid
        print(f"[REPO] create -> {dto}")
        return dto

    def find_by_id(self, uid: int) -> Optional[UserDTO]:
        sql = "SELECT * FROM users WHERE id = %s"
        with self._conn() as conn:
            with conn.cursor(pymysql.cursors.DictCursor) as cur:
                cur.execute(sql, (uid,))
                row = cur.fetchone()
        result = UserDTO.from_dict(row) if row else None
        print(f"[REPO] find_by_id({uid}) -> {result}")
        return result

    def find_all(self, min_age: int = 0, page: int = 1, page_size: int = 10) -> list:
        offset = (page - 1) * page_size
        sql = """
            SELECT id, username, email, age, created_at
            FROM users WHERE age >= %s
            ORDER BY id LIMIT %s OFFSET %s
        """
        with self._conn() as conn:
            with conn.cursor(pymysql.cursors.DictCursor) as cur:
                cur.execute(sql, (min_age, page_size, offset))
                rows = cur.fetchall()
        results = [UserDTO.from_dict(r) for r in rows]
        print(f"[REPO] find_all(min_age={min_age}, page={page}) -> {len(results)} 条")
        for u in results:
            print(f"        {u}")
        return results

    def update(self, uid: int, **kwargs) -> int:
        allowed = {'email', 'age', 'username'}
        fields = {k: v for k, v in kwargs.items() if k in allowed}
        if not fields:
            return 0
        set_clause = ', '.join(f"{k}=%s" for k in fields)
        sql = f"UPDATE users SET {set_clause} WHERE id = %s"
        params = list(fields.values()) + [uid]
        with self._conn() as conn:
            with conn.cursor() as cur:
                affected = cur.execute(sql, params)
        print(f"[REPO] update(id={uid}, {fields}) -> 影响行数 {affected}")
        return affected

    def delete(self, uid: int) -> int:
        with self._conn() as conn:
            with conn.cursor() as cur:
                affected = cur.execute("DELETE FROM users WHERE id = %s", (uid,))
        print(f"[REPO] delete(id={uid}) -> 影响行数 {affected}")
        return affected

    def count(self) -> int:
        with self._conn() as conn:
            with conn.cursor() as cur:
                cur.execute("SELECT COUNT(*) FROM users")
                return cur.fetchone()[0]

# ─────────────────────────────────────────
# 应用层(Service)
# ─────────────────────────────────────────
class UserService:
    def __init__(self, repo: UserRepository):
        self._repo = repo

    def register(self, username: str, email: str, age: int) -> UserDTO:
        if age < 0 or age > 150:
            raise ValueError(f"无效的年龄:{age}")
        if '@' not in email:
            raise ValueError(f"无效的邮箱:{email}")
        dto = UserDTO(username=username, email=email, age=age)
        return self._repo.create(dto)

    def get_profile(self, uid: int) -> Optional[UserDTO]:
        return self._repo.find_by_id(uid)

    def update_email(self, uid: int, new_email: str) -> bool:
        if '@' not in new_email:
            raise ValueError(f"无效的邮箱:{new_email}")
        return self._repo.update(uid, email=new_email) > 0

# ─────────────────────────────────────────
# 入口
# ─────────────────────────────────────────
if __name__ == '__main__':
    _pool = PooledDB(
        creator=pymysql, maxconnections=5, mincached=1,
        host='localhost', user='root', password='your_password',
        database='py_middleware_demo', charset='utf8mb4',
    )
    repo = UserRepository(_pool)
    svc  = UserService(repo)

    print("=" * 55)
    print("=== 用户系统数据层演示 ===\n")

    # 注册
    u1 = svc.register('ivy',  'ivy@app.com',  24)
    u2 = svc.register('jack', 'jack@app.com', 38)

    # 查询
    svc.get_profile(u1.id)

    # 更新
    svc.update_email(u1.id, 'ivy_new@app.com')
    svc.get_profile(u1.id)

    # 列表
    repo.find_all(min_age=20, page=1, page_size=5)

    # 统计
    total = repo.count()
    print(f"\n[SERVICE] 当前用户总数:{total}")

    # 清理
    repo.delete(u1.id)
    repo.delete(u2.id)

打印输出:

=======================================================
=== 用户系统数据层演示 ===

[REPO] create -> UserDTO(id=8, username='ivy', email='ivy@app.com', age=24)
[REPO] create -> UserDTO(id=9, username='jack', email='jack@app.com', age=38)
[REPO] find_by_id(8) -> UserDTO(id=8, username='ivy', email='ivy@app.com', age=24)
[REPO] update(id=8, {'email': 'ivy_new@app.com'}) -> 影响行数 1
[REPO] find_by_id(8) -> UserDTO(id=8, username='ivy', email='ivy_new@app.com', age=24)
[REPO] find_all(min_age=20, page=1) -> 5 条
        UserDTO(id=1, username='alice', email='alice@example.com', age=28)
        UserDTO(id=2, username='bob', email='bob_new@example.com', age=30)
        UserDTO(id=3, username='carol', email='carol@example.com', age=25)
        UserDTO(id=8, username='ivy', email='ivy_new@app.com', age=24)
        UserDTO(id=9, username='jack', email='jack@app.com', age=38)

[SERVICE] 当前用户总数:6
[REPO] delete(id=8) -> 影响行数 1
[REPO] delete(id=9) -> 影响行数 1

十四、最佳实践与总结

14.1 安全性

原则说明
永远使用参数化查询禁止字符串拼接 SQL,使用 %s 占位符
最小权限原则应用程序账号只授予必要的表级别权限,禁止使用 root
不在代码中硬编码密码使用环境变量或配置文件管理敏感信息
敏感字段加密密码字段使用 bcrypt 等算法哈希存储

14.2 性能

原则说明
使用连接池避免频繁建立/断开连接
批量操作优先executemany 比循环单条快数十倍
合理使用索引对 WHERE、JOIN、ORDER BY 字段建索引
避免 SELECT *只查询需要的字段,减少网络传输
大结果集用流式查询使用 SSCursor 避免内存溢出

14.3 可靠性

原则说明
显式事务管理关闭 autocommit,手动控制提交/回滚
使用上下文管理器确保连接和游标被正确关闭
实现重试机制对网络抖动等临时错误自动重试
完善的异常处理区分可重试错误与不可重试错误

14.4 可维护性

原则说明
分层设计将 SQL 封装在 Repository 层,Service 层调用
使用 ORM大型项目使用 SQLAlchemy,降低 SQL 维护成本
数据库迁移使用 Alembic(SQLAlchemy 配套)管理 DDL 变更
统一日志记录慢查询、异常 SQL,便于排查问题

结语

本文从环境配置出发,系统介绍了 Python 操作 MySQL 的完整知识体系:

  • PyMySQL / mysql-connector-python 完成基础的 CRUD
  • 参数化查询 防御 SQL 注入
  • 事务与保存点 保证数据一致性
  • 连接池(DBUtils / pooling) 提升并发性能
  • 批量操作与流式查询 优化大数据量场景
  • 上下文管理器 简化资源管理
  • SQLAlchemy ORM 构建企业级数据访问层
  • 重试装饰器 应对网络不稳定场景
  • 最终通过 用户系统实战案例 将所有知识融会贯通