编辑页要先把旧值填进表单,一行 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 秒实验
- 把 UserForm(obj=u) 改成 UserForm(),再打开编辑页,看三个 input 的 value。
- 把删除路由的 methods 改成 GET,直接访问那个地址,看会不会真删掉。
- 想一想:编辑页保存时,如果邮箱没改,会不会撞上唯一约束?
常见误区
- 编辑页每次新建对象再 commit:变成插入新记录,行数越改越多。
- 删除挂 GET:被预加载或爬虫误触发,而且很难复现。
- 不用 get_or_404 直接取:拿 None 去取属性,报 AttributeError 而不是 404。
- 字段名和模型属性不一致:回填静默失败,页面全空,也不报错。
自测三问(点开对答案)
1. UserForm(obj=u) 靠什么把旧值填进去? —— 按字段名去对象上取同名属性,所以两边名字必须一致。 2. 404 和 405 分别是什么意思? —— 404 是地址没有对应路由,405 是路由在但没挂这个方法。 3. 为什么删除不能挂 GET? —— GET 会被图片标签、预加载、爬虫触发,用户没点也会删。增删改查齐了。下一篇收尾:把公共布局和配置抽出来,第 2 章就结束了。
- ☐ (本篇完,下一篇 2.8 模板通用化与配置管理)