文章目录
优雅操作 MySQL:Python 封装高可用 MySQL 工具类详解
在 Python 后端开发中,MySQL 作为主流关系型数据库,其操作效率与安全性直接影响项目质量。原生 pymysql
库虽能实现数据库交互,但需手动处理连接建立/关闭、事务提交/回滚、SQL 注入防护等重复工作,不仅代码冗余,还易因疏忽引发资源泄漏或数据安全问题。
本文将基于 pymysql
封装一个 高可用、易扩展、安全的 MySQL 工具类 MySQLUtils
,从设计思路到实战应用,再到进阶优化,带你掌握 Python 操作 MySQL 的最佳实践。
一、背景:为什么需要封装 MySQL 工具类?
原生 pymysql
操作 MySQL 的痛点如下:
- 连接管理繁琐:每次操作需手动创建连接、获取游标,结束后需关闭游标和连接,若忘记关闭会导致连接泄漏;
- 事务处理易错:增删改操作需手动提交事务,异常时需回滚,否则易造成数据不一致;
- SQL 注入风险:直接拼接 SQL 字符串会引发注入攻击,需手动处理参数化查询;
- 结果处理不直观:默认返回元组格式的查询结果,需手动映射字段名,可读性差;
- 异常处理分散:每个数据库操作都需单独捕获异常,代码冗余度高。
基于以上痛点,封装 MySQLUtils
工具类可实现 “一次封装,多处复用”,统一处理连接、事务、安全与异常,大幅提升开发效率。
二、MySQLUtils 工具类核心设计与实现
2.1 依赖与前置准备
首先安装 pymysql
库(Python 3.6+ 兼容):
pip install pymysql
2.2 工具类完整代码
import pymysql
from pymysql.cursors import DictCursor
from typing import List, Dict, Tuple, Optional
import logging
# 配置日志(可选,用于记录SQL执行与异常)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler()]
)
logger = logging.getLogger("MySQLUtils")
class MySQLUtils:
def __init__(self, host: str, user: str, password: str, db: str, port: int = 3306, charset: str = "utf8mb4"):
"""
初始化数据库连接参数(仅存储参数,不立即建立连接)
:param host: 数据库主机地址
:param user: 数据库用户名
:param password: 数据库密码
:param db: 数据库名称
:param port: 数据库端口(默认3306)
:param charset: 字符编码(默认utf8mb4,支持emoji)
"""
self.host = host
self.user = user
self.password = password
self.db = db
self.port = port
self.charset = charset
# 连接与游标对象(初始化时为None,connect()方法中创建)
self.connection: Optional[pymysql.connections.Connection] = None
self.cursor: Optional[pymysql.cursors.DictCursor] = None
def __enter__(self):
"""
上下文管理器入口:自动建立数据库连接
使用with语句时,会自动调用此方法返回工具类实例
"""
self.connect()
logger.info("MySQL连接已建立")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""
上下文管理器出口:自动关闭连接,处理异常
:param exc_type: 异常类型(无异常则为None)
:param exc_val: 异常实例(无异常则为None)
:param exc_tb: 异常追踪信息(无异常则为None)
"""
self.close()
if exc_type:
logger.error(f"MySQL操作异常: {exc_val} (类型: {exc_type.__name__})")
else:
logger.info("MySQL连接已安全关闭")
def connect(self) -> None:
"""建立数据库连接并创建游标(DictCursor:查询结果返回字典)"""
try:
self.connection = pymysql.connect(
host=self.host,
user=self.user,
password=self.password,
db=self.db,
port=self.port,
charset=self.charset,
cursorclass=DictCursor, # 关键:结果以{字段名:值}格式返回
autocommit=False # 关闭自动提交,手动控制事务
)
self.cursor = self.connection.cursor()
except Exception as e:
# 抛出自定义连接异常,便于上层捕获处理
raise ConnectionError(f"数据库连接失败: {str(e)}") from e
def close(self) -> None:
"""关闭游标与连接(避免资源泄漏)"""
# 先关闭游标,再关闭连接(顺序不可颠倒)
if self.cursor and not self.cursor.closed:
self.cursor.close()
logger.debug("游标已关闭")
if self.connection and not self.connection._closed:
self.connection.close()
logger.debug("连接已关闭")
def execute(self, sql: str, params: Optional[Tuple] = None) -> int:
"""
执行增删改操作(INSERT/UPDATE/DELETE)
:param sql: SQL语句(使用%s作为参数占位符,不可用{}或%)
:param params: SQL参数(元组格式,如(1, "张三"),防SQL注入)
:return: 受影响的行数
"""
if not self.cursor:
raise RuntimeError("游标未初始化,请先建立数据库连接")
params = params or ()
try:
logger.info(f"执行SQL: {sql} | 参数: {params}")
rows_affected = self.cursor.execute(sql, params)
self.connection.commit() # 提交事务(增删改必须提交)
logger.info(f"SQL执行成功,受影响行数: {rows_affected}")
return rows_affected
except Exception as e:
self.connection.rollback() # 异常时回滚事务,避免数据不一致
raise RuntimeError(f"SQL执行失败: {str(e)} (SQL: {sql}, 参数: {params})") from e
def query_one(self, sql: str, params: Optional[Tuple] = None) -> Optional[Dict]:
"""
查询单条记录
:return: 字典({字段名:值}),无结果则返回None
"""
if not self.cursor:
raise RuntimeError("游标未初始化,请先建立数据库连接")
params = params or ()
try:
logger.info(f"查询SQL: {sql} | 参数: {params}")
self.cursor.execute(sql, params)
result = self.cursor.fetchone() # 获取单条结果
logger.info(f"单条查询结果: {result}")
return result
except Exception as e:
raise RuntimeError(f"查询失败: {str(e)} (SQL: {sql}, 参数: {params})") from e
def query_all(self, sql: str, params: Optional[Tuple] = None) -> List[Dict]:
"""
查询多条记录
:return: 字典列表([{字段名:值}, ...]),无结果则返回空列表
"""
if not self.cursor:
raise RuntimeError("游标未初始化,请先建立数据库连接")
params = params or ()
try:
logger.info(f"批量查询SQL: {sql} | 参数: {params}")
self.cursor.execute(sql, params)
results = self.cursor.fetchall() # 获取所有结果
logger.info(f"批量查询结果总数: {len(results)}")
return results
except Exception as e:
raise RuntimeError(f"批量查询失败: {str(e)} (SQL: {sql}, 参数: {params})") from e
三、实战:MySQLUtils 完整使用示例
以“学生信息管理”为例,演示从 建表→插入→查询→更新→删除 的全流程操作,覆盖工具类核心方法。
3.1 配置数据库连接
首先定义数据库配置(建议从配置文件或环境变量读取,避免硬编码):
# 数据库配置(替换为你的实际环境配置)
DB_CONFIG = {
"host": "localhost", # 数据库地址(本地为localhost,远程为IP)
"user": "root", # 数据库用户名
"password": "your_pwd", # 数据库密码(生产环境需复杂度足够)
"db": "test_db", # 数据库名称(需提前创建)
"port": 3306 # 默认MySQL端口
}
3.2 全流程操作示例
if __name__ == "__main__":
# 1. 使用上下文管理器(自动建立/关闭连接,无需手动调用connect()和close())
with MySQLUtils(**DB_CONFIG) as db:
# 2. 1 建表:创建students表(IF NOT EXISTS避免重复创建)
create_table_sql = """
CREATE TABLE IF NOT EXISTS students (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID(自增)',
name VARCHAR(50) NOT NULL COMMENT '学生姓名',
age INT COMMENT '学生年龄',
gender ENUM('male', 'female') COMMENT '学生性别',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表';
"""
db.execute(create_table_sql)
print(" 学生表创建成功(若不存在)")
# 2.2 插入数据:参数化查询(%s为占位符,防SQL注入)
# 单条插入
insert_single_sql = "INSERT INTO students (name, age, gender) VALUES (%s, %s, %s)"
db.execute(insert_single_sql, ("张三", 20, "male"))
# 批量插入(若需批量,可循环调用execute或扩展execute_many方法)
db.execute(insert_single_sql, ("李四", 22, "female"))
db.execute(insert_single_sql, ("王五", 19, "male"))
print(" 3条学生数据插入成功")
# 2.3 查询单条记录:根据姓名查学生
query_single_sql = "SELECT * FROM students WHERE name = %s LIMIT 1"
student_zhang = db.query_one(query_single_sql, ("张三",))
print("\n 单条查询结果(张三):")
print(student_zhang) # 输出:{'id': 1, 'name': '张三', 'age': 20, ...}
# 2.4 查询多条记录:查询所有男生
query_all_sql = "SELECT id, name, age FROM students WHERE gender = %s ORDER BY age ASC"
male_students = db.query_all(query_all_sql, ("male",))
print("\n 批量查询结果(所有男生):")
for student in male_students:
print(f"ID: {student['id']}, 姓名: {student['name']}, 年龄: {student['age']}")
# 2.5 更新数据:将张三的年龄改为21
update_sql = "UPDATE students SET age = %s WHERE name = %s"
db.execute(update_sql, (21, "张三"))
updated_student = db.query_one(query_single_sql, ("张三",))
print("\n 更新后(张三年龄):")
print(f"姓名: {updated_student['name']}, 原年龄: 20 → 新年龄: {updated_student['age']}")
# 2.6 删除数据:删除李四的记录
delete_sql = "DELETE FROM students WHERE name = %s"
db.execute(delete_sql, ("李四",))
remaining_students = db.query_all("SELECT name FROM students")
print("\n 删除李四后剩余学生:")
print([student["name"] for student in remaining_students]) # 输出:['张三', '王五']
四、进阶优化:让工具类更健壮高效
上述工具类已满足基础需求,针对生产环境,可进一步优化以下方向:
4.1 引入连接池:解决频繁连接开销
频繁创建/关闭数据库连接会消耗大量资源,尤其是高并发场景。可使用 DBUtils
库实现连接池,复用连接:
pip install DBUtils # 安装连接池依赖
修改 MySQLUtils
的 connect
方法,改用连接池:
from dbutils.pooled_db import PooledDB
class MySQLUtils:
# 类级别的连接池(全局复用,避免重复创建)
_pool = None
def __init__(self, **db_config):
self.db_config = db_config
# 初始化连接池(仅第一次实例化时创建)
if not MySQLUtils._pool:
MySQLUtils._pool = PooledDB(
creator=pymysql, # 数据库驱动
maxconnections=10, # 最大连接数
mincached=2, # 初始化时闲置的连接数
maxcached=5, # 最大闲置连接数
blocking=True, # 无连接时是否阻塞等待
cursorclass=DictCursor,
**db_config
)
def connect(self):
# 从连接池获取连接,而非新建连接
self.connection = self._pool.connection()
self.cursor = self.connection.cursor()
4.2 扩展批量操作方法
针对批量插入/更新场景,扩展 execute_many
方法,比循环调用 execute
效率更高:
def execute_many(self, sql: str, params_list: List[Tuple]) -> int:
"""
批量执行增删改操作
:param params_list: 参数列表,如[("张三",20), ("李四",22)]
:return: 总受影响行数
"""
if not self.cursor or not params_list:
raise RuntimeError("游标未初始化或参数列表为空")
try:
logger.info(f"批量执行SQL: {sql} | 参数总数: {len(params_list)}")
rows_affected = self.cursor.executemany(sql, params_list)
self.connection.commit()
logger.info(f"批量SQL执行成功,总受影响行数: {rows_affected}")
return rows_affected
except Exception as e:
self.connection.rollback()
raise RuntimeError(f"批量SQL执行失败: {str(e)} (SQL: {sql})") from e
使用示例(批量插入3条数据):
params_list = [("赵六", 23, "male"), ("孙七", 21, "female"), ("周八", 20, "male")]
db.execute_many(insert_single_sql, params_list)
4.3 配置文件管理:避免硬编码
将数据库配置写入 .env
文件,用 python-dotenv
加载,提高安全性和可维护性:
pip install python-dotenv
创建 .env
文件:
# .env 文件(添加到.gitignore,避免泄露)
MYSQL_HOST=localhost
MYSQL_USER=root
MYSQL_PASSWORD=your_pwd
MYSQL_DB=test_db
MYSQL_PORT=3306
加载配置:
from dotenv import load_dotenv
import os
load_dotenv() # 加载.env文件
DB_CONFIG = {
"host": os.getenv("MYSQL_HOST"),
"user": os.getenv("MYSQL_USER"),
"password": os.getenv("MYSQL_PASSWORD"),
"db": os.getenv("MYSQL_DB"),
"port": int(os.getenv("MYSQL_PORT", 3306)) # 默认3306
}
五、总结:封装的价值与适用场景
5.1 工具类核心优势
- 简化开发:上下文管理器自动管理连接,减少70%重复代码;
- 安全可靠:参数化查询防SQL注入,事务回滚防数据不一致;
- 易用直观:查询结果返回字典,无需手动映射字段名;
- 可扩展性强:支持连接池、批量操作、日志记录等进阶需求。
5.2 适用场景
- 中小型 Python 后端项目(如 Flask/Django 项目的数据库层);
- 数据脚本(如数据迁移、定时任务中的 MySQL 操作);
- 对性能有一定要求,但无需复杂 ORM 功能的场景(大型项目可结合 SQLAlchemy)。
通过封装 MySQLUtils
,我们不仅实现了“代码复用”,更建立了 Python 操作 MySQL 的“规范”——统一的异常处理、日志记录、安全防护,让团队协作更高效,项目维护更轻松。掌握这种“封装思维”,对后续其他数据库(如 PostgreSQL、MongoDB)的操作也具有借鉴意义。