
本文介绍在django中通过一个通用id(如`mprep`实例)关联并批量保存多个`msform`子表单的完整实现方案,解决单次提交仅保存一条子记录的问题,并提供前端动态增行与后端正确处理的关键代码。
要实现“一次提交、多条子表单保存”,核心在于:使用 Django 的 modelformset_factory 替代单个 ModelForm,并确保主表(Mprep)实例在首次保存后获得有效主键(id),再将该 ID 正确赋值给每条子表单记录。原代码中 spe_form = MSForm(...) 仅创建单个表单,无法处理多行数据;且 prep.id = None 的写法不仅无效(prep 是主表对象,不应置空ID),还可能引发逻辑错误。
✅ 正确做法如下:
1. 后端:改用 modelformset_factory 处理多条子记录
from django.forms import modelformset_factory
def preps(request, prep=None):
# 获取或初始化主表对象
if prep:
info = Mprep.objects.get(id=prep)
else:
info = Mprep()
# 构建子表单集(支持0~n条记录)
MSFormSet = modelformset_factory(
model=MSprep, # 子模型
form=MSForm,
extra=1, # 默认显示1行空白
can_delete=True
)
if request.method == 'POST':
gen_form = MGForm(request.POST, instance=info)
# 使用 queryset 绑定已有子记录(若存在),否则为空集
spe_formset = MSFormSet(
request.POST,
queryset=MSprep.objects.filter(prep=info) if info.pk else MSprep.objects.none(),
prefix='spe'
)
if gen_form.is_valid() and spe_formset.is_valid():
# 先保存主表,确保 info 获得有效 id
info = gen_form.save() # 注意:必须赋值回 info,获取新生成的 pk
# 批量保存子表单,并关联 prep 字段
instances = spe_formset.save(commit=False)
for instance in instances:
instance.prep = info # 关联主表
# 批量入库
for obj in instances:
obj.save()
# 处理被标记删除的记录
for obj in spe_formset.deleted_objects:
obj.delete()
messages.success(request, '所有信息保存成功')
return redirect('preps_view', prep=info.id)
else:
messages.error(request, '表单填写有误,请检查')
else:
gen_form = MGForm(instance=info)
spe_formset = MSFormSet(
queryset=MSprep.objects.filter(prep=info) if info.pk else MSprep.objects.none(),
prefix='spe'
)
context = {'gen_form': gen_form, 'spe_formset': spe_formset}
return render(request, 'base.html', context)2. 前端模板:渲染 Formset 并支持动态增行(兼容 Django 表单集)
<form method="post">
{% csrf_token %}
<!-- 主表单 -->
<div class="form-row">
<div class="form-group col-4">{{ gen_form.project|as_crispy_field }}</div>
<div class="form-group col-4">{{ gen_form.cell|as_crispy_field }}</div>
<div class="form-group col-4">{{ gen_form.sample|as_crispy_field }}</div>
</div>
<!-- 子表单集 -->
<table class="table mt-4">
<thead>
<tr>
<th>名称</th>
<th>数值</th>
<th>单位</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for form in spe_formset %}
<tr>
<td>{{ form.name }}</td>
<td>{{ form.value }}</td>
<td>{{ form.unit }}</td>
<td>{{ form.DELETE }} 删除</td>
{{ form.id }}
</tr>
{% endfor %}
{{ spe_formset.management_form }} {# 必须包含 #}
</tbody>
</table>
<input type="submit" class="btn btn-primary mt-3" value="保存全部">
</form>
<!-- 动态添加新行(需配合 JS 初始化空行) -->
<script>
// 简化版:点击新增一行(基于 Django formset 的命名规则)
document.getElementById("add-row-button").addEventListener("click", function () {
const tbody = document.querySelector("table tbody");
const totalForms = parseInt(document.querySelector("[name='spe-TOTAL_FORMS']").value);
const newRow = document.createElement("tr");
newRow.innerHTML = `
<td><input type="text" name="spe-${totalForms}-name" class="form-control"></td>
<td><input type="number" name="spe-${totalForms}-value" class="form-control"></td>
<td><input type="text" name="spe-${totalForms}-unit" class="form-control"></td>
<td><input type="checkbox" name="spe-${totalForms}-DELETE"> 删除</td>
<input type="hidden" name="spe-${totalForms}-id" value="">
`;
tbody.appendChild(newRow);
// 更新 TOTAL_FORMS
document.querySelector("[name='spe-TOTAL_FORMS']").value = totalForms + 1;
});
</script>⚠️ 关键注意事项:
- spe_formset.management_form 必须渲染(通常隐藏),否则 Django 拒绝 POST;
- 主表 info = gen_form.save() 必须赋值,确保后续子表单能读取其 pk;
- 子表单 queryset 应基于 info.pk 动态过滤,避免未保存主表时查询异常;
- 若需严格校验(如不允许空行),可在 MSForm.clean() 中添加逻辑;
- 生产环境建议为动态行添加客户端验证(如禁用空提交)+ 后端二次校验。
通过以上重构,即可安全、稳定地实现「一个主ID,N条子记录」的一键批量保存,兼顾可扩展性与数据一致性。










