🧪 第2章 建立数据库应用程序
📄 一句话概括

编辑页要先把旧值填进表单,一行 UserForm(obj=u) 就够,靠的是字段名对齐。 查不到就 404,用 db.get_or_404 一行顶掉「查、判空、abort」三行。 删除必须挂 POST:GET 能被一个 img 标签触发,也能被爬虫触发。

📝 本章和前面几章的关系

这一篇对应教材 2.5.4 与 2.5.5:编辑使用者、删除使用者。 接着上一篇的项目做,同一套目录,只加两个端点。

一、编辑页怎么把旧值填进去

u = db.get_or_404(User, uid)
form = UserForm(obj=u)
if form.validate_on_submit():
    u.name = form.name.data
    u.email = form.email.data or None
    db.session.commit()
    return redirect(url_for("main.users"))
return render_template("form.html", form=form)

实测 GET /users/1/edit 拿到的三个 input:name 的 value 是「张三」,email 的 value 是邮箱, submit 是「保存」。 没有 obj=u 这个参数,编辑页每次都是空的,用户得把整张表重填一遍。 原理:WTForms 按字段名去对象上取同名属性。所以字段名和模型属性必须一致。

这条对应关系是隐式的,填不上不报错。编辑页一片空白时,先查字段名。

二、提交之后改的是哪一行

更新不用新建对象,直接改取出来那个对象的属性,再 commit。 实测 POST 之后返回 302,Location 是 /users;库里 id=1 的姓名变成「张三改」。 对象是从库里查出来的,会话里有它的快照,commit 时 SQLAlchemy 自己算差异。

如果新建一个 User 再 commit,那是插入一条新记录,不是更新。这个错的表现是「改一次多一行」, 很好认。

三、查不到就 404

实测 GET /users/999/edit 返回 404。 db.get_or_404(User, uid) 等于「按主键取,取不到就 abort(404)」。 自己写 get、判 None、再 abort 也行,三行变一行,而且不会忘记写 abort。

忘记 abort 的后果是拿 None 去渲染模板,报的是 AttributeError,不是 404——用户看到的是 500, 你还得从头查。

四、删除为什么必须是 POST

实测 GET /users/1/delete 返回 405,行数没变。POST 之后返回 302,行数从 3 变成 2。 挂 POST 的理由不是「规范好看」。 GET 可以被一个 img 标签、一次浏览器预加载、一个爬虫触发,用户什么都没点就删了。 删除、转账、改密码这类操作,一律 POST 加 CSRF 令牌。

405 这个数字要认得出:方法不允许,说明路由在,只是没挂这个方法。和 404「路由不在」是两回事。

五、删之前先看有没有子记录

教材在 2.5.5 之后紧接着预告第 5 章要建两张表,那时候删除就没这么简单了。 一条用户下面挂着十条记录,删用户时那十条怎么办?数据库默认会拦,报的是外键约束错误。 所以要提前决定策略,不能等报错再想。这一步设计错了,改起来要动数据。

六、四种策略,没有标准答案

策略 做法
先删子表 手动 delete 关联对象,再删主表
级联删除 relationship 上配 cascade
置空外键 子表外键改成可空,删主表时置 NULL
禁止删除 有子记录就不让删,提示先处理

先删子表最直白,但代码里要写清顺序,漏一处就是半删状态。 级联删除最省事,风险也最大:一次误删带走一整片。 置空外键适合「历史记录要留着」的场景,把归属改成未知。

禁止删除最保守,适合有审计要求的系统。 这四条没有对错,取决于一件事:删掉之后那些数据还有没有意义。

七、动手 Demo:CRUD 之二:编辑与删除

这个 Demo 接着上一篇的项目加两个端点:一个编辑、一个删除。 第三节和第四节各有一个容易忽略的点:404 与 405 的区别、GET 删除为什么危险。

@bp.route("/users/<int:uid>/delete", methods=["POST"])
def user_delete(uid):
    u = db.get_or_404(User, uid)
    db.session.delete(u)
    db.session.commit()
    flash("已删除 %s" % u.name, "warning")
    return redirect(url_for("main.users"))

实跑输出:

--------------------------------------------------------------------
CRUD 之二:编辑与删除
--------------------------------------------------------------------

  ① 编辑页怎么把旧值填进表单
  --------------------------------------------------------------------
  GET /users/1/edit -> 200
  input name=name     value=张三             type=text
  input name=email    [email protected]  type=text
  input name=submit   value=保存             type=submit
  ! UserForm(obj=u) 一句话把对象的值灌进表单字段。
  没有它,编辑页每次都是空的,用户得把整张表重填一遍。

  ② 保存之后再查
  --------------------------------------------------------------------
  POST 编辑 -> 302,Location=/users
  库里 id=1 的姓名 -> 张三改

  ③ 不存在的 id
  --------------------------------------------------------------------
  GET /users/999/edit -> 404
  ! db.get_or_404 是「查不到就 404」的简写。
  自己写 get 再判 None 再 abort 也行,但三行变成一行,还不会忘。

  ④ 删除:为什么必须是 POST
  --------------------------------------------------------------------
  GET /users/1/delete -> 405
  行数还是 -> 3
  POST /users/1/delete -> 302
  删完后行数 -> 2
  ! 删除一定要挂 POST,而且表单里要有 CSRF 令牌。
  用 GET 的话,一个 <img src="/users/1/delete"> 就能把数据删掉。

  ⑤ 关联数据要先处理
  --------------------------------------------------------------------

| 策略 | 做法 |
|---|---|
| 先删子表 | 手动 delete 关联对象,再删主表 |
| 级联删除 | relationship 上配 cascade="all, delete-orphan" |
| 置空外键 | 子表外键改成 nullable,删主表时置 NULL |
| 禁止删除 | 有子记录就不让删,提示先处理 |

  第 5 章建两张表时会真的遇到这个问题,那时再回来看这张表。

第一节那三行 input 就是回填的证据:value 里是库里原来的值。 第四节那个 405 是「路由在、方法不对」,和 404 完全不同,排错时这两个数字别混。 最后一节那张表,等第 5 章建了两张表还会再回来看一次。

30 秒实验

  1. 把 UserForm(obj=u) 改成 UserForm(),再打开编辑页,看三个 input 的 value。
  2. 把删除路由的 methods 改成 GET,直接访问那个地址,看会不会真删掉。
  3. 想一想:编辑页保存时,如果邮箱没改,会不会撞上唯一约束?

常见误区

  1. 编辑页每次新建对象再 commit:变成插入新记录,行数越改越多。
  2. 删除挂 GET:被预加载或爬虫误触发,而且很难复现。
  3. 不用 get_or_404 直接取:拿 None 去取属性,报 AttributeError 而不是 404。
  4. 字段名和模型属性不一致:回填静默失败,页面全空,也不报错。
自测三问(点开对答案) 1. UserForm(obj=u) 靠什么把旧值填进去? —— 按字段名去对象上取同名属性,所以两边名字必须一致。 2. 404 和 405 分别是什么意思? —— 404 是地址没有对应路由,405 是路由在但没挂这个方法。 3. 为什么删除不能挂 GET? —— GET 会被图片标签、预加载、爬虫触发,用户没点也会删。

增删改查齐了。下一篇收尾:把公共布局和配置抽出来,第 2 章就结束了。

相关笔记