后台的技术难点很少,难在「不能写错」:它的每个动作都会成批影响前台数据。
下架不是删除,是改一个状态字段——因为历史订单还要引用这件商品。
批量更新不一定更快:文件库上写盘占主导,一条 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 秒实验
- 把
is_on的条件从「前台查询」里去掉,看下架的商品会不会出现在列表里。 - 给
tb_goods.cat_id加外键(指向tb_category),再删 7 号类别,看报什么错。 - 把批量更新的
ids从一千条改成一万条,看两个数的差距怎么变。
常见误区
- 用
DELETE下架商品。要么被外键拦住,要么把历史订单的明细变成悬空行。 下架就是is_on = 0。 - 改完不判影响行数。改到 0 行照样提示「成功」,用户以为生效了,刷新才发现没变。
- 删类别不查引用。没外键时会留下一批「分类不存在」的商品:列表里看得见, 按分类筛永远筛不出来。
自测三问(点开对答案)
1. 为什么下架不用删除? —— 商品被历史订单明细引用。真删会让旧订单指向不存在的行,关掉外键检查则变成静默的数据损坏。 改状态两边都满足:前台不显示,后台查得到。 2. 删一个不存在的类别,代码怎么知道没删掉? —— 看影响行数。`DELETE` 一条不存在的记录不报错,`rowcount` 是 0。不判就会提示「删除成功」。 3. 为什么「批量比逐条快」有时测不出来? —— 因为时间可能花在写盘上,而不是「往返次数」上。 文件库上两者都被 I/O 拖住,换到内存库才能看到纯开销的差别。商品管完了,下一篇管订单:多条件查询、时间范围、主表加明细怎么查才不慢, 以及发货这个只有一步却最容易改错的动作。
- ☐ (本篇完,下一篇 24.11 后台订单管理模块设计)