问题与目标
应用连接数据库时,字符串拼接 SQL、忘记提交或连接未关闭都会制造安全和稳定性问题。本篇用 MySQL Connector/Python 完成一次事务写入和查询。
完成标准:能从环境变量读取连接配置,使用参数化查询,成功时提交、失败时回滚,并始终关闭游标和连接。
核心概念
连接代表与数据库的一次会话,游标负责执行 SQL 和读取结果。Connector/Python 参数占位符使用 %s 或命名形式;参数值作为第二个参数传入,而不是自己加引号或用 f-string 拼接。
事务边界应对应一个完整业务动作。默认自动提交关闭时,写入后必须显式 commit();发生异常则 rollback()。连接信息属于部署配置,不应写死或提交到 Git。
数据库异常至少要区分连接失败、约束冲突和 SQL 编程错误。底层异常可以记录在应用日志中,对外则转换为稳定的业务错误,不能把数据库地址和 SQL 细节直接返回给客户端。
可运行实现
安装依赖并设置环境变量:
bash
python -m pip install mysql-connector-python
export DB_HOST=127.0.0.1
export DB_USER=app_user
export DB_PASSWORD='replace-me'
export DB_NAME=engineering_notes
保存为 mysql_demo.py:
python
import os
import mysql.connector
from mysql.connector import MySQLConnection
def connect() -> MySQLConnection:
return mysql.connector.connect(
host=os.environ.get("DB_HOST", "127.0.0.1"),
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
database=os.environ.get("DB_NAME", "engineering_notes"),
charset="utf8mb4",
)
def create_task(project_id: int, title: str) -> int:
connection = connect()
cursor = connection.cursor()
try:
cursor.execute(
"INSERT INTO tasks (project_id, title) VALUES (%s, %s)",
(project_id, title),
)
task_id = cursor.lastrowid
connection.commit()
return int(task_id)
except Exception:
connection.rollback()
raise
finally:
cursor.close()
connection.close()
def list_tasks(project_id: int) -> list[tuple[int, str, str]]:
connection = connect()
cursor = connection.cursor()
try:
cursor.execute(
"SELECT id, title, status FROM tasks WHERE project_id = %s ORDER BY id",
(project_id,),
)
return [(int(row[0]), str(row[1]), str(row[2])) for row in cursor.fetchall()]
finally:
cursor.close()
connection.close()
if __name__ == "__main__":
new_id = create_task(1, "verify parameterized query")
print("created:", new_id)
print(list_tasks(1))
单元素参数必须写成 (project_id,)。输入是项目 ID 和标题,输出是新任务 ID 与任务列表。
批量写入使用 executemany(),仍然只提交一次业务事务:
python
def create_many(project_id: int, titles: list[str]) -> int:
connection = connect()
cursor = connection.cursor()
try:
rows = [(project_id, title) for title in titles]
cursor.executemany(
"INSERT INTO tasks (project_id, title) VALUES (%s, %s)",
rows,
)
connection.commit()
return cursor.rowcount
except Exception:
connection.rollback()
raise
finally:
cursor.close()
connection.close()
结果需要字段名时可创建 connection.cursor(dictionary=True),减少依赖列顺序。长期运行的 API 通常使用有上限的连接池,借出连接后仍必须归还;连接池解决重复建连成本,不解决事务遗漏和连接泄漏。
常见问题与排查
- SQL 中写成
?占位符:不同驱动规则不同,Connector/Python 使用%s。 - 用
%或 f-string 拼 SQL:既有注入风险,也会破坏引号和类型处理。 - 插入后其他连接看不到:检查是否调用
commit(),以及是否处于预期数据库。 - 连接过多:确保所有分支关闭资源;服务应用还应使用有上限的连接池。
- 批量写入一部分成功、一部分失败:确认是否在同一事务中,失败时整体回滚,并记录可以安全重放的输入标识。
- 使用字典游标后字段不存在:检查 SQL 别名和驱动返回的键名,不要在业务层依赖
SELECT *。 - 捕获所有异常却不重新抛出:调用方会误以为写入成功。回滚后保留原异常。
- 日志打印完整连接配置:密码和令牌必须脱敏。
小结
可靠数据库调用包含四件事:配置外置、值参数化、事务明确、资源必定释放。它们比“SQL 能执行”更接近真实应用的完成标准。
许可协议:CC BY-NC 4.0
更新于 2 小时前
觉得文章有帮助?点个赞吧!
0 条评论


