bigywater.tech
Django · 65

ModelForm.save(commit=False) 的真实行为

commit=False 让你在保存前补几个服务端字段,但它有一个经典的坑:多对多。这篇看它到底做了什么、没做什么,并复现那个不会报错的数据丢失。

0 次查询
save(commit=False) 不写库,pk 为 None
静默丢失
commit=False 后忘了 save_m2m(),多对多不写、也不报错
save_m2m
是 save(commit=False) 时挂到表单对象上的方法

commit=True

实测 · 本机执行输出
   INSERT INTO "lab_post" ("title", "owner") VALUES ('t1', '') RETURNING "lab_post"."id"
   BEGIN
   SELECT "lab_tag"."id" AS "id" FROM "lab_tag" INNER JOIN "lab_post_tags" ON ("lab_tag"."id" = "lab_po
   INSERT OR IGNORE INTO "lab_post_tags" ("post_id", "tag_id") VALUES (1, 1), (1, 2)
   COMMIT
  tags = ['a', 'b']

普通路径:form.save() 一步到位。

commit=False

实测 · 本机执行输出
save(commit=False) 发出的查询数: 0  p.pk = None  库里 Post 数量: 0
p.save():
   INSERT INTO "lab_post" ("title", "owner") VALUES ('t2', 'me') RETURNING "lab_post"."id"
  此时 tags = []  ← 多对多还没写
f.save_m2m():
   BEGIN
   SELECT "lab_tag"."id" AS "id" FROM "lab_tag" INNER JOIN "lab_post_tags" ON ("lab_tag"."id" = "lab_po
   INSERT OR IGNORE INTO "lab_post_tags" ("post_id", "tag_id") VALUES (2, 1), (2, 2)
   COMMIT
  之后 tags = ['a', 'b']
  form 上有 save_m2m 属性是因为 save(commit=False) 把它挂上去了: True

返回的是一个还没有主键的对象,所以多对多关系此刻不可能写。Django 把「稍后补写多对多」的动作打包成 form.save_m2m。

忘了 save_m2m

实测 · 本机执行输出
忘了 save_m2m():库里这个 Post 的 tags = [] (用户选的 c 丢了,没有任何报错)
没调用 save(commit=False) 就用 save_m2m: AttributeError: 'PF' object has no attribute 'save_m2m'

这个 bug 只在使用多对多的表单上出现,而且不会抛异常。代码审查时看到 commit=False 就要找 save_m2m()。

标准写法

实测 · 本机执行输出
owner = request.user  tags = ['a']

如果只是想补一个字段,还有另一种写法:重写 form.save() 或在 form.instance 上先设置字段再 form.save()(本文没有测)。

分步演示:commit=False

commit=False
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite
    • 没有验证:PostgreSQL 上多对多写入的 SQL、commit=False 与 inline formset 的组合、instance 在 form.instance 上预先设置字段的写法
    京ICP备2026067502号-1京公网安备11010502063487号