1. 程式人生 > 其它 >解決django中ModelForm多表單組合的問題

解決django中ModelForm多表單組合的問題

django是python語言快速實現web服務的大殺器,其開發效率可以非常的高!但因為秉承了語言的靈活性,django框架又太靈活,以至於想實現任何功能都有種“條條大路通羅馬”的感覺。這麼多種選擇放在一起,如何分出高下?我想此時的場景下就兩個標準:

1、相同的功能用最少的程式碼實現(程式碼少BUG也會少);

2、相對最易於理解,從而易於維護和擴充套件。書歸正傳,web服務允許使用者輸入,基本上要靠表單。而django對錶單的支援力度非常大,我們用不著在瀏覽器端的html檔案裡寫大量

程式碼,再到web端去匹配form裡的id/name/value、驗證規則,再與持久層資料庫比較並做操作。我們需要完成的工作非常少,可以沒有相似的重複程式碼。有些複雜的場景,會要求一個表單的內容存放到多張表裡,本文將通過4個部分,闡述它的實現方法。

1、django基礎表單的功能

定義一個表單非常簡單,繼承類django.forms.Form即可,例如:

    class ProjectForm(forms.Form):
      name = forms.CharField(label='專案名稱', max_length=20)

這個表單類可以生成HTML形式的form,可以從request.POST中解析form到ProjectForm類例項。怎麼做到的呢?

看下django.forms.Form定義:

    class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
      "A collection of Fields, plus their associated data."
      # This is a separate class from BaseForm in order to abstract the way
      # self.fields is specified. This class (Form) is the one that does the
      # fancy metaclass stuff purely for the semantic sugar -- it allows one
      # to define a form using declarative syntax.
      # BaseForm itself has no way of designating self.fields.

註釋說得很清楚,Form這個類就是為了實現declarative
syntax的,也就是說,繼承了Form後,我們直觀的表達ProjectForm裡要有一個Field名叫name,不關心其語法實現,而通過Form多繼承中的DeclarativeFieldsMetaclass語法糖,將會把name弄到類例項的self.fields裡。

我們重點關注表單的BaseForm類,它實現了基本的邏輯。截選了一小段對接下來的陳述有意義的程式碼,做一個簡單的註釋。

    class BaseForm(object):
      def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
             initial=None, error_class=ErrorList, label_suffix=None,
             empty_permitted=False, field_order=None, use_required_attribute=None):
        #data引數用於接收request.POST字典,如果是GET方法就不傳
    	self.data = data or {}
    	#files用於接收request.FILES,也就是處理上傳檔案
    	self.files = files or {}
    	#本篇文章的重點在於多個表單整合到一個form中,此時為防止有同名的field,需要加prefix字首
        if prefix is not None:
          self.prefix = prefix
    	#GET顯示錶單時,如果要顯示初始值,請用initial引數
        self.initial = initial or {}
     
      #模板中顯示{{form}}時,預設是以<table></table>顯示的
      def __str__(self):
        return self.as_table()
     
      #如果模板中不想寫重複程式碼,只以固定的格式來顯示每一個field,那麼就用{% for field, val in form %}來遍歷處理吧
      def __iter__(self):
        for name in self.fields:
          yield self[name]
     
      #如果傳入了prefix引數,html中每個field的name和id裡都會加上prefix字首
      def add_prefix(self, field_name):
        return '%s-%s' % (self.prefix, field_name) if self.prefix else field_name
     
      #模板中以html格式顯示form就靠這個方法
      def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
        top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
        output, hidden_fields = [], []
     
      #除了預設的table方式顯示外,還可以<ul><li>或者<p>方式顯示
      def as_table(self):
        "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
      def as_ul(self):
        "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
      def as_p(self):
        "Returns this form rendered as HTML <p>s."
    

所以,基本表單的功能看BaseForm已經足夠了。

2、從模型建立表單

django對於MVC中的C與M間的對映是非常體貼的,集中體現中Model模型中(比如模型的許可權與使用者認證)。那麼,一個模型代表著RDS中的一張表,模型的例項代表著關係資料庫中的一行,而form如何與一行相對應呢?

定義一個模型引申出的表單非常簡單,例如:

    class ProjectForm(ModelForm):
      class Meta:
        model = Project
        fields = ['approvals','manager','name','fund_rource','content','range',]

在model中告訴django模型是誰,在fields中告訴django需要在表單中建立哪些欄位。django會有一個django.db.models.Field到django.forms.Field的轉換規則,此時會生成Form。我們看看ModelForm是什麼樣的:

    class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
      pass

類似Form類,ModelFormMetaclass就是語法糖,我們重點看BaseModelForm類:

    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):
        opts = self._meta
    	#相比較BaseForm,多了instance引數,它等價於Model模型的一個例項
        if instance is None:
          #不傳instance引數,則會新構造model物件
          self.instance = opts.model()
          object_data = {}
        else:
          self.instance = instance
          object_data = model_to_dict(instance, opts.fields, opts.exclude)
    	#此時傳遞了initial也一樣可以生效,同時還會設定到Model中
        if initial is not None:
          object_data.update(initial)
     
      def save(self, commit=True):
    	#預設commit是True,此時就會儲存Model例項到資料庫
        if commit:
          self.instance.save()
    	#同時儲存many-to-many欄位對應的關係表
          self._save_m2m()
        else:
    	#注意,本篇文章主要用到commit=False這個引數,它會返回Model例項,允許我們在修改instance後,在instance上再呼叫save方法
          self.save_m2m = self._save_m2m
        return self.instance
    

所以,對於ModelForm我們可以傳入instance引數初始化表單,可以呼叫save()方法直接將從html裡得到的表單資料持久化到資料庫中。而我們只需要幾十行程式碼就可以完成這麼多工作。

3、通用檢視

django.views.generic.ListView和django.views.generic.edit下的CreateView,
UpdateView,
DeleteView都是通用檢視。即,我們又可以通過它們,把很多重複的工作交給django完成,又可以少寫很多程式碼完成同樣的功能了。這裡僅以CreateView為例說明,因為它相對最複雜,接下來的多ModelForm的提交也是在CreateView上進行的。

通用檢視使用時,只需要承繼後,再設定model或者form_class即可。比如CreateView就會由django自動的把頁面上POST出的form資料解析到model生成的表單(或者form_calss指定的ModelForm型別表單),同時呼叫表單的save方法將資料新增到模型對應的資料庫表中。當然GET請求時會生成空form到頁面上。可以看到,除去定義model或者form類外,幾行程式碼就可以搞定這麼多事。我們看看CreateView的繼承關係:

簡單介紹下CreateView通用檢視中每個父類的作用。

View是所有檢視類的父類,根據方法名分發請求到具體的get或者post等方法,提供as_view方法。

TemplateResponseMixin提供render_to_response方法將響應通過context上下文在模板上渲染。

ContextMixin在context上下文中加入'view'元素,值為self例項。

ProcessFormView在GET請求上渲染表單,在POST請求上解析form到表單例項。注意,它會在post請求中判斷表單是否可用,is_valid為真時,會呼叫form_valid方法,因此,重寫form_valid方法是第4部分處理多model到一個form的關鍵。

FormMixin允許處理表單,可指定form_class為某個表單。

SingleObjectMixin生成context上下文,同時根據model模型名稱生成object並新增到上下文中的'object'元素。

ModelFormMixin提供在請求中處理modelform的方式。

SingleObjectTemplateResponseMixin幫助TemplateResponseMixin提供模板。

所以,在用CreateView、一個模型、一個模板實現新增一行記錄的功能時是多麼簡單,因為這些父類會自動生成object,渲染到模板,解析form表單,save到資料庫中。所以,從模型創建出的表單ModelForm,配合上通用檢視後,威力巨大!!

4、多個ModelForm在一個form裡提交

終於可以回到本文的主題了。CreateView預設是處理一個Model模型、一個ModelForm表單的,然而,很多時候為了解耦,會把一張表拆成多張表,通過id關聯在一起。在django的模型中就體現為ForeignKey、ManyToManyField或者OneToOneField。而在業務邏輯上,需要體現為一張表單,對應著資料庫裡的多張表。

例如,我們希望錄入合同,其中合同Model中還有地址Model和專案Model,而專案Model中又有地址Model,等等。

當然,我們有很多種實現的方案,但是,前面三部分說了那麼多,不是浪費口水的。我們已經有了通用檢視+ModelForm這樣的利器,難道還需要手動去寫Form表單?我們已經習慣了在Model裡定義好型別和有點註釋作用還能當label的verbose_name,還需要在forms.Form裡再來一遍?還需要在檢視中寫這麼通用的邏輯程式碼嗎?當然不用。

inlineformset_factory是一種方案,但它限制太多,而且有些晦澀,我個人感覺是不太好用的。

那麼,從第1部分我介紹的Form裡的prefix,以及第3部分裡類圖中的ProcessFormView允許重定義form_valid,以及第2部分中ModelForm的save方法的行為控制,解決方案已經一目瞭然了。

拿上面提到的例子來說,我們建立合同時,指明瞭專案,包括專案地址和合同簽訂地址,這涉及到三張表和四條記錄(地址表有兩條)。

我們三張表的模型如下:

    class PrimeContract(models.Model):
      address = models.ForeignKey(Address, related_name="prime_contract_address", verbose_name="address")
      project = models.ForeignKey(Project, related_name="prime_contract", verbose_name="project")
    class Project(models.Model):
      address = models.ForeignKey(Address, related_name="project_address", verbose_name="project address")
    class Address(models.Model):
      pass
    

接著,定義ModelForm表單,這非常簡單:

    class AddressForm(ModelForm):
      class Meta:
        model = Address
     fields = ...
    class ProjectForm(ModelForm):
      class Meta:
        model = Project
     fields = ...
    class PrimeContractForm(ModelForm):
      class Meta:
        model = PrimeContract
     fields = ...

再寫檢視,這裡要重寫2個方法:

    class PrimeContractAdd(CreateView):
      success_url = ...
      template_name = ...
      form_class = PrimeContractForm
      def get_context_data(self, **kwargs):
        context = super(PrimeContractAdd, self).get_context_data(**kwargs)
        #SingleObjectMixin父類只會處理PrimeContractForm表單,另外三條資料庫記錄對應的表單我們要自己處理了,此時prefix派上用場了,因為Field重名是百分百的事
        if self.request.method == 'POST':
          contractAddressForm = AddressForm(self.request.POST, prefix='contractAddressForm')
          projectAddressForm = AddressForm(self.request.POST, prefix='projectAddressForm')
          projectForm = ProjectForm(self.request.POST, prefix='projectForm')
        else:
          contractAddressForm = AddressForm(prefix='contractAddressForm')
          projectAddressForm = AddressForm(prefix='projectAddressForm')
          projectForm = ProjectForm(prefix='projectForm')
        #注意要把自己處理的表單放到context上下文中,供模板檔案使用
        context['contractAddressForm'] = contractAddressForm
        context['projectAddressForm'] = projectAddressForm
        context['projectForm'] = projectForm
        return context
      
    	#重寫form_valid,父類ProcessFormView會在PrimeContractForm表單is_valid方法返回True時呼叫該方法
      def form_valid(self, form):
    	  #首先我們要獲取到PrimeContractForm表單對應的模型,此時是不能save的,因為外來鍵project和address對應的資料庫記錄還沒有建立,所以commit傳為False
        contract = form.save(commit=False)
    		#獲取上面get_context_data方法中在POST裡得到的表單
        context = self.get_context_data()
    		#按照四條資料庫記錄的順序依次的建立(呼叫save方法)、主鍵賦到下一條記錄的外來鍵中、下一次記錄建立(save)
        projectAddress = context['projectAddressForm'].save()
    		#從專案表單中獲取到模型,先把地址的id賦到外來鍵上再儲存
        project = context['projectForm'].save(commit=False)
        project.address = projectAddress
        project.save()
        contractAddress = context['contractAddressForm'].save()
    		#將合同模型中的address和project都設定好後再儲存
        contract.address = contractAddress
        contract.project = project
        contract.save()
        
        return super(PrimeContractAdd, self).form_valid(form)
    

最後寫模板:

    #這三個表單我們手動處理過的
    {{ contractAddressForm }}
    {{ projectAddressForm }}
    {{ projectForm }}
    #這是FormMixin父類幫我們生成的
    {{ form }}

至此,我們可以只用幾十行程式碼就完成複雜的功能,程式碼邏輯也清晰可控。

從這篇文章裡也可以看得出,django實在是快速開發網站的必備神器!當然,快速不代表不能夠支撐大併發的應用,instagram這個很火的服務就是用django寫的。由於python和django過於靈活,都將要求django的開發者們唯有更資深才能寫出生產環境下的服務。