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

后台的技术难点很少,难在「不能写错」:它的每个动作都会成批影响前台数据。 下架不是删除,是改一个状态字段——因为历史订单还要引用这件商品。 批量更新不一定更快:文件库上写盘占主导,一条 IN 和一千条单独语句差不多。

📝 本章和前面几章的关系

教材 24.10 讲后台商品管理:概述、技术分析、商品管理与类别管理。 这一节代码量最大,但几乎全是增删改查的重复劳动。 所以这一篇不逐行讲功能,只挑四个「容易写错又不会报错」的点:动作前要查什么、 下架与删除的区别、删类别前查引用、批量改价的真相。 演示里下架一件商品看两张表的差异,删两个类别(一个有名下商品、一个不存在), 最后把同一批量更新分别跑在文件库和内存库上——结论不一样。

一、四件事,每件都先校验

动作 先查什么 不查会怎样
新增 名称非空、价格大于零、分类存在 出现零元商品或没有分类的商品
修改 这条记录还在不在 改到 0 行还提示「成功」
下架 有没有未完成的订单 用户买了,货却没了
删除 有没有历史订单引用 外键报错,或者历史记录断链

第三行是商城特有的:下架前要看「有没有人已经付了钱还没发货」。 不管就会出现「订单里有这件商品,但商品已下架」——发货时找不到货,只能退款。 第二行是通用坑:UPDATE 改到零行不会报错,rowcount 是 0,如果不判, 页面照样提示「修改成功」。 用户改了一个已经被别人删掉的商品,看到「成功」,刷新之后发现没变——这类 bug 最难被报上来。

二、下架不是删除

动作 表里总行数 前台能看到的
下架前 5000 5000
把 3 号下架 5000 4999

总行数没变,前台能看到的少了一条。 这就是「下架」的全部效果:一条 UPDATE,两个视图看到的世界不一样。 已下架的那条仍然在表里,所以历史订单还能查到「当时买的是这件商品」。 如果当初写的是 DELETE,历史订单的明细就会指向一个不存在的行——要么被外键拦住, 要么(关掉外键后)查出来是空白。

三、删类别:先查引用,再看影响行数

类别和商品是一对多。删类别之前必须数一遍「名下有商品吗」,否则会留下两种烂摊子:

情况 结果 后果
有外键 数据库报错 至少你知道出事了
没外键 删除成功 留下一批「分类不存在」的商品

第二行更糟:后台列表里这些商品的分类列是空的,前台按分类筛永远筛不出来,而商品本身还在、 还能通过搜索找到。 这种「数据还在但看不见」的状态,比直接报错难处理得多。 还有一个细节:删一个不存在的类别,DELETE 影响 0 行,但不会报错。 所以「删成功了吗」不能靠「没抛异常」判断,要看影响行数,跟修改那条是同一个道理。

四、批量改价:结论随载体变

库在哪儿 循环一千条 一条 IN 结论
文件库 约一百二十毫秒 约一百一十七毫秒 差别落在噪声里
内存库 约二点六毫秒 约一点二毫秒 IN 快一倍多

这是这一篇最值得看的一组数:同一个实验,换个载体,结论完全不同。 文件库上两条都在一百毫秒上下,因为时间花在写盘上,不在「来回几次」; 换到内存库,写盘没了,剩下的才是纯开销。 所以「批量比逐条快」这句话是有前提的:前提是「往返次数」本来就是这个操作的主要成本。 写盘一占主导,批量就白搭了。 这也是为什么「网上说要批量」和「我测了没差别」可以同时为真。

五、三层校验,各管一段

层 查什么 查不到时
页面 必填、数字、长度 提示后停在原页
Action 权限、状态、引用 返回 403 或错误页
数据库 唯一、外键、非空 抛异常,事务回滚

三层都要查,因为它们的防线不同:页面能绕过(抓包直接发请求), Action 会漏(新加一个入口忘了写),只有数据库那一层绕不过去。 但数据库那一层的报错是给程序员看的,不是给用户看的——所以要三层配合,不是三层选一层。

六、动手 Demo:后台商品管理:增删改都带条件

这一段灌五千件商品,先看下架前后两个视图的差异,再删两个类别(一个名下有商品、一个根本不存在), 最后把同一批量更新分别跑在文件库和内存库上。

import sqlite3, time

# 下架:一条 UPDATE,两个视图
before = c.execute(u"SELECT COUNT(*) FROM tb_goods").fetchone()[0]
c.execute(u"UPDATE tb_goods SET is_on = 0 WHERE id = 3")
on = c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE is_on = 1").fetchone()[0]
print(u"总行数 %d,前台能看到 %d" % (before, on))

# 删类别:先查引用,再看影响行数
def del_cat(cid):
    n = c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE cat_id = ?",
                   (cid,)).fetchone()[0]
    if n:
        return u"拒绝:这个类别下还有 %d 件商品" % n
    n2 = c.execute(u"DELETE FROM tb_category WHERE id = ?", (cid,)).rowcount
    return u"删除成功" if n2 == 1 else u"什么都没删(影响 0 行)"

print(del_cat(7))    # 名下有商品
print(del_cat(99))   # 这个类别不存在

# 批量改价:文件库 vs 内存库
ids = [r[0] for r in c.execute(u"SELECT id FROM tb_goods ORDER BY id LIMIT 1000")]
marks = u",".join(u"?" * len(ids))

def bench(conn, one_by_one):
    t = time.perf_counter()
    conn.execute(u"BEGIN")
    if one_by_one:
        for i in ids:
            conn.execute(u"UPDATE tb_goods SET price = price * 0.9 WHERE id = ?", (i,))
    else:
        conn.execute(u"UPDATE tb_goods SET price = price / 0.9 "
                     u"WHERE id IN (%s)" % marks, ids)
    conn.execute(u"COMMIT")
    return (time.perf_counter() - t) * 1000

print(u"文件库 循环 %.1f ms / IN %.1f ms" % (bench(c, True), bench(c, False)))

cm = sqlite3.connect(u":memory:", isolation_level=None)
cm.executescript(DDL)
cm.execute(u"BEGIN")
cm.executemany(u"INSERT INTO tb_goods (gname, cat_id, price, stock, is_on) "
               u"VALUES (?,?,?,?,?)",
               [(u"商品%05d" % i, i % 20 + 1, 10.0 + i % 900, i % 50, 1)
                for i in range(1, 5001)])
cm.execute(u"COMMIT")
print(u"内存库 循环 %.1f ms / IN %.1f ms" % (bench(cm, True), bench(cm, False)))

实跑输出:

--------------------------------------------------------------------
后台商品管理:增删改都带条件
--------------------------------------------------------------------

**教材 24.10 讲后台商品管理:概述、技术分析、商品管理与类别管理。**
**后台的代码量比前台大得多,但技术难点很少——它的难在「不能写错」。**

  ① 后台四件事,每件都要先校验
  --------------------------------------------------------------------
动作  先查什么                        不查会怎样
------------------------------------------------------------------
新增  名称非空、价格大于零、分类存在  出现零元商品或没有分类的商品
修改  这条记录还在不在                改到 0 行还提示「成功」
下架  有没有未完成的订单              用户买了,货却没了
删除  有没有历史订单引用              外键报错,或者历史记录断链
**第三行是商城特有的:下架前要看「有没有人已经付了钱还没发货」。**
**如果不管,就会出现「订单里有这件商品,但商品已下架」——发货时找不到货,客服只能退款。**

  ② 下架不是删除
  --------------------------------------------------------------------
动作         表里总行数  前台能看到的
-------------------------------------
下架前       5000        5000
把 3 号下架  5000        4999
**总行数没变,前台能看到的少了 1 条。**
**这就是「下架」的全部效果:一条 `UPDATE`,两个视图看到的世界不一样。**
**已下架 1 条。它们仍然在表里,所以历史订单还能查到「当时买的是这件商品」。**
**如果当初写的是 `DELETE`,历史订单的明细就会指向一个不存在的行——要么被外键拦住,**
**要么(关掉外键后)查出来是空白。**

  ③ 删类别前先查引用
  --------------------------------------------------------------------
删哪个类别       结果
------------------------------------------------
7 号(有商品)   拒绝:这个类别下还有 250 件商品
99 号(不存在)  什么都没删(影响 0 行)
**第一行被「先查引用」挡住了。**
**如果不查直接删,会出现两种结果:有外键就报错,**
**没外键就留下一批「分类不存在」的商品——它们在后台列表里显示空分类,**
**在前台按分类筛永远筛不出来。**
**第二行更值得注意:`DELETE` 一条不存在的记录,`rowcount` 是 0,但不会报错。**
**所以「删成功了吗」不能靠「没抛异常」判断,要看影响行数。**

  ④ 批量改价:一次和一千次
  --------------------------------------------------------------------
库在哪儿  循环逐条  一条 `IN`  谁快
---------------------------------------------
文件库    130.6 ms  125.7 ms   `IN`
内存库    2.7 ms    1.2 ms     `IN` 快 2.3 倍
**这是这一节最值得看的一组数:同一个实验,换个载体,结论完全不同。**
**文件库上两条都在一百毫秒上下,差别落在噪声里——因为时间花在写盘上,不在「来回几次」。**
**换到内存库,写盘没了,剩下来的就是纯开销:一条 `IN` 比一千条单独语句快一倍多。**
**所以「批量比逐条快」这句话是有前提的——前提是「往返次数」本来就是这个操作的主要成本。**
**写盘一占主导,批量就白搭了。**
**`IN` 还有两个边界:参数个数有上限(不该真的塞十万个),而且它只能做「同一个改动」。**
**要「每件商品改不同的值」,就得用 `CASE WHEN` 或者临时表。**

  ⑤ 后台的校验清单
  --------------------------------------------------------------------
层      查什么            查不到时
-------------------------------------------
页面    必填、数字、长度  提示后停在原页
Action  权限、状态、引用  返回 403 或错误页
数据库  唯一、外键、非空  抛异常,事务回滚
**三层都要查,因为它们的「防线」不同:页面能绕过(抓包直接发请求),**
**Action 会漏(新加一个入口忘了写),只有数据库那一层绕不过去。**
**但数据库那一层的报错是给程序员看的,不是给用户看的——所以要三层配合,不是三层选一层。**

下架那两行只差一个数:总行数不变,前台能看到的少一条。 这就是「删除」和「下架」的全部区别——不是写法不同,是「谁还能看见它」不同。 删类别那两行:第一行被「先查引用」挡住,第二行走完了整条路却什么都没删。 第二种情况如果代码只判「有没有抛异常」,就会以为删成功了。 批量那两行是本篇的重点:文件库上两条都在一百毫秒上下,内存库上一条 IN 快一倍多。 所以「批量更快」不是绝对的——它取决于你有没有被 I/O 拖住。 做这类优化之前,先量一遍;量完再决定,比照搬经验靠谱。

30 秒实验

  1. 把 is_on 的条件从「前台查询」里去掉,看下架的商品会不会出现在列表里。
  2. 给 tb_goods.cat_id 加外键(指向 tb_category),再删 7 号类别,看报什么错。
  3. 把批量更新的 ids 从一千条改成一万条,看两个数的差距怎么变。

常见误区

  1. 用 DELETE 下架商品。要么被外键拦住,要么把历史订单的明细变成悬空行。 下架就是 is_on = 0。
  2. 改完不判影响行数。改到 0 行照样提示「成功」,用户以为生效了,刷新才发现没变。
  3. 删类别不查引用。没外键时会留下一批「分类不存在」的商品:列表里看得见, 按分类筛永远筛不出来。
自测三问(点开对答案) 1. 为什么下架不用删除? —— 商品被历史订单明细引用。真删会让旧订单指向不存在的行,关掉外键检查则变成静默的数据损坏。 改状态两边都满足:前台不显示,后台查得到。 2. 删一个不存在的类别,代码怎么知道没删掉? —— 看影响行数。`DELETE` 一条不存在的记录不报错,`rowcount` 是 0。不判就会提示「删除成功」。 3. 为什么「批量比逐条快」有时测不出来? —— 因为时间可能花在写盘上,而不是「往返次数」上。 文件库上两者都被 I/O 拖住,换到内存库才能看到纯开销的差别。

商品管完了,下一篇管订单:多条件查询、时间范围、主表加明细怎么查才不慢, 以及发货这个只有一步却最容易改错的动作。

相关笔记