🛢️ 第24章 Struts2+Spring+Hibernate+MySQL实现网络商城
📄 一句话概括

订单查询为空有三种常见原因,页面上长得一模一样:少了百分号、条件不一致、参数是空串。 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 秒实验

  1. 把 >= '' 改成 >= '2026-05-01',看第三行会不会有结果。
  2. 把 guard() 改成「只判 session 是否为空」,看刚登录那一行会不会被误拦。
  3. 给自己的项目写一个「打印最终 SQL 和参数」的小函数,遇到「查不到」先用它。

常见误区

  1. 查询为空就去改 SQL 里的条件。先打印最终执行的 SQL 和参数。 三种原因长得一样,改错一处会引出新的问题。
  2. 参数为空也照拼条件。>= '' 什么都匹配不上,= '' 也一样。参数为空就应该跳过这个条件。
  3. 在每个 Action 里各写一遍登录判断。新加一个入口就会漏。放在拦截器里按路径配,漏不了。
自测三问(点开对答案) 1. 订单查出来是空的,第一步该做什么? —— 打印最终执行的 SQL 和参数。 三种常见原因(少了百分号、条件不一致、参数空串)在页面上完全一样,只有看到 SQL 才能分辨。 2. 为什么说 Session 超时的空指针不是「代码写错」? —— 取用户名那一行本身是对的,错在没检查用户还在不在。 正确做法是在拦截器里统一判断,让业务代码拿到的 session 一定有效。 3. 这一章八条规矩的共同点是什么? —— 违反之后都不会立刻报错。 它们表现为「数字不对」「偶尔出问题」「用户投诉」,所以只能靠结构(约束、事务、条件更新、 拦截器)来保证,不能靠人记得。

第 48 课完结。这一章从一个商城的主线讲到八条规矩,其实只讲了一件事: 把「必须做的事」变成「做不到就出事」——约束、事务、条件更新、拦截器,四个手段, 四种「默认安全」。 下一课见。

相关笔记