Django如何在同一表单中创建Recipe父模型与RecipeIngredient子模型
问题解决方案
不需要预先生成空白Recipe实例,这种方案会产生大量无效脏数据,还要额外处理未提交数据的清理逻辑,成本很高,推荐使用Django原生功能实现。
标准实现方案:使用Django Inline Formset
Django内置的Inline Formset是官方专门为父子关联模型同表单提交的场景设计的功能,核心逻辑是先校验所有表单数据,校验通过后再保存父模型获取主键,最后用主键关联保存所有子模型数据,天然解决外键依赖问题。
具体实现步骤如下:
- 第一步:定义表单和表单集
在forms.py中声明父模型表单和子模型内联表单集:
from django.forms import ModelForm from django.forms.models import inlineformset_factory from .models import Recipe, RecipeIngredient # 父模型Recipe的表单 class RecipeForm(ModelForm): class Meta: model = Recipe fields = ['name', 'description', 'image'] # 子模型RecipeIngredient的内联表单集 RecipeIngredientFormSet = inlineformset_factory( Recipe, RecipeIngredient, fields=['name', 'description'], extra=3, # 默认展示3个空白食材输入行 can_delete=True # 允许用户删除多余的输入行 )
- 第二步:视图层处理提交逻辑
在views.py中处理表单提交的完整流程:
from django.shortcuts import render, redirect from .forms import RecipeForm, RecipeIngredientFormSet def recipe_create(request): if request.method == 'POST': recipe_form = RecipeForm(request.POST, request.FILES) ingredient_formset = RecipeIngredientFormSet(request.POST) # 同时校验父表单和子表单集所有数据 if recipe_form.is_valid() and ingredient_formset.is_valid(): # 先保存Recipe父模型,补充当前登录用户的外键值 recipe = recipe_form.save(commit=False) recipe.user = request.user recipe.save() # 绑定外键后保存所有食材数据 ingredients = ingredient_formset.save(commit=False) for ingredient in ingredients: ingredient.recipe = recipe ingredient.save() # 处理用户标记删除的食材数据 for del_obj in ingredient_formset.deleted_objects: del_obj.delete() # 保存完成后跳转到食谱详情页 return redirect('recipe_detail', pk=recipe.pk) else: recipe_form = RecipeForm() ingredient_formset = RecipeIngredientFormSet() return render(request, 'recipe_create.html', { 'recipe_form': recipe_form, 'ingredient_formset': ingredient_formset })
- 第三步:模板层渲染表单
在模板文件中渲染表单,注意必须渲染表单集的management_form字段,这是Django用来记录表单集行数、删除状态的内置字段,缺失会导致校验失败:
<form method="post" enctype="multipart/form-data"> {% csrf_token %} <!-- 渲染食谱基础信息表单 --> {{ recipe_form.as_p }} <!-- 渲染表单集管理字段,不可省略 --> {{ ingredient_formset.management_form }} <!-- 渲染所有食材输入表单 --> {% for form in ingredient_formset %} <div class="ingredient-item"> {{ form.as_p }} </div> {% endfor %} <button type="submit">保存食谱</button> </form>
自定义实现方案(前后端分离场景适用)
如果是前后端分离架构,不需要使用Django内置表单渲染,可以用数据库事务实现:
- 前端把Recipe基础信息和所有Ingredient的信息组装为统一的请求参数传给后端
- 后端开启数据库事务,先校验并保存Recipe实例获取主键,再循环创建所有Ingredient实例绑定该Recipe主键,全部操作成功后提交事务,任意一步报错则回滚事务,不会产生脏数据。
内容的提问来源于stack exchange,提问作者Nick
相关产品推荐
相关产品推荐

