1. 程式人生 > >Python自動化開發學習19-Django

Python自動化開發學習19-Django

python django

接下來,我們把Django分為視圖(View)、路由系統(URL)、ORM(Model)、模板(Templates )這4塊進行學習。

視圖

提交數據

上節課已經用過 request.POST.get() 獲取提交的數據了,現在來看看有多選框的情況,多選的話應該要提交多個數據。先寫一個有單選、多選、下拉列表的html:

<body>
<form action="/choice/" method="post">
    <p>
        性別:
        <input type="radio" name="gender" id="man" value="male" />
            <label for="man">男性</label>
        <input type="radio" name="gender" id="female" value="female" />
            <label for="female">女性</label>
    </p>
    <p>
        愛好:
        <input type="checkbox" id="football" name="favor" value="football" />
            <label for="football">足球</label>
        <input type="checkbox" id="basketball" name="favor" value="basketball" />
            <label for="basketball">籃球</label>
        <input type="checkbox" id="volleyball" name="favor" value="volleyball" />
            <label for="volleyball">排球</label>
        <input type="checkbox" id="baseball" name="favor" value="baseball" />
            <label for="baseball">棒球</label>
    </p>
    <p>
        <label for="city">城市:</label>
        <select name="city" id="city">
            <option value="BJ">北京</option>
            <option value="SH">上海</option>
            <option value="GJ">廣州</option>
            <option value="SZ">深圳</option>
        </select>
    </p>
    <p>
        <label for="skill">技能:</label>
        <select name="skill" id="skill" multiple="multiple">
            <option value="python">Python</option>
            <option value="html">HTML</option>
            <option value="css">CSS</option>
            <option value="js">JavaScript</option>
        </select>
    </p>
    <p>
        上傳:<input type="file" name="file" />
    </p>
    <input type="submit" value="提交" />
</form>
</body>

然後寫一個處理函數,用get方法獲取一下提交的值:

def choice(request):
    if request.method == ‘GET‘:
        return render(request, ‘choice.html‘)
    elif request.method == ‘POST‘:
        gender = request.POST.get(‘gender‘)
        favor = request.POST.get(‘favor‘)
        city = request.POST.get(‘city‘)
        skill = request.POST.get(‘skill‘)
        file = request.POST.get(‘file‘)
        print(gender, favor, city, skill)
        print(file, type(file))
        return render(request, ‘choice.html‘)
    # 處理POST和GET,還有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect(‘/admin/‘)

所有的值都能獲取到,但是對於多選的值也只能獲取到一個,對於這種情況,我們需要用到另一個方法 request.POST.getlist() ,把上面的代碼都替換成新的方法:

def choice(request):
    if request.method == ‘GET‘:
        return render(request, ‘choice.html‘)
    elif request.method == ‘POST‘:
        gender = request.POST.getlist(‘gender‘)
        favor = request.POST.getlist(‘favor‘)
        city = request.POST.getlist(‘city‘)
        skill = request.POST.getlist(‘skill‘)
        file = request.POST.getlist(‘file‘)
        print(gender, favor, city, skill)
        print(file, type(file))
        return render(request, ‘choice.html‘)
    # 除了POST和GET,客戶端還可能有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect(‘/admin/‘)

使用 getlist() 方法,返回的是一個列表,多個值的情況也能獲取完整。當然,單選的話還是繼續使用 get() 方法方便。
例子中還有個上傳文件的input,這裏只能獲取到文件名,下面接著講。

上傳文件

普通的form接收不了文件,需要在form標簽中要定義 enctype="multipart/form-data" 。然後把 type="file" 的input標簽放在這個form裏。所以得為上傳文件單獨寫一個form。另外POST裏並沒有文件內容,文件內容再FILES裏:

<form action="/upload/" method="post" enctype="multipart/form-data">
    <p>
        上傳:<input type="file" name="file" />
    </p>
    <input type="submit" value="提交" />
</form>

然後再處理函數裏先嘗試獲取一下接收到的內容:

def upload(request):
    if request.method == ‘GET‘:
        return render(request, ‘upload.html‘)
    elif request.method == ‘POST‘:
        obj = request.FILES.get(‘file‘)
        print(obj, type(obj), obj.name)
        return render(request, ‘upload.html‘)
    # 處理POST和GET,還有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect(‘/admin/‘)

上面打印了3個變量,obj打印出來是個文件名,但是實際是個class。打印type(obj)可以看到它的類型。obj.name才是真正的文件名。現在要把文件保存到本地,先建一個專門的文件夾upload,準備存放接收到的文件。要接收文件的內容,我們需要讀取 obj.chunks() ,所以要接收文件上傳參考下面的方法:

def upload(request):
    if request.method == ‘GET‘:
        return render(request, ‘upload.html‘)
    elif request.method == ‘POST‘:
        # 獲取文件對象
        obj = request.FILES.get(‘file‘)
        # 本地創建一個文件用來接收上傳的文件內容
        with open(‘%s/%s‘ % (‘upload‘, obj.name), ‘wb‘) as file:
            # 循環接收文件的內容,寫入到本地的文件中去
            for data in obj.chunks():
                file.write(data)
        return render(request, ‘upload.html‘)
    # 處理POST和GET,還有其他的提交方法,比如:PUT、DELETE、HEAD、OPTION
    else:
        return redirect(‘/admin/‘)

CBV 和 FBV

到目前為止,所有的處理都是寫在一個函數裏的。Django還提供另外一個方式,我們也可以通過類來處理。

  • FBV(function base views) 就是在視圖裏使用函數處理請求。
  • CBV(class base views) 就是在視圖裏使用類處理請求。

創建處理請求的類
現在使用CBV把上面提交數據裏的choice方法重新寫一下:

from django.views import View
# CBV 的類需要繼承上面的View
class Choice(View):

    def get(self, request):
        """GET請求提交到這裏"""
        print(‘get‘)
        return render(request, ‘choice.html‘)

    def post(self, request):
        """POST請求提交到這裏"""
        gender = request.POST.getlist(‘gender‘)
        favor = request.POST.getlist(‘favor‘)
        city = request.POST.getlist(‘city‘)
        skill = request.POST.getlist(‘skill‘)
        file = request.POST.getlist(‘file‘)
        print(gender, favor, city, skill)
        print(file, type(file))
        return render(request, ‘choice.html‘)

創建對應關系
urls.py 裏的對應關系也要修改一下,中間寫上類名,後面固定跟一個 .as_view() ,就是執行這個類的as_view()方法,具體如下:

from cmdb import views
urlpatterns = [
    # path(‘choice/‘, views.choice),
    path(‘choice/‘, views.Choice.as_view()),
]

各種類型的提交方法
先去看看繼承的View類裏有什麽,在源碼的base.py這個文件裏。首先裏面定義了一個公有屬性:

http_method_names = [‘get‘, ‘post‘, ‘put‘, ‘patch‘, ‘delete‘, ‘head‘, ‘options‘, ‘trace‘]

所以處理post和get,還可以處理這麽多的請求方法,用一起來也很簡單,在類裏照著別的一樣定義一個同名方法就可以了。
處理執行前後自定義操作
繼續看源碼的View。這裏可以跳過只看結論,調用了as_view()方法裏面會再調用一個dispatch()方法。這個dispatch()方法裏是通過映射獲取我們的 request.method 即提交的方法來調用我們的處理方法的。dispatch()的源碼如下:

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn‘t exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn‘t on the approved list.
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)

結論就是,根據不同的請求類型提交到不同的處理方法,是用過dispatch()方法裏通過映射來實現的。先執行dispatch()方法然後再調用對應的提交類型的處理方法。所以通過繼承和重構dispatch()方法,可以在處理方法執行前和執行後自定義一些操作。如果需要的話就在我們的類裏繼承並重構,參考這裏:

from django.views import View
# CBV 的類需要繼承上面的View
class Choice(View):

    def dispatch(self, request, *args, **kwargs):
        print(‘before‘)  # 處理前執行的操作
        # 完全執行父類的這個方法
        obj = super(Choice, self).dispatch(request, *args, **kwargs)
        print(‘after‘)  # 處理後執行的操作
        # 這裏一定要把處理結果返回
        return obj

所有提交類型都不匹配的情況
還是上面的dispatch()方法,在最後return之前,也就是提交的類型沒有匹配的處理方法的時候,默認調用執行 http_method_not_allowed() 方法,返回一個405頁面。如果需要按照之前FBV中的else那樣匹配其余所有類型的提交方法的話,那就在我們的類裏重構這個方法,把之前FBV中if裏面最後的else的邏輯補上:

    def http_method_not_allowed(self, request, *args, **kwargs):
        return redirect(‘/admin/‘)

路由系統,URL

模板言語循環字典

模板語言不屬於路由系統,由於後面的例子會用到,先講一點。
先看一下模板語言如何處理字典的,在 views.py 裏添加一個字典,然後在頁面裏返回:

DICT = {
    ‘k1‘: ‘value1‘,
    ‘k2‘: ‘value2‘,
    ‘k3‘: ‘value3‘,
}

def dict(request):
    return render(request, ‘dict.html‘, {‘dict‘: DICT})

下面的html裏演示了用法:

<body>
<p>返回整個字典</p>
{{ dict }}
<p>返回的是key</p>
<ul>
    {% for i in dict %}
        <li>{{ i }}</li>
    {% endfor %}
</ul>
<p>返回的是key</p>
<ul>
    {% for key in dict.keys %}
        <li>{{ key }}</li>
    {% endfor %}
</ul>
<p>返回的是value</p>
<ul>
    {% for value in dict.values %}
        <li>{{ value }}</li>
    {% endfor %}
</ul>
<p>返回key和value</p>
<ul>
    {% for k,v in dict.items %}
        <li>{{ k }}: {{ v }}</li>
    {% endfor %}
</ul>
</body>

循環字典,和python裏是差不多的,就是後面沒有括號():

  • 直接dict :循環的是key,不明確所以不推薦
  • dict.keys :循環key
  • dict.values :循環values
  • dict.items :循環key和values

一條對應關系對應多個頁面

現在我們已經可以用模板語言處理字典了,先來一個有點數據的字典:

USER_DICT = {
    ‘1‘: {‘name‘: ‘Adam‘, ‘age‘: 22, ‘dept‘: ‘IT‘},
    ‘2‘: {‘name‘: ‘Bob‘, ‘age‘: 32, ‘dept‘: ‘IT‘},
    ‘3‘: {‘name‘: ‘Carmen‘, ‘age‘: 30, ‘dept‘: ‘Sales‘},
    ‘4‘: {‘name‘: ‘David‘, ‘age‘: 40, ‘dept‘: ‘HR‘},
    ‘5‘: {‘name‘: ‘Edda‘, ‘age‘: 26, ‘dept‘: ‘HR‘},
}

def users(request):
    return render(request, ‘users.html‘, {‘user_dict‘: USER_DICT})

上面的處理函數只是把內存的數據變的復雜了一點。另外這裏的key用的是數字,我們可以把它當做是數據庫獲取到的數據的自增id。

基於get方法的實現

接下來重新寫一個簡單的html,頁面裏只顯示字典的name的值,其他的值都不顯示出來。換做提供一個a標簽,可以通過點擊a標簽打開一個顯示詳細內容的頁面:

<ul>
    {% for k,v in user_dict.items %}
        <li><a target=‘_blank‘ href=‘/detail/?nid={{ k }}‘>{{ v.name }}</a></li>
    {% endfor %}
</ul>

去urls.py裏添加完對應關系後,就可以打開這個頁面。上面a標簽裏的連接指向的是一個detail的頁面,並且提交的同時也提交一個nid值用於detail頁面查找並顯示出詳細的內容。
顯示詳細的處理函數:

def detail(request):
    # 用get方法獲取到nid
    nid = request.GET.get(‘nid‘)
    # 通過nid獲取到詳細數據,最後給return返回
    detail_info = USER_DICT[nid]
    return render(request, ‘detail.html‘, {‘detail_info‘: detail_info})

還要寫一個detail.html 的頁面。上面處理函數已經通過get請求的nid去獲取到具體得詳細數據並返回了,這裏直接把數據顯示出來:

<body>
<h1>詳細信息</h1>
<h3>用戶名:{{ detail_info.name }}</h3>
<h3>年齡:{{ detail_info.age }}</h3>
<h3>部門:{{ detail_info.dept }}</h3>
</body>

上面的方法是在users頁面以get形式提交到detail頁面,然後detail頁面裏分析get的請求內容,獲取到對應的詳細信息,在頁面裏顯示出來。

基於正則表達式的url來實現

還有另外一種實現方式。下面說的效果一樣,但是這種方式更好。不傳入參數,而是不同的urel ‘/detail-1/‘ 這樣,這個就需要用到正則表達式。先把urls.py裏的對應關系改成正則的形式:

from django.urls import path, re_path
from cmdb import views
urlpatterns = [
    # path(‘detail/‘, views.detail),
    re_path(‘^detail-(\d+).html‘, views.detail),
]

因為這裏要匹配正則了,之前的path不再適用,這裏要導入re_path來匹配正則。url的正則表達式都以^開頭,從頭開始匹配
users.html顯示不用改,但是要修改一個a標簽裏的內容,現在url後面不需要用get方式提交任何數據,但是請求的url本事是會變化的:

<ul>
    {% for k,v in user_dict.items %}
        <!--
        <li><a target=‘_blank‘ href=‘/detail/?nid={{ k }}‘>{{ v.name }}</a></li>
        -->
        <li><a target=‘_blank‘ href=‘/detail-{{ k }}.html‘>{{ v.name }}</a></li>
    {% endfor %}
</ul>

最後是顯示詳細信息的頁面,detail.html不需要任何變動,只要views.py裏處理函數的return不變就好,但是獲取數據的方式變了:

def detail(request, nid):
    # print(nid)
    detail_info = USER_DICT[nid]
    return render(request, ‘detail.html‘, {‘detail_info‘: detail_info})

這裏的處理函數多傳入了一下參數nid。名字不重要,但是這個值是正確分組匹配的結果。正則是這個 ‘detail-(\d+).html‘ ,裏面括號中的 \d+ 的內容就傳給了後面的第一個參數。也可以傳多個參數(用多個括號),但是數量要一致(處理函數開頭的形式參數),否則打開的頁面會報錯。
為什麽這種更好:路由關系是一個動態的關系,一對多,一類url對應一個函數或類。

捕獲參數

捕獲組就是把正則表達式中子表達式匹配的內容,保存到內存中以數字編號或顯式命名的組裏,方便後面引用。當然,這種引用既可以是在正則表達式內部,也可以是在正則表達式外部。捕獲組有兩種形式:

  • 普通捕獲組:(Expression)
  • 命名捕獲組:(?P&lt;name&gt;Expression)這個是python中的語法,其他語言了有的有,但是可能有點小差別,比如沒有這個P,比如不用尖括號換成引號
    前面的就是普通捕獲組的例子。如果你的正則有多個子表達式,比如:‘detail-(\d+)-(\d+).html’ 。那麽定義函數的時候必須註意參數的位置(名字為所謂)。這裏可以使用命名捕獲組來寫正則表達式,正則本身沒有任何變化,只是在子表達式前面加上加上一個命名。views.py裏的對應關系可以這麽寫
    from django.urls import path, re_path
    from cmdb import views
    urlpatterns = [
    # re_path(‘^detail2-(\d+)-(\d+).html‘, views.detail2),
    re_path(‘^detail2-(?P<nid>\d?)-(?P<uid>\d?).html‘, views.detail2),
    ]

    上面被註釋的是普通捕獲組的寫法,下面的是命名捕獲組的寫法。使用了命名捕獲組後,我們的處理函數的參數名字就是正則中的命名,但是位置無所謂了。下面的處理函數直接在頁面輸出2個參數的值,就不寫頁面了:

    def detail2(request, uid, nid):
    return HttpResponse(‘%s-%s‘ % (nid, uid))

    可以用這樣的url測試 http://127.0.0.1:8000/detail2-1-3.html

    用 path() 方法實現捕獲參數

    課上講的是舊版本,現在Django已經2.0了,url()方法被path()方法替代,用法也有區別。
    re_path() 可以看做是2.0裏向下兼容的一個方法,也就是舊的1.0的 url() 方法。在2.0裏用 path() 方法也可以實現捕獲組的對應關系。使用 path() 方法需要註意:

    1. 要捕獲一段url中的值,需要使用尖括號,而不是之前的圓括號;
    2. 可以轉換捕獲到的值為指定類型,比如int。默認情況下,捕獲到的結果保存為字符串類型,不包含 ‘/‘ 這個特殊字符;
    3. 匹配模式的最開頭不需要添加 ‘/‘ ,因為默認情況下,每個url都帶一個最前面的 ‘/‘ ,既然大家都有的部分,就不用浪費時間特別寫一個了。

那麽現在 urls.py 裏的對應關系可以這麽寫:

    # re_path(‘detail2-(\d+)-(\d+).html‘, views.detail2),
    # re_path(‘^detail2-(?P<nid>\d+)-(?P<uid>\d+).html‘, views.detail2),
    path(‘detail2-<int:nid>-<int:uid>.html‘, views.detail2),

上面的例子,就是捕獲一個0或正整數,並且返回一個int類型,再用冒號把命名也完成了。除了int,還有下面這些。
默認情況下,Django內置下面的路徑轉換器:

  • str:匹配任何非空字符串,但不含斜杠/,如果你沒有專門指定轉換器,那麽這個是默認使用的;
  • int:匹配0和正整數,返回一個int類型;
  • slug:可理解為註釋、後綴、附屬等概念,是url拖在最後的一部分解釋性字符。該轉換器匹配任何ASCII字符以及連接符和下劃線,比如’ building-your-1st-django-site‘ ;
  • uuid:匹配一個uuid格式的對象。為了防止沖突,規定必須使用破折號,所有字母必須小寫,例如’075194d3-6885-417e-a8a8-6c931e272f00‘ 。返回一個UUID對象;
  • path:匹配任何非空字符串,重點是可以包含路徑分隔符’/‘。這個轉換器可以幫助你匹配整個url而不是一段一段的url字符串。

小結

上面各種實現的方法由淺入深,並且一個比一個好,推薦用最後面的實現方式:

  • 基於正則的url比使用get方式獲取參數的好
  • 命名捕獲組比普通捕獲組好
  • 推薦還是用最後的 path() 方法來實現,如果是1.x的版本,那麽就是推薦基於正則的命名捕獲組的方法。

另外,在定義函數的時候也可以寫成這種萬能的模式: def detail2(request, *args, **kwargs): ,這樣的話,要使用 args[0] (普通捕獲組)或 kwargs[‘nid‘] (命名捕獲組)來取值。

路由對應的名稱

還可以對url的關系進行命名,完成命名後,以後可以通過這個名字獲取到對應的url。好處是html裏使用url的名稱而不是寫死,那麽urls.py裏修改了url,不用到html裏修改了。
命名是在寫對應關系的時候,加上一個參數 name=[url的名稱]

    path(‘myurl/‘, views.myurl, name=‘myurl‘),

然後再網頁中使用的時候用 {% url [url的名稱] %} 替代原來寫死的url。

<form action="{% url ‘myurl‘ %}" method="get">
    <input type="text" name="name" placeholder="NAME" />
    <input type="submit" value="提交" />
</form>

打開頁面後可以按F12進入開發這模式查看頁面中的url已經是轉化後的具體的url了,就是我們在 urls.py 的對應關系裏寫的內容。
帶捕獲參數的情況:
url如果帶有捕獲參數,比如要捕獲2個參數:

    path(‘jump-<int:nid>-<int:uid>/‘, views.jump, name=‘jump‘),

那麽首先處理函數你必須寫上這2個參數(不寫會報錯),也可以用通用的 *args,**kwargs 的形式:

def jump(request, nid, uid):
    # return HttpResponse(‘%s-%s‘ % (nid, uid))
    return render(request, ‘jump.html‘)

然後頁面上做使用模板語言的時候,引用jump作為動態的url,但是後面也要跟上需要捕獲的值:

<a href="{% url ‘jump‘ 3 4 %}">跳轉</a>
<a href="{% url ‘jump‘ uid=3 nid=4 %}">跳轉</a>

上面兩種寫法都可以,上面的是按照位置傳參。下面故意先寫了uid,會按照名字來傳參。
訪問測試頁面隨便輸一個url,比如 “http://127.0.0.1:8000/jump-1-2/” 。然後頁面裏的兩個a連接生成的是各自新的url。新url整體不變,但是捕獲參數的值是在url名字後面的參數決定的。
引用當前頁面的url:引申一下,{{ request.path_info }} 就是當前頁面的url,如要要用的話,request默認就是處理函數裏要返回的變量,所以頁面裏直接用就可以了。
在處理函數中根據名稱獲取url:
from django.urls import reverse 使用這個reverse也能獲取到url。直接看獲取帶捕獲的url的方法:

def jump(request, nid, uid):
    from django.urls import reverse
    r1 = reverse(‘jump‘, args=(3, 4))
    r2 = reverse(‘jump‘, kwargs={‘uid‘:3, ‘nid‘:4})
    print(r1, r2, request.path_info)
    # return HttpResponse(‘%s-%s‘ % (nid, uid))
    return render(request, ‘jump.html‘)

也是2種形式都可以。

路由分發

之前所有的對應關系都是寫在app目錄外的urls文件裏的。當我們的項目有多個app的時候,所有的頁面都寫在一起也不好。應該是每個app各自管理自己的那部分url。這就需要路由分發。
首先我們app目錄外的公共url文件中導入一個include方法,聲明好app的url文件的位置:

from django.urls import include
urlpatterns = [
    path(‘admin/‘, admin.site.urls),
    path(‘cmdb/‘, include("cmdb.urls"))
]

上面的例子中保留了原有的admin的對應關系,如果有別的公共頁面還是可以在這裏寫的。然後是聲明了cmdb這個app的url文件的位置。現在去 cmdb 目錄下創建一個 urls.py 的文件。在這裏寫這個app的對應關系:

from django.urls import path
from cmdb import views
urlpatterns = [
    path(‘login/‘, views.login),
]

上面例子中的url是在cmdb這個app中的,所以訪問的時候要帶先加上app的名字,應該是 http://127.0.0.1:8000/cmdb/login/ 。這樣app裏可以有一個login頁面,外面也可以有一個login頁面。如果還有別的app,那個app也可以有login頁面。名字前面會加上自己的app的名字,命名空間不沖突。
梳理一下邏輯:當一個url請求過來之後,先到達項目目錄下的urls文件進行匹配。這裏如果匹配到了項目名,比如cmdb。那麽會再把它分發給之後的(就是app目錄裏的)urls文件繼續處理。所以配置過分發後,首先還是到項目目錄下的urls文件裏進行匹配,然後再用這裏的規則分發出去。

ORM

連接sqlite數據庫

默認使用的是sqlite3作為數據庫,使用數據庫需要一下步驟
一、創建你的數據庫表結構
app目錄下的models.py文件就是用來寫你的表結構的:

from django.db import models

# Create your models here.

# 文件默認會有上面的2行,下面是我們添加的內容
class UserInfo(models.Model):
    # 默認會自動創建自增id並作為主鍵
    username = models.CharField(max_length=32)  # 字符串,長度32
    password = models.CharField(max_length=64)

上面的類等到去數據庫創建表的時候,表名是 “cmdb_userinfo” ,也就是 [app名]_[類名] 。
二、設置settings.py文件
在 INSTALLED_APPS 註冊你的app,把你的app追加到這個列表裏:

INSTALLED_APPS = [
    ‘django.contrib.admin‘,
    ‘django.contrib.auth‘,
    ‘django.contrib.contenttypes‘,
    ‘django.contrib.sessions‘,
    ‘django.contrib.messages‘,
    ‘django.contrib.staticfiles‘,
    ‘cmdb‘,
]

配置你的數據庫連接,默認已經配置好了一個sqlite3,所以不需要修改:

# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

DATABASES = {
    ‘default‘: {
        ‘ENGINE‘: ‘django.db.backends.sqlite3‘,
        ‘NAME‘: os.path.join(BASE_DIR, ‘db.sqlite3‘),
    }
}

三、去終端執行2條命令

python manage.py makemigrations
python manage.py migrate

第一條命令會在 app 目錄下的 migrations 目錄下創建一個文件(0001_initial.py),記錄我們對 models.py 所做的變動。
第二條命令是真正去操作數據庫了,除了創建我們自己寫的表以外,還創建了很多 django 自己的表。
上面兩條命令都是作用於全局的,如果要限定作用於只在某個app,可以在最後加上app的名稱:

python manage.py makemigrations cmbd
python manage.py migrate cmdb

關於SQLite:

SQLite是一種嵌入式數據庫,它的數據庫就是一個文件。由於SQLite本身是C寫的,而且體積很小,所以,經常被集成到各種應用程序中,甚至在iOS和Android的App中都可以集成。
Python就內置了SQLite3,所以,在Python中使用SQLite,不需要安裝任何東西,直接使用。

使用SQLite

連接mysql數據庫

步驟同上,理論上只要修改一下 settings.py 裏的 DATABASES 的值就好了。但是還有一些別的坑。這裏主要演示一下怎麽連上mysql數據庫,連上之後,後面的操作還是在SQLite下來做。
DATABASES 設置的上面就是官方的幫助文檔的連接,或者直接參考下面的進行設置就好了:

DATABASES = {
    ‘default‘: {
        ‘ENGINE‘: ‘django.db.backends.mysql‘,
        ‘NAME‘: ‘cmdb‘,
        ‘USER‘: ‘admin‘,
        ‘PASSWORD‘: ‘admin123‘,
        ‘HOST‘: ‘192.168.246.12‘,
        ‘port‘: ‘3306‘,
    }
}

然後是坑,首先用戶我們得自己去數據庫上創建好,註意如果不是本地的數據庫,需要能夠遠程訪問。庫也要自己創建好,創建庫:

CREATE DATABASE cmdb CHARSET "utf8";

然後可以試著執行終端的2條命令,但是可能會報錯:

import MySQLdb as Database
ModuleNotFoundError: No module named ‘MySQLdb‘

意思是找不到這個庫,在python3裏mysql我們用 pymysql 這個庫。不過裝好了pymysql還是會提示找不到庫,因為django就是耿直的要找MySQLdb。解決辦法是編輯項目名同名目錄下的 __init__.py 文件,在這裏導入我們的pymysql並且會把它的名字就當做是 MySQLdb :

import pymysql
pymysql.install_as_MySQLdb()

ORM操作

添加

添加數據有2種方法,推薦用第一種。下面是寫在app目錄的views.py裏的處理函數:

from cmdb import models
def add_user(request):
    models.UserInfo.objects.create(
        username=‘root‘,
        password=‘123456‘
    )
    # 另外一個方法,先創建一個實例,然後調用它的save()方法
    obj = models.UserInfo(
        username=‘admin‘,
        password=‘admin123‘
    )
    obj.save()
    # 方法一的變種,把字典直接作為參數傳入
    dic = {‘username‘: ‘user‘, ‘password‘: ‘user123‘}
    models.UserInfo.objects.create(**dic)
    return HttpResponse("add user")

首先我們要操作某個表,就要先把這個創建這個表的那個類導入進來,例子的第一行。上面一個創建了3條數據了。

查詢

用all方法查詢到的數據,首先是放在一個列表裏,列表的元素是一個一個的對象,每一個對象就是一條記錄。
篩選的方法有filter,這個返回的也是個列表,因為可能返回多條。

def show_user(request):
    res = models.UserInfo.objects.all()
    for row in res:
        print(row.id, row.username, row.password)
    users = models.UserInfo.objects.filter(username=‘root‘)
    for row in users:
        print(row.id, row.username, row.password)
    return HttpResponse("show user")

filter()裏面還可以傳入多個參數,就是多個條件,他們之間的關系是邏輯與(and)。
還有一個first()方法,取出第一個值,這樣返回就不是列表而直接就是對象了。可以直接用,也可以用在filter()方法後面。all()方法後面也是可以用的,不過沒意義

models.UserInfo.objects.first()
models.UserInfo.objects.filter(username=‘root‘, password=‘123456‘).first()
# 上面就是一個驗證用戶登錄的邏輯了,返回結果是None或者是找到的第一個對象

另外還有一個get方法也可以獲取到一條數據,但是如果數據不存在不是返回空而是會報錯。如果要用那就得寫個try:

models.UserInfo.objects.get(id=10)

QuerySet 對象,分別打印出查詢結果和這個對象的query屬性:

res = models.UserInfo.objects.all()
print(res)  # 結果在下面
# <QuerySet [<UserInfo: UserInfo object (1)>, <UserInfo: UserInfo object (2)>, <UserInfo: UserInfo object (3)>]>
print(res.query)  # 結果寫下面
# SELECT "cmdb_userinfo"."id", "cmdb_userinfo"."username", "cmdb_userinfo"."password" FROM "cmdb_userinfo"

可以看到這是一個 QuerySet 對象,不是一個普通的列表。這裏要引出它的一個屬性 query 。
這個對象有一個query屬性,該屬性的內容是獲取這個對象時對應的SQL語句。

刪除

刪除前首先要先做查找,調用查找結果的delete()方法,就完成了刪除:

models.UserInfo.objects.all().delete
models.UserInfo.objects.filter(id=2).delete

修改

修改也是在查找的基礎上,調用update()方法來完成的:

models.UserInfo.objects.filter(id=1).update(password=‘root123‘)

示例

先來寫一個登錄頁面 index.html 用來提交用戶名和密碼進行驗證:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        label{
            width: 80px;
            text-align: right;
            display: inline-block;
        }
    </style>
</head>
<body>
<form action="/login/" method="post">
    <p>
        <label for="usernmae">用戶名:</label>
        <input id="usernmae" name="user" type="text" />
    </p>
    <p>
        <label for="password">密碼:</label>
        <input id="password" name="pwd" type="password" />
        <input type="submit" value="提交">
        <span style="color: red">{{ error_msg }}</span>
    </p>
</form>
</body>
</html>

然後是index的處理函數,用戶驗證失敗報錯誤信息,驗證成功跳轉的下一個頁面:

def login(request):
    if request.method == ‘GET‘:
        return render(request, ‘login.html‘)
    elif request.method == ‘POST‘:
        user = request.POST.get(‘user‘)
        pwd = request.POST.get(‘pwd‘)
        obj = models.UserInfo.objects.filter(username=user, password=pwd).first()
        if obj:
            # 先跳轉到admin,可以測一下,之後再寫index頁面
            return redirect(‘/admin/‘)
            # return redirect(‘/userlist/‘)
        else:
            return render(request,
                          ‘login.html‘,
                          {‘error_msg‘: ‘用戶名或密碼錯誤‘})
    else:
        return HttpResponse("不支持您的請求方式")

測試跳轉沒問題之後,就可以把上面的跳轉從admin頁面換到userlist頁面,然後就來寫這個userlist頁面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h2>添加用戶</h2>
    <form method="post" action="/userlist/">
        <input type="text" name="user" placeholder="username">
        <input type="text" name="pwd" placeholder="password">
        <input type="submit" value="添加">
    </form>
</div>
<div>
    <h2>用戶列表</h2>
    <table border="1">
        <thead>
        <tr>
            <th>id</th>
            <th>username</th>
            <th>password</th>
            <th>按鈕</th>
        </tr>
        </thead>
        <tbody>
        {% for row in users %}
        <tr>
            <td>{{ row.id }}</td>
            <td>{{ row.username }}</td>
            <td>{{ row.password }}</td>
            <td>
                <a href="/userdel-{{ row.id }}/">刪除</a>
                <span>|</span>
                <a href="/useredit-{{ row.id }}/">編輯</a>
            </td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>
</body>
</html>

首先頁面的中要實現數據庫查詢的功能,就是顯示用戶列表,通過GET方法來實現。
另外還有一個增加數據的功能,頁面上面的添加用戶,請求是通過POST方法提交過來,完成的數據添加。POST方法可以有2中return的方式,直接的方式就是和GET方法一樣。或者也可以用例子裏使用的方法,就是再提交一次GET請求:

def user_list(request):
    if request.method == ‘GET‘:
        users = models.UserInfo.objects.all()
        return render(request, ‘userlist.html‘, {‘users‘: users})
    elif request.method == ‘POST‘:
        username = request.POST.get(‘user‘)
        password = request.POST.get(‘pwd‘)
        models.UserInfo.objects.create(username=username, password=password)
        # 這裏可以和get返回的一樣
        # return render(request, ‘userlist.html‘, {‘users‘: users})
        # 這裏選擇用redirect()方法返回,就是再調用一次get方法返回頁面
        return redirect(‘/userlist/‘)

刪除功能不需要寫頁面,只需要一個處理函數:

def user_del(request, nid):
    models.UserInfo.objects.filter(id=nid).delete()
    return redirect(‘/userlist/‘)

最後還有一個編輯功能,現在只能用寫一個新的頁面然後再那個頁面裏提交。這樣實現起來比較簡單,主要就是通過這個示例把數據庫的最刪改查都用一遍:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h2>編輯用戶</h2>
    <form method="post" action="/useredit-{{ obj.id }}/">
        <input type="text" name="user" value="{{ obj.username }}">
        <input type="text" name="pwd" value="{{ obj.password }}">
        <input type="submit" value="提交">
    </form>
</div>
</body>
</html>

這個頁面對應的處理函數是如下:

def user_edit(request, nid):
    if request.method == ‘GET‘:
        obj = models.UserInfo.objects.filter(id=nid).first()
        return render(request, ‘useredit.html‘, {‘obj‘: obj})
    elif request.method == ‘POST‘:
        username = request.POST.get(‘user‘)
        password = request.POST.get(‘pwd‘)
        models.UserInfo.objects.filter(id=nid).update(
            username=username, password=password)
        return redirect(‘/userlist/‘)

在userlist頁面點擊編輯按鈕後,GET請求跳轉到useredit頁面。在編輯頁面提交後向useredit發送一個POST請求修改數據,然後返回userlist頁面,完成一次編輯。
上面一個有4個處理函數,其中3個有html頁面,urls.py的對應關系如下:

    path(‘login/‘, views.login),
    path(‘userlist/‘, views.user_list),
    path(‘userdel-<int:nid>/‘, views.user_del),
    path(‘useredit-<int:nid>/‘, views.user_edit),

ORM表結構

修改表結構

修改過表結構之後,需要再執行一下下面的2行命令,把新的表結構應用到數據庫。

python manage.py makemigrations
python manage.py migrate

修改數據長度、刪除一列,這類情況沒什麽特別的問題。
增加一列,默認情況下字段值不允許為空,此時會有提示。要麽全部都設為空,要麽你給個默認值,全部都設為默認值。另外還可以直接定義到表結構中:

class UserInfo(models.Model):
    # 默認會自動創建自增id並作為主鍵
    username = models.CharField(max_length=32)  # 字符串,長度32
    password = models.CharField(max_length=64)
    email = models.CharField(max_length=64, null=True)  # 設置為允許空值

字段類型

基本的字段類型有:字符串、數字、時間、二進制。
Django的ORM提供了非常多的字段類型,比如:EmailField、URLField、GenericIPAddressField。這些其實都是字符串類型而已,並且確實對我們沒任何用(並不能幫我們做數據驗證)。這些字段類型的只有在用Django的後臺管理頁面 admin 的時候才能發揮數據驗證的效果。只有通過admin提交數據的時候才會驗證你的數據格式是否正確。接下來就先講怎麽登進去
自增id,之前定義表結構的時候,省略了主鍵,讓Django幫我創建了自增id。也可以自己定義主鍵和自增id:

class UserGroup(models.Model):
    uid = models.AutoField(primary_key=True)  # 數據類型是自增,並且設為主鍵
    group = models.CharField(max_length=32)

登錄Admin

admin具體要到後面講,這裏先讓我們登錄進去

  1. 創建超級管理員,輸入命令後會提示你輸入用戶名、郵箱(可以直接回車)、密碼(似乎有長度和復雜度的要求):
    python manage.py createsuperuser
  2. 配置後臺管理url,就是admin頁面的對應關系,默認urls.py裏面已經配好了:
    path(‘admin/‘, admin.site.urls),
  3. 註冊和配置 admin 後臺管理頁面,把你的表註冊號之後,就可以通過admin進行管理了:
    from django.contrib import admin
    # Register your models here.
    # 上面是文件默認就有的內容
    from cmdb import models
    admin.site.register(models.UserInfo)

參數

null :數據庫中字段是否可以為空
default :數據庫中字段的默認值
db_column :數據庫中字段的列名。默認列明就是我們的變量名,可以通過這個參數設置成不一樣的

class UserInfo(models.Model):
    username = models.CharField(max_length=32)  # 字段名就是變量名 username
    password = models.CharField(max_length=64, db_column=‘pwd‘)  # 數據庫中的字段名會是 pwd

db_index :是否建立索引
unique :是否可以建立索引
unique_for_date :只對字段中【日期】部分建立唯一索引
unique_for_month :只對字段中【月】部分建立唯一索引
unique_for_year :只對字段中【年】部分建立唯一索引
auto_now :自動生成一個當前時間,數據更新時(包括創建)
auto_now_add :自動生成一個當前時間,數據創建時

class UserInfo(models.Model):
    # 比如用戶註冊時會生成用戶名和密碼
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    # 創建記錄時會生成當前時間存放在ctime裏,這個就是用戶的註冊時間
    ctime = models.DataTimeField(auto_now_add=True)
    # 用戶修改密碼會更新uptime的時間,這個就是上次修改密碼的時間
    uptime = models.DataTimeField(auto_now=True)

# models.UserInfo.objects.filter(id=nid).update(password=‘123456‘) 這種方法更新是不會刷新 auto_now 的時間的
# 用save()方法更新可以刷新 auto_now 的時間
# obj =  models.UserInfo.objects.filter(id=nid).first()
# obj.password = ‘654321‘
# obj.save()

Admin中有效果的參數
choices :Admin中顯示選擇框的內容。(用不變動的數據放在內存中從而避免跨表操作,跨表操作會涉及到性能問題)

class UserInfo(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    # 用戶有各種類型
    user_type_choices = (
        (1, ‘管理員‘)
        (2, ‘普通用戶‘)
        (3, ‘訪客‘)
    )
    # 定義一個用戶類型的字段
    user_type_id = models.IntegerField(choices=user_type_choices, default=2)
# 這樣數據庫裏是一個整數類型,值是1、2、3
# 但是我們在admin裏看選擇框的時候看到的是“管理員”、“普通用戶”、“訪客”,這就是因為把選項所對應的內容放到了內存中了
# 有了Django這個功能就不用再搞一張表,存放各個數值對應的內容了,還要做外鍵關聯,用的時候還要連表查詢
# 即使不用admin,我們也可以在自己的代碼裏讀取這個屬性獲取到內容,避免連表查詢

blank :Admin中是否允許用戶輸入為空
verbose_name :Admin中顯示的字段名稱,默認顯示為變量名
editable :Admin中是否可以編輯。默認是True,設為False後就是在admin中不可編輯了,也不會顯示出來了。
error_messages :自定義錯誤信息(字典類型)。字典key:null、blank、invalid、invalid_choice、unique、unique_for_date

class UserInfo(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64, error_messages={‘null‘: "不能為空", ‘invalid‘: ‘格式錯誤‘})

help_text :Admin中該字段的提示信息。默認沒有提示信息,設置後會顯示在input框的下方
validators :自定義錯誤驗證(列表類型),具體要等到後面講

外鍵操作-一對多

上面講的choices參數,提供了一種將數據存在內存中來提高效率的方法。好處是避免了跨表操作提高了效率。壞處也有,就是數據不方便修改。如果要修改,那就要修改好之後重啟一下服務使你的修改生效。而重啟操作是有風險的應該避免,那麽對於這種經常要修改的內容就不適合放在內存中了,而是要放到另外一張表裏。

創建外鍵關聯-修改表結構

在models.py裏修改我們的表結構,新增一張用戶部門表,原來的用戶信息表中新增一列部門id:

from django.db import models

# Create your models here.

# 新增一張表
class UserGroup(models.Model):
    group_id = models.AutoField(primary_key=True)  # 這次自己寫自增id
    dept = models.CharField(max_length=32, unique=True)

class UserInfo(models.Model):
    # 默認會自動創建自增id並作為主鍵
    username = models.CharField(max_length=32)  # 字符串,長度32
    password = models.CharField(max_length=64)
    # 新增一列存放部門
    # to_field參數可以缺省,默認就是主鍵
    # on_delete=models.CASCADE,這個在1裏應該是默認值,現在不能缺省了
    user_group = models.ForeignKey(‘UserGroup‘, on_delete=models.CASCADE, to_field=‘group_id‘)

然後去終端執行那2條命令使新的表結構生效:

python manage.py makemigrations
python manage.py migrate

添加部門數據

這裏可以的話最好直接是直接去操作數據庫,否則簡單搞個網頁來添加數據。參看之前的userlist,簡單搞個只做顯示和添加的頁面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h2>添加部門</h2>
    <form method="post" action="/grouplist/">
        <input type="text" name="dept" placeholder="部門名稱">
        <input type="submit" value="添加">
    </form>
</div>
<div>
    <h2>部門列表</h2>
    <table border="1">
        <thead>
        <tr>
            <th>id</th>
            <th>dept</th>
        </tr>
        </thead>
        <tbody>
        {% for row in dept %}
        <tr>
            <td>{{ row.group_id }}</td>
            <td>{{ row.dept }}</td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>
</body>
</html>

然後是views.py裏的處理函數:

def group_list(request):
    if request.method == ‘GET‘:
        dept = models.UserGroup.objects.all()
        return render(request, ‘grouplist.html‘, {‘dept‘: dept})
    elif request.method == ‘POST‘:
        dept = request.POST.get(‘dept‘)
        models.UserGroup.objects.create(dept=dept)
        return redirect(‘/grouplist/‘)

urls.py裏的對應g關系:

    path(‘grouplist/‘, views.group_list),

查看被關聯的屬性

對於UserInfo中新增的一列,在類中我們的屬性名稱是 "user_group" ,而實在在數據庫中創建的自動名是 "user_group_id"。
我們再操作的時候就有2個屬性可以操作:

  • .user_group_id :就是這個字段裏的值,也就是數據庫裏實際存放的內容
  • .user_group :這是一個對象,通過這個對象取到UserGroup裏的內容,比如:
    • .user_group.group_id :就是UserGroup表裏的自增id,結果和 .user_group_id 應該是一樣的
    • .user_group.dept :就是這個username鎖關聯的部門名稱了

修改之前的userlist頁面,現在把部門名稱也顯示出來。這裏只需要改html,處理函數時不用修改的。實際也只需要在表格中加上一列直接可以去到關聯的表裏的屬性值。下面是userlist表格的部分內容:

<div>
    <h2>用戶列表</h2>
    <table border="1">
        <thead>
        <tr>
            <th>id</th>
            <!-- 這裏加一列 -->
            <th>username</th>
            <th>dept</th>
            <th>password</th>
            <th>按鈕</th>
        </tr>
        </thead>
        <tbody>
        {% for row in users %}
        <tr>
            <td>{{ row.id }}</td>
            <td>{{ row.username }}</td>
            <!-- 這裏加一列,直接就能取到部門名稱 -->
            <td>{{ row.user_group.dept }}</td>
            <td>{{ row.password }}</td>
            <td>
                <a href="/userdel-{{ row.id }}/">刪除</a>
                <span>|</span>
                <a href="/useredit-{{ row.id }}/">編輯</a>
            </td>
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>

添加用戶時選擇部門

顯示沒問題了,頁面的上部還有添加用戶,現在再要添加用戶就需要把用戶部門也加上了。部門搞成一個下拉框,不過下拉框的內容還需要修改處理函數傳值過來。處理函數還要處理頁面提交的內容:

def user_list(request):
    if request.method == ‘GET‘:
        users = models.UserInfo.objects.all()
        # 這裏多獲取一個部門的列表,傳給頁面,頁面的下來列表會用到。直接找UserGroup獲取數據
        # 把對象傳給頁面的下拉列表,列表的value就是對象的id,列表的內容就是對象的dept
        depts = models.UserGroup.objects.all()
        return render(request, ‘userlist.html‘, {‘users‘: users, ‘depts‘: depts})
    elif request.method == ‘POST‘:
        username = request.POST.get(‘user‘)
        password = request.POST.get(‘pwd‘)
        # 這裏通過select獲取到的直接就是id的值,所以提交的時候也簡單的提交值就可以了
        group_id = request.POST.get(‘group_id‘)
        models.UserInfo.objects.create(username=username, password=password, user_group_id=group_id)
        return redirect(‘/userlist/‘)

頁面裏加上下拉列表,下面是添加用戶的部分:

<div>
    <h2>添加用戶</h2>
    <form method="post" action="/userlist/">
        <input type="text" name="user" placeholder="username">
        <select name="group_id">
            {% for item in depts %}
            <option value="{{ item.group_id }}">{{ item.dept }}</option>
            {% endfor %}
        </select>
        <input type="text" name="pwd" placeholder="password">
        <input type="submit" value="添加">
    </form>
</div>

數據添加的另外一種方法
上面通過下拉列表方便的獲取到了部門id的值,所以直接通過傳值給user_group_id完成了數據的添加。也可以通過傳對象給user_group完成數據的添加,大概是這樣的:

        group = models.UserGroup.objects.filter(id=1).first()
        models.UserInfo.objects.create(username=username, password=password, user_group=group)

兩種方法根據實際情況選擇,不過傳值的方法更好,少一次數據庫的操作。
還有更多內容要下節講了

模板

這天沒講到

課後練習

用戶管理:

  • 一張用戶表、一張用戶組表。做好外鍵關聯。分別實現對兩張表的增刪改查
  • 添加,做成模態對話框的形式
  • 修改,目前可以用頁面跳轉的形式,但是要顯示默認值
  • 做一個比較好看的頁面,推薦套用模板

Python自動化開發學習19-Django