订单查询为空有三种常见原因,页面上长得一模一样:少了百分号、条件不一致、参数是空串。 Session 超时后的空指针不是「代码写错了」,是「没检查用户还在不在」。 拦截器的价值不是少写判断,是让业务代码根本碰不到空值。
教材 24.12 讲开发技巧与难点:解决订单查询为空、用拦截器解决 Session 超时空指针。 这一节是整本书最后一节技术内容,讲的两个坑都很「小」,但都很典型。 它们的共同点是:报错信息指不到真正的原因。 查询为空不报错,空指针指向的那一行代码其实是对的。 演示里把三种「查不到」并排放出来,让它们都返回 0; 再把 Session 超时的两种处理方式对照跑一遍。 最后用两张表收尾:一张是这一章的规矩清单,一张是三个项目最该带走的一句话。
一、查询为空:三个原因,一个症状
| 写错的哪一处 | 查出来几行 | 怎么改 |
|---|---|---|
| 关键字没加百分号 | 零 | 用 LIKE '%关键字%' |
| 总数和数据用了两套条件 | 零 | 共用同一份 where 和参数 |
| 时间参数传了空串 | 零 | 参数为空就不拼这个条件 |
| 条件写对 | 九十九 | — |
四行里有三行返回 0,但原因完全不同,修法也不同。
>= '' 在字符串比较里几乎什么都匹配不上——因为空串比任何日期都小。
三种错误在页面上长得一模一样:一个空列表,加一句「共 0 条」。
所以定位的办法不是盯着页面看,是把【最终执行的 SQL 和参数】打出来。
打印一次,三秒钟就能分辨:是少了百分号、条件不一致、还是参数是空串。
这个习惯值得一直带着:任何「查出来不对」的问题,第一步都是把 SQL 和参数打出来,
而不是猜哪里写错了。
二、Session 超时:空指针从哪来
| Session 状态 | 不拦截 | 先过拦截器 |
|---|---|---|
| 刚登录 | 张三 | 张三 |
| 超时后 | KeyError/NullPointerException |
跳到登录页 |
第二行第一列那个异常,指向的是业务代码里「取用户名」的那一行——看起来像那一行写错了。 实际上代码没错,错在「没人检查用户还在不在」。 Session 过期后里面是空的,取任何键都会炸。 第二行第三列是拦截器挡下来的结果:直接跳登录页,业务代码一行都没跑。 这就是拦截器的真正价值——它不是「帮你少写几行判断」,而是「让业务代码根本碰不到空值」。 两者的区别在「漏写」的时候显现:少写判断会炸,少配拦截器不会漏,因为它是按路径配的。
三、八条规矩,一个共同点
| 环节 | 一句话规矩 | 违反了会怎样 |
|---|---|---|
| 表设计 | 历史记录要存快照 | 对账对不上 |
| 约束 | 唯一、外键、非空交给数据库 | 并发下代码守不住 |
| 下单 | 多表改动必须包事务 | 半成品,不报错 |
| 库存 | 判断写进 WHERE,看影响行数 |
超卖 |
| 查询 | 条件与参数同增同减 | 总数和列表对不上 |
| 排序 | 参数走白名单 | 注入,且不报错 |
| 后台 | 删除改成改状态 | 历史断链 |
| 登录 | 权限检查放拦截器 | 空指针、越权 |
八行的共同点:违反之后都不会立刻报错。 它们要么表现为「数字不对」,要么表现为「偶尔出问题」,要么表现为「用户投诉了才发现」。 这也是数据库这边的工作和写页面最大的区别:页面错了看得见,数据错了看不见。 所以这一章的每一条规矩,本质都是同一件事——把「靠人记得」改成「靠结构保证」。
四、三个项目,一条线
| 项目 | 技术栈 | 最该带走的一条 |
|---|---|---|
| 22 章 物流网 | PHP | 多表统计先想清「按什么分组」 |
| 23 章 在线笔记 | Python + Flask | 每条查询都要带归属条件 |
| 24 章 网络商城 | Java SSH | 多表改动包事务,状态写进 WHERE |
三个项目换了三种语言、三套框架,但数据这边的事一模一样:表怎么连、条件怎么写、 什么时候要事务、什么时候要约束。 框架会换,这些不会。 第 48 课到这里结束。从第 1 章「数据库是什么」到这一章的「商城怎么不超卖」,中间隔了 119 篇。 如果只记一句话,记这句:凡是「错了也不会报错」的地方,都值得多花十分钟。
五、动手 Demo:两个经典坑:查不出数据、Session 一过期就空指针
这一段造两百张订单,先把三种「查不到」并排放出来(都返回 0),再用两种方式处理 Session 超时: 直接取用户名,和先过拦截器。
import sqlite3
for ddl in (u"CREATE TABLE tb_order (id INTEGER PRIMARY KEY,",
u" order_no TEXT UNIQUE, state TEXT, created_at TEXT)",):
c.execute(ddl)
c.executemany(u"INSERT INTO tb_order VALUES (?,?,?,?)",
[(i, u"NO%04d" % i, u"待发货", u"2026-05-%02d" % (i % 28 + 1))
for i in range(1, 201)])
# 三种「查不到」
print(c.execute(u"SELECT COUNT(*) FROM tb_order WHERE order_no = 'NO00'").fetchone())
print(c.execute(u"SELECT COUNT(*) FROM tb_order WHERE state = '已发货'").fetchone())
print(c.execute(u"SELECT COUNT(*) FROM tb_order "
u"WHERE created_at >= '' AND created_at < ''").fetchone())
print(c.execute(u"SELECT COUNT(*) FROM tb_order "
u"WHERE order_no LIKE '%NO00%'").fetchone())
# Session 超时:两种处理
def action_bad(session):
return session[u"user"][u"name"]
def guard(session):
return bool(session.get(u"user"))
def action_ok(session):
if not guard(session):
return u"跳到登录页"
return session[u"user"][u"name"]
for label, s in ((u"刚登录", {u"user": {u"name": u"张三"}}), (u"超时后", {})):
try:
bad = action_bad(s)
except Exception as e:
bad = u"%s: %s" % (type(e).__name__, e)
print(u"%s -> 不拦截 %s / 拦截 %s" % (label, bad, action_ok(s)))
实跑输出:
--------------------------------------------------------------------
两个经典坑:查不出数据、Session 一过期就空指针
--------------------------------------------------------------------
**教材 24.12 讲开发技巧与难点:解决订单查询为空、用拦截器解决 Session 超时空指针。**
**两个坑都不难,但它们有个共同点——报错信息完全指不到真正的原因。**
① 订单查询为空:三个原因,一个症状
--------------------------------------------------------------------
写错的哪一处 查出来几行
----------------------------------
关键字没加百分号 0
总数和数据用了两套条件 0
时间参数传了空串 0
对照:条件写对 99
**四行里有三行返回 0,但原因完全不同,修法也不同:**
**第一行该用 `LIKE` 加百分号;第二行是「总数用了一套条件、数据用了另一套」;**
**第三行是参数没传值,`>= ''` 在字符串比较里几乎什么都匹配不上。**
**三种错误在页面上长得一模一样:一个空列表,加一句「共 0 条」。**
**所以定位的办法不是看页面,是【把最终执行的 SQL 和参数打出来】。**
**打印一次,三秒钟就能看出来是少了百分号、条件不一致、还是参数是空串。**
② Session 超时:空指针从哪来
--------------------------------------------------------------------
Session 状态 不拦截 先过拦截器
------------------------------------------
刚登录 张三 张三
超时后 KeyError: 'user' 跳到登录页
**第二行第一列是 `KeyError`(Java 里是 `NullPointerException`)——它指向的是业务代码那一行,**
**看起来像「取用户名的代码写错了」。**
**实际上代码没错,错在「没检查用户还在不在」。**
**第二行第三列是拦截器挡下来的结果:直接跳登录页,业务代码一行都没跑。**
**这就是拦截器的价值——它不是「少写几行判断」,而是「让业务代码根本碰不到空值」。**
③ 收尾清单:这一章真正要带走的东西
--------------------------------------------------------------------
环节 一句话规矩 违反了会怎样
----------------------------------------------------------
表设计 历史记录要存快照 对账对不上
约束 唯一、外键、非空都交给数据库 并发下全靠代码守不住
下单 多表改动必须包事务 半成品,不报错
库存 判断写进 `WHERE`,看影响行数 超卖
查询 条件与参数同增同减 总数和列表对不上
排序 参数走白名单 注入,且不报错
后台 删除改成改状态 历史断链
登录 权限检查放拦截器 空指针、越权
**八行的共同点:违反之后都不会立刻报错。**
**它们要么表现为「数字不对」,要么表现为「偶尔出问题」,要么表现为「用户投诉了才发现」。**
**这也是数据库这边的工作和写页面最大的区别——页面错了看得见,数据错了看不见。**
④ 三个项目,一条线
--------------------------------------------------------------------
项目 技术栈 最该带走的一条
----------------------------------------------------------------
22 章 物流网 PHP 多表统计先想清楚「按什么分组」
23 章 在线笔记 Python + Flask 每条查询都要带归属条件
24 章 网络商城 Java SSH 多表改动包事务,状态写进 `WHERE`
**三个项目换了三种语言、三套框架,但数据这边的事一模一样:表怎么连、条件怎么写、**
**什么时候要事务、什么时候要约束。**
**框架会换,这些不会。**
**第 48 课到这里结束。**
**从第 1 章「数据库是什么」到这一章的「商城怎么不超卖」,中间隔了 119 篇——如果只记一句话,**
**记这句:凡是「错了也不会报错」的地方,都值得多花十分钟。**
前四条打印里,前三条都是 0,第四条是 99。
三条 0 的原因完全不同:一条是「等值匹配太严」,一条是「条件写错了状态」,
一条是「范围参数是空串」。
created_at >= '' 这一条最隐蔽:代码看起来没错,参数也确实传了(只是传了空串),
结果一条都匹配不上。
所以「参数为空就不拼这个条件」不是优化,是必须写的一步。
Session 那两行:不拦截的那一列,
超时后直接抛 KeyError(Java 里是 NullPointerException);拦截那一列干净地跳到登录页。
差别不在「有没有写对」,在「有没有一个地方统一挡在前面」。
30 秒实验
- 把
>= ''改成>= '2026-05-01',看第三行会不会有结果。 - 把
guard()改成「只判 session 是否为空」,看刚登录那一行会不会被误拦。 - 给自己的项目写一个「打印最终 SQL 和参数」的小函数,遇到「查不到」先用它。
常见误区
- 查询为空就去改 SQL 里的条件。先打印最终执行的 SQL 和参数。 三种原因长得一样,改错一处会引出新的问题。
- 参数为空也照拼条件。
>= ''什么都匹配不上,= ''也一样。参数为空就应该跳过这个条件。 - 在每个 Action 里各写一遍登录判断。新加一个入口就会漏。放在拦截器里按路径配,漏不了。
自测三问(点开对答案)
1. 订单查出来是空的,第一步该做什么? —— 打印最终执行的 SQL 和参数。 三种常见原因(少了百分号、条件不一致、参数空串)在页面上完全一样,只有看到 SQL 才能分辨。 2. 为什么说 Session 超时的空指针不是「代码写错」? —— 取用户名那一行本身是对的,错在没检查用户还在不在。 正确做法是在拦截器里统一判断,让业务代码拿到的 session 一定有效。 3. 这一章八条规矩的共同点是什么? —— 违反之后都不会立刻报错。 它们表现为「数字不对」「偶尔出问题」「用户投诉」,所以只能靠结构(约束、事务、条件更新、 拦截器)来保证,不能靠人记得。第 48 课完结。这一章从一个商城的主线讲到八条规矩,其实只讲了一件事: 把「必须做的事」变成「做不到就出事」——约束、事务、条件更新、拦截器,四个手段, 四种「默认安全」。 下一课见。
- ☐ (全书完)