Django之ModelForm操作
一、ModelForm的使用
顾名思义,ModelForm就是将Model与Form进行绑定,Form有自动生成表单的作用,但是每一个forms字段需要自己手动填写,而Model就是数据库表包含了所有的数据字段。所以ModelForm有着以下功能:
- Form所有的功能
- 将Model字段自动转换成forms字段
(一)实例演示
1、创建ModelForm
from app01 import models from django.forms import ModelForm from django.forms.widgets import Textarea class BookModelForm(ModelForm): class Meta: model = models.Book #对应的Model类 fields = '__all__' #对应的Model类中字段 exclude = None #排除的字段 labels = { "title":"书籍名", #用于html页面中显示的名字 "price":"价格" } help_texts = { "title":"我是书籍的帮助信息" #自定义帮助信息 } error_messages = { "title":{"required":"书籍名不能为空"} #自定义错误信息 } widgets = { "title":Textarea(attrs={"class":"form-control"}) #自定义属性 }
2、添加数据
from django.shortcuts import render,redirect,HttpResponse def BookAdd(request): book_list = models.Book.objects.all() #获取添加数据的表单 if request.method == "GET": form = BookModelForm() return render(request,'booklist.html',locals()) #POST请求添加数据 form = BookModelForm(data=request.POST) if form.is_valid(): #保存数据 form.save() return HttpResponse('...')
3、修改数据
def BookEdit(request,id): book = models.Book.objects.filter(id=id).first() #获取修改数据的表单 if request.method == "GET": form = BookModelForm(instance=book) return render(request, 'booklist.html', locals()) #POST请求添加修改过后的数据 form = BookModelForm(data=request.POST,instance=book) #对数据验证并且保存 if form.is_valid(): form.save() return HttpResponse('...')
4、路由配置
urlpatterns = [ re_path('books/$', tests.BookAdd), re_path('books/(?P<id>\d+)/$', tests.BookEdit), ]
5、前端html渲染
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="POST" novalidate> {% csrf_token %} {% for book in form %} <div> {# 拿到数据字段的labels,没有就默认显示字段名 #} <label >{{ book.label }}</label> <div>{{ book }}{{ book.help_text }}</div> </div> {% endfor %} <div class="col-md-2 col-md-offset-10"> <input type="submit" value="提交" class="btn-primary"> </div> </form> </body> </html>
(二)实例解析
1、model字段转成forms字段
在创建ModelForm类时会将model字段转成forms字段,这里着重说明三种情况:
-
model字段是ForeignKey
如果model中是外键,那么在forms字段中对应的就是ModelChoiceField,如果使用的是Form,那么外键就应该这样定义:
publish=forms.ModelChoiceField(queryset=Publish.objects.all())
当然,在ModelForm中已经帮你自动实现了,将会产生这样的标签:
<select id="id_publish" name="publish"> <option value="obj1.pk">Object1</option> <option value="obj2.pk">Object2</option> ... </select>
-
model字段是ManyToMany
如果model中是ManyToMany,那么在forms字段中对应的就是ModelMultipleChoiceField,如果使用的是Form,那么ManyToMany就应该这样定义:
authors=forms.ModelMultipleChoiceField(queryset=Author.objects.all())
当然,在ModelForm中已经帮你自动实现了,将会产生这样的标签:
<select name="authors" id="id_authors" multiple="multiple" required=""> <option value="obj1.pk">obj1</option> <option value="obj2.pk">obj2</option> ... </select>
-
model字段中有choice参数
在model中可能会遇到这样的情况:
status_choices=( (1,'已签合同'), (2,'未签合同') ) status=models.IntegerField(choices=status_choices,verbose_name='状态',default=2)
这样的情况在forms中对应的字段是ChoiceField字段,如果使用Form自定义字段,可以这样写:
status=forms.ChoiceField(choices=((1,"已签合同"),(2,"未签合同")))
当然,在ModelForm中也已经帮你自动实现了。
-
总结
在查看ChoiceField、ModelChoiceField、ModelMultipleChoiceField源码可知它们三者关系:
ChoiceField(Field)
ModelChoiceField(ChoiceField)
ModelMultipleChoiceField(ModelChoiceField)
它们分别依次继承,所以最后一个有它们所有的属性和方法。
2、保存数据时使用save方法
- 添加数据
这比Form更为简单和直接,在forms中要么通过cleaned_data将数据依次取出分别保存,要么以字典的形式一次存入:
... obj = BookForm(request.POST) if obj.is_valid(): models.Book.objects.create(**obj.cleaned_data) ...
但是在ModelForm中,可以这样使用:
... obj = BookModelForm(request.POST) if obj.is_valid(): obj.save() ...
- 修改数据
在修改数据时Form和ModelForm也是略有不同的在Form中:
... obj = BookForm(request.POST) if obj.is_valid(): models.Book.objects.filter(id=nid).update(**obj.cleaned_data) ...
而在ModelForm中需要传入实例:
... obj = BookModelForm(data=request.POST,instance=book) if form.is_valid(): form.save() ...
三、源码一览
假设这里以修改视图流程来了解一下源码:
(一)ModelForm实例化
form = BookModelForm(instance=book)
ModelForm实例化并且传入instance参数,它会先使用元类生成自己,如果有__new__先执行__new__方法并且返回生成的对象,然后执行__init__方法初始化参数。在元类当中可以看到最后返回了当前BookModelForm,并且将收集了所有的已经声明的字段赋值给该类:
class ModelFormMetaclass(DeclarativeFieldsMetaclass): def __new__(mcs, name, bases, attrs): ... ... new_class.base_fields = fields ... ... return new_class
然后再执行BaseModelForm中的__init__方法进行初始化:
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=None, empty_permitted=False, instance=None, use_required_attribute=None):
...
...
object_data = {}
...
self.instance = instance
...
... #获取self.fields = copy.deepcopy(self.base_fields) super().__init__( data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted, use_required_attribute=use_required_attribute, ) ...
初始化过程中将instance接收进来,并且将self.base_fields进行深拷贝给self.fields。
(二)is_valid
这个对ModelForm进行校验就是和Form的一样:
def is_valid(self): """Return True if the form has no errors, or False otherwise.""" return self.is_bound and not self.errors
在self.errors方法中执行的self.full_clean方法:
def full_clean(self): """ Clean all of self.data and populate self._errors and self.cleaned_data. """ self._errors = ErrorDict() if not self.is_bound: # Stop further processing. return self.cleaned_data = {} # If the form is permitted to be empty, and none of the form data has # changed from the initial data, short circuit any validation. if self.empty_permitted and not self.has_changed(): return self._clean_fields()#对每一个字段进行执行clean_fieldname方法 self._clean_form() #返回cleaned_data self._post_clean() #预留钩子
这也就注定了Form中存在的功能ModelForm都有,无论是字段的验证还是其它的功能。
(三)save
save方法应该说比Form操作更方便快捷,可以简单的看看内部源码:
def save(self, commit=True): """ Save this form's self.instance object if commit=True. Otherwise, add a save_m2m() method to the form which can be called after the instance is saved manually at a later time. Return the model instance. """ if self.errors: raise ValueError( "The %s could not be %s because the data didn't validate." % ( self.instance._meta.object_name, 'created' if self.instance._state.adding else 'changed', ) ) if commit: # If committing, save the instance and the m2m data immediately. self.instance.save() self._save_m2m() else: # If not committing, add a method to the form to allow deferred # saving of m2m data. self.save_m2m = self._save_m2m return self.instance
save方法有一个默认参数commit=True,表示保存实例以及ManyToMany数据,self.instance.save(),调用的是model实例的save方法(位于django.db.models.Model):
def save(self, force_insert=False, force_update=False, using=None, update_fields=None): """ Save the current instance. Override this in a subclass if you want to control the saving process. The 'force_insert' and 'force_update' parameters can be used to insist that the "save" must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set. """ # Ensure that a model instance without a PK hasn't been assigned to # a ForeignKey or OneToOneField on this model. If the field is # nullable, allowing the save() would result in silent data loss. for field in self._meta.concrete_fields: # If the related field isn't cached, then an instance hasn't # been assigned and there's no need to worry about this check. if field.is_relation and field.is_cached(self): obj = getattr(self, field.name, None) # A pk may have been assigned manually to a model instance not # saved to the database (or auto-generated in a case like # UUIDField), but we allow the save to proceed and rely on the # database to raise an IntegrityError if applicable. If # constraints aren't supported by the database, there's the # unavoidable risk of data corruption. if obj and obj.pk is None: # Remove the object from a related instance cache. if not field.remote_field.multiple: field.remote_field.delete_cached_value(obj) raise ValueError( "save() prohibited to prevent data loss due to " "unsaved related object '%s'." % field.name ) using = using or router.db_for_write(self.__class__, instance=self) if force_insert and (force_update or update_fields): raise ValueError("Cannot force both insert and updating in model saving.") deferred_fields = self.get_deferred_fields() if update_fields is not None: # If update_fields is empty, skip the save. We do also check for # no-op saves later on for inheritance cases. This bailout is # still needed for skipping signal sending. if len(update_fields) == 0: return update_fields = frozenset(update_fields) field_names = set() for field in self._meta.fields: if not field.primary_key: field_names.add(field.name) if field.name != field.attname: field_names.add(field.attname) non_model_fields = update_fields.difference(field_names) if non_model_fields: raise ValueError("The following fields do not exist in this " "model or are m2m fields: %s" % ', '.join(non_model_fields)) # If saving to the same database, and this model is deferred, then # automatically do a "update_fields" save on the loaded fields. elif not force_insert and deferred_fields and using == self._state.db: field_names = set() for field in self._meta.concrete_fields: if not field.primary_key and not hasattr(field, 'through'): field_names.add(field.attname) loaded_fields = field_names.difference(deferred_fields) if loaded_fields: update_fields = frozenset(loaded_fields) self.save_base(using=using, force_insert=force_insert, force_update=force_update, update_fields=update_fields)
如果commit=False,就不会保存实例,当调用save方法后不会保存ManyToMany字段,需要自行去调用save_m2m方法,例如:
# Create a form instance with POST data. >>> f = BookForm(request.POST) # Create, but don't save the new book instance. >>> new_book = f.save(commit=False) # Modify the book in some way. >>> new_book.some_field = 'some_value' # Save the new instance. >>> new_book.save() # Now, save the many-to-many data for the form. >>> f.save_m2m()
参考文章:https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#modelform
四、扩展
(一)自定义BaseModelForm
class BaseRequestModelForm(object): def __init__(self, request, *args, **kwargs): self.request = request super(BaseRequestModelForm, self).__init__(*args, **kwargs)
这样,ModelForm中可以传入request参数,当然还可以添加其它参数,然后再继承自己的ModelForm,这样自定义的ModelForm不仅仅有自己的功能,还可以传参定制其它功能,在使用时继承下面这个ModelForm即可:
class BaseModelForm(BaseRequestModelForm,forms.ModelForm): def __init__(self,request,*args,**kwargs): super().__init__(request,*args,**kwargs) #####给modelform字段加样式 for name,field in self.fields.items(): attrs_dict={'class':'form-control'} if 'DateTimeField' in field.__repr__(): attrs_dict = {'class': 'form-control', 'date_time': 'datetimepicker', 'size': '16'} field.widget.attrs.update(attrs_dict)
(二)动态生成ModelForm
每一个model都可以对应一个ModelForm类可用于自动生成表单等功能,但是如果能够动态生成ModelForm岂不是更加省事,其实就是动态的生成一个类,并且设置类的一些属性,首先先看一个普通的ModelForm类,依照此类动态生成:
class BookModelForm(ModelForm): class Meta: model = models.Book #对应的Model类 fields = '__all__' #对应的Model类中字段 def __new__(cls,*args,**kwargs): """ :param cls: :param args: :param kwargs: :return: """ #base_fields = [{'field_name':field_obj},] forms字段对象 for field_name in cls.base_fields: field_obj = cls.base_fields[field_name] field_obj.widget.attrs.update({'class':'form-control'}) return ModelForm.__new__(cls)
然后就可以进行动态生成这样的ModelForm类了。
from django.forms import ModelForm def CreateDynamicModelForm(model,fields=None,form_create=False,*args,**kwargs): #默认为修改表单 attrs = {} #创建类使用的属性字典 #如果没有传入fields默认就是全部 if not fields: fields = "__all__" #传入request参数 if kwargs.get('request'): attrs["request"] = kwargs.get('request') class Meta: pass setattr(Meta,'model',model) setattr(Meta,'fields',fields) attrs["Meta"] = Meta #如果给每一个字段加入样式,重写__new__方法 def __new__(cls,*args,**kwargs): """ :param cls: :param args: :param kwargs: :return: """ #base_fields = [{'field_name':field_obj},] forms字段对象 for field_name in cls.base_fields: field_obj = cls.base_fields[field_name] field_obj.widget.attrs.update({'class':'form-control'}) return ModelForm.__new__(cls) attrs["__new__"] = __new__ ##创建类 name = 'DynamicModelForm' #创建类的名称 bases = (ModelForm,) #创建类的基类 dynamic_model_form = type(name,bases,attrs) return dynamic_model_form
以后就可以这样使用了,也不用那么麻烦的手动写ModelForm类了。
from django.shortcuts import render,redirect,HttpResponse from app01 import models #添加数据 def BookAdd(request): book_list = models.Book.objects.all() BookModelForm = CreateDynamicModelForm(models.Book, form_create=True, request=request) #获取添加数据的表单 if request.method == "GET": form = BookModelForm() return render(request,'booklist.html',locals()) #POST请求添加数据 form = BookModelForm(data=request.POST) if form.is_valid(): #保存数据 form.save() return HttpResponse('...') #修改数据 def BookEdit(request,id): #动态生成ModelForm类 BookModelForm = CreateDynamicModelForm(models.Book,request=request) book = models.Book.objects.filter(id=id).first() #获取修改数据的表单 if request.method == "GET": form = BookModelForm(instance=book) return render(request, 'booklist.html', locals()) #POST请求添加修改过后的数据 form = BookModelForm(data=request.POST,instance=book) #对数据验证并且保存 if form.is_valid(): form.save() return HttpResponse('...')