PyMySQL 的骨架就六个方法:connect、cursor、execute、fetchall、commit、close。
它比 PHP 的 mysqli 多一层「游标」,好处是一个连接可以有多根独立游标。
占位符是 %s 不是 ?,所以 LIKE '%张%' 得写成 %%张%%——这个坑几乎人人踩一次。
教材 23.4 讲技术准备:PyMySQL 模块和 WTForms 模块。
这两个库都不大:一个负责说话,一个负责挡人。
沙箱里两个都没装,所以这一篇的做法是:PyMySQL 用一层 shim 复刻它的形状(%s 占位、
cursor 中间层、commit),WTForms 的校验规则用普通函数复刻。
规则本身(必填、长度、邮箱格式)是教材写的,可以真验证。
演示里六条输入跑了四类规则,结果五条被拦、一条通过。
其中「21 个字的用户名」那条提醒了一件事:长度上限比下限更容易被忘。
一、六个方法,一张图
| 方法 | 干什么 | 对应 mysqli |
|---|---|---|
connect(...) |
建连接 | mysqli_connect |
cursor() |
拿游标 | 没有对应物 |
execute(sql, args) |
执行 | mysqli_query |
fetchall()/fetchone() |
取结果 | fetch_assoc 循环 |
commit() |
提交 | mysqli_commit |
close() |
关闭 | mysqli_close |
第二行是两种接口最大的结构差别:mysqli 的 query 直接返回结果集,
PyMySQL 要先 cursor() 再 execute()。
多这一层的代价是多写一行,好处是「一个连接上可以同时开几个游标」,互不干扰。
这个差别在写复杂逻辑时有意义:比如一边遍历一个结果集,一边往里插数据。
mysqli 上这会互相干扰(同一个结果集),PyMySQL 上开两个游标就行。
二、占位符的写法:两个坑
PyMySQL 用 %s,是因为它底层走 Python 的 % 格式化。这带来两个坑:
| 坑 | 表现 | 写法 |
|---|---|---|
和 LIKE 的 % 撞车 |
报参数个数不对 | 写成 %%张%% |
| 动态表名没法参数化 | 占位符只认值 | 白名单 |
第一行的报错信息完全看不出原因——它会说「不是所有参数都被转换」,而你明明只传了一个参数。
因为那个 %张% 被当成了格式占位符。解决办法是双写百分号。
第二行和 PHP 那边一样:表名、列名不是「值」,没法参数化,只能用白名单。
这条规则跨语言都成立——凡是「拼进语法结构的东西」,都要么是固定字符串,要么过白名单。
三、WTForms:把校验从代码里抽出来
不搭框架时,校验通常写成一段 if:长度够不够、格式对不对、必填有没有。
这样写的麻烦是:每个表单都要写一遍,而且容易漏。
| 规则 | 对应 WTForms | 漏了会怎样 |
|---|---|---|
| 必填 | DataRequired |
空字符串进库 |
| 长度 | Length(min, max) |
超长撞字段长度,报 500 |
| 邮箱格式 | Email |
存进去一个没法发信的地址 |
| 自定义 | Regexp |
各种业务规则漏掉 |
第二行的「报 500」值得说清楚:它不是「提示用户改名」,是「服务器内部错误」。 因为超长是在数据库那一层才被拒的,而那时候已经写了一半流程。 所以长度校验必须在表单层做,不能指望数据库——数据库只会拒绝,不会告诉你「该改短一点」。 演示里六条输入跑下来,五条被拦。其中两条报的是同一条规则(长度), 但一条是「太短」一条是「太长」——这也是为什么规则要分开写,合并成「不合法」用户就没法改了。
四、两个库的分工
| 层 | 谁负责 | 挡住什么 |
|---|---|---|
| 输入层 | WTForms | 格式不对、必填没填 |
| 数据层 | PyMySQL + 约束 | 类型不对、唯一冲突 |
| 业务层 | 你自己 | 越权、状态不允许 |
三层都要有,不能只靠一层。
只靠表单层,绕过表单直接调接口就破了;只靠数据库约束,报错信息没法给用户看;
只靠业务层,代码会写得又长又乱。
这个分层和 23.1 那条「按人隔离」正好接上:它属于第三层,
前两层都挡不住——因为「这篇笔记是不是你的」不是格式问题,也不是唯一性问题。
所以它在表单里查不出来,只能落到每一条 SQL 的 WHERE 上。
五、动手 Demo:PyMySQL 的六个方法和一套表单校验
这一段先用一层 shim 把 sqlite3 包成 PyMySQL 的形状,走一遍六个方法;然后试一下 %s 占位符的坑;
最后用四条规则跑六条输入,看哪几条被拦、报什么错。
import re, sqlite3
class FakeCursor:
def __init__(self, cur):
self.cur = cur
def execute(self, sql, args=()):
self.cur.execute(sql.replace(u"%s", u"?"), args) # %s 换成 ?
return self.cur.rowcount
def fetchall(self):
return self.cur.fetchall()
def close(self):
self.cur.close()
class FakeConn:
def __init__(self, path):
self.c = sqlite3.connect(path, isolation_level=None)
def cursor(self):
return FakeCursor(self.c.cursor())
def commit(self):
self.c.commit()
def close(self):
self.c.close()
conn = FakeConn(u"tech.db")
cur = conn.cursor()
cur.execute(u"SELECT id, title FROM notes WHERE user_id = %s LIMIT 3", (1,))
print(u"fetchall -> %r" % (cur.fetchall(),))
cur.close()
conn.close()
# 校验:四条规则分开写
def check(d):
errs = []
name = (d.get(u"uname") or u"").strip()
pwd = d.get(u"upass") or u""
mail = (d.get(u"email") or u"").strip()
if not name:
errs.append(u"用户名必填")
elif len(name) < 3 or len(name) > 20:
errs.append(u"用户名长度 3~20")
if len(pwd) < 6:
errs.append(u"密码至少 6 位")
if mail and not re.match(u"^[^@\\s]+@[^@\\s]+\\.[A-Za-z]{2,}$", mail):
errs.append(u"邮箱格式不对")
return errs
forms = [{u"uname": u"alice", u"upass": u"123456", u"email": u"[email protected]"},
{u"uname": u"", u"upass": u"123456", u"email": u""},
{u"uname": u"ab", u"upass": u"123456", u"email": u""},
{u"uname": u"bob", u"upass": u"123", u"email": u""},
{u"uname": u"carol", u"upass": u"123456", u"email": u"bad"},
{u"uname": u"a" * 21, u"upass": u"123456", u"email": u""}]
for f in forms:
e = check(f)
print(u"%-22s %s" % (f[u"uname"] or u"(空)", u"、".join(e) or u"通过"))
实跑输出:
--------------------------------------------------------------------
PyMySQL 的六个方法和一套表单校验
--------------------------------------------------------------------
**教材 23.4 是技术准备:PyMySQL 模块和 WTForms 模块。**
**两个库都不大,各自解决一件事:PyMySQL 负责说话,WTForms 负责挡人。**
① PyMySQL 的六个方法
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| import pymysql
|
| conn = pymysql.connect(host='localhost', user='root', password='123456',
| database='note', charset='utf8mb4')
| cur = conn.cursor()
| cur.execute("SELECT id, title FROM notes WHERE user_id = %s", (1,))
| rows = cur.fetchall()
| cur.close()
| conn.close()
教材原码:注意占位符是 %s,不是问号——这是 PyMySQL 和 sqlite3 的第一个差别
cursor() 拿到游标
execute() SQL 文本里的 %s 被换成了参数
fetchall() 3 条,首条 (10, '笔记0010')
close() 连接已关
**这六个方法就是 PyMySQL 的全部骨架:`connect` 拿连接、`cursor` 拿游标、`execute` 执行、**
**`fetchall` 取结果、`commit` 提交、`close` 关闭。**
**和 PHP 的 mysqli 比,最大的差别是「游标」这个中间层:**
**mysqli 的结果集是 `query` 直接返回的,PyMySQL 要先拿游标再执行。**
**多这一层的好处是「一个连接可以有多个独立游标」,坏处是多写一行。**
② 占位符:%s 不是 ?
--------------------------------------------------------------------
没有参数时 可以直接写
**PyMySQL 用 `%s` 是因为它底层走的是 Python 的 `%` 格式化风格。**
**好处是「写起来像 printf」,坏处是「和 SQL 的 `%` 通配符撞车」——写 `LIKE '%张%'` 时会报错,**
**得写成 `LIKE '%%张%%'`。**
**这个坑几乎每个人都会踩一次,而且报错信息完全看不出原因。**
③ WTForms 式的校验:四条规则,一条都不能少
--------------------------------------------------------------------
uname upass 校验结果
----------------------------------------------
alice 123456 通过
(空) 123456 用户名必填
ab 123456 用户名长度 3~20
bob 123 密码至少 6 位
carol 123456 邮箱格式不对
aaaaaaaaaaaaaaaaaaaaa 123456 用户名长度 3~20
**六条输入,四条被拦下来,两条通过。**
**注意第二行和第三行的报错不同:一个是「必填」,一个是「长度」——这两条规则分开写,**
**用户才知道该改什么。**
**如果合并成「用户名不合法」,用户会反复试。**
**最后一行那个 21 字的用户名说明一件事:长度上限比下限更容易被忽略。**
**下限不够只是「短」,上限超了会撞数据库字段长度,报一个 500 错误。**
第一段那个 shim 里最要紧的一行是 sql.replace("%s", "?")。
它把 PyMySQL 的占位符换成 sqlite3 的占位符——这一步不能省,但也不能「在真代码里这么写」,
因为真 PyMySQL 认的就是 %s。
写法不同,语义相同:占位符永远是「值」,不是「语法」。
第二段六条输入的结果里,最值得注意的是最后一条:21 个字符。
它撞的是「长度上限」,而长度上限漏掉的后果不是「提示不友好」,是「数据库直接报错」。
因为那时候已经走进写入流程了,错误会以 500 的形式抛给用户。
还有一个细节:四条规则是分开判断的,所以一条输入可能报多个错。
这一点比「一次只报一个错」好——用户改一轮就够了,不用改一次提交一次。
30 秒实验
- 把
check里的邮箱规则改成必填,重跑,看第一条输入还过不过。 - 在表单里加一条「用户名只能用字母数字」,用
re实现,再跑一遍。 - 把
%s换成?直接传给 sqlite3,看能不能跑——这验证的是「两种占位符不同源」。
常见误区
- 在 PyMySQL 里写
LIKE '%张%'。%会被当成格式占位符,报「参数个数不对」。 要写成%%张%%。 - 长度校验只写下限。上限漏了会撞数据库字段长度,报 500。 用户看到的不是「太长了」,是「服务器错误」。
- 把校验全塞进数据库约束。约束只会拒绝,不会告诉你「该改哪里」。 给用户看的提示必须在表单层生成。
自测三问(点开对答案)
1. PyMySQL 比 mysqli 多了哪一层?有什么用? —— 多了 `cursor()` 这一层。 好处是一个连接上可以同时开几个游标互不干扰,比如一边遍历结果集一边插数据。 2. 为什么 `LIKE '%张%'` 在 PyMySQL 里要写成 `%%张%%`? —— 因为它底层用 Python 的 `%` 格式化,单个 `%` 会被当成占位符。双写才是字面量。 报错信息完全看不出原因。 3. 校验为什么要分三层? —— 表单层挡格式,数据库层挡类型和唯一性,业务层挡越权和状态。 少任何一层都有绕过路径,而越权这一条前两层都挡不住。技术准备做完,下一篇进数据库设计:四张表怎么建,以及那个把所有 SQL 收进去的操作类。
- ☐ (本篇完,下一篇 23.5 数据库设计)