Django雜篇(1)
這里我們介紹以下Django常用的一些小工具,分別是:
- bulk_create,一種將數據批量插入數據庫的方法,效率較高
- Pagination,自定義的一種分頁器,重點在於其思路和使用
- 多對多表關系的創建方法,常見的有三種
- form校驗組件的應用,其主要作用就是按照我們的要求校驗數據的格式,並取到符合條件以及不符合條件的數據和報錯
bulk_create
其實批量插入數據的原理非常簡單,在日常來看,我們在向數據庫插入數據的時候,通常都是一條一條的插入,如果有相似數據,我們通常會用一個循環,來持續插入,實際上這種方法如果在數據量比較大的情況下會非常耗時,所以我們才會引入這種插入方式,下面就用一個非常簡單的例子來做一下對比,首先我們創建一個Django項目,命名第一個應用名為app01.
# 我們在models里面創建一個最簡單的書籍表
# app01/models.py
class Book(models.Model):
title=models.CharField(max_length=32)
# 然后在Terminal窗口,輸入python manage.py makemigrations和python manage.py migrate之后就成功建立了表,然后在urls.py里面加路由
# urls.py
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^index/', views.index),
]
# 然后在views.py里面寫對應路由的函數,以下這種就是效率最低,也是最常用的插入方式,1000條數據甚至要用兩分鍾左右,而且對數據庫的壓力特別大,因為每次寫入都要操作數據庫
# views.py,常規插入方式
def index(request):
# 往書籍表中插入數據 1000
for i in range(1000): # 這種插入方式 效率極低
models.Book.objects.create(title='第%s本書'%i)
book_queryset = models.Book.objects.all()
return render(request,'index.html',locals())
# index.html
<body>
{% for book_obj in book_queryset %}
<p>{{ book_obj.title }}</p>
{% endfor %}
</body>
換用bulk_create的話,views.py里面就要按以下這種方式寫:
# views.py,bulk_create批量插入方式
def index(request):
book_list = []
for i in range(100000):
book_list.append(models.Book(title='第%s本書'%i))
models.Book.objects.bulk_create(book_list) # 批量插入數據
book_queryset = models.Book.objects.all()
return render(request,'index.html',locals())
我們可以很明確的發現,用批量插入十萬條數據僅僅用了不到二十秒,差距非常明顯.其實原理非常簡單
- 無非就是平時是一條一條插入,用了bulk_create之后我們是把所有需要插入的數據先插入一個列表中,這個操作是在計算機內部完成的,耗時非常低,然后通過bulk_create把這個列表插入到數據庫中,從而極大的提升了效率
Pagination
首先我們要知道分頁器的概念,因為通常情況下一個網站其頁面大小是有限的,不能展示其全部的信息,所以需要分頁器來吧這些內容分成不同的頁面,以便於我們瀏覽,那么我們首先用自己的思路來實現一個簡單的分頁器,我們繼續上面的項目寫,把插入數據的那幾行注釋掉就好
# views.py
# 自定義分頁器
def index(request):
# 1. 獲取用戶想要訪問的頁碼數
current_page = request.GET.get('page', 1) # 獲取用戶輸入,如果沒有page參數,默認展示第一頁
current_page = int(current_page)
# 2. 每頁展示多少條數據,這里我們設置為每頁10條
per_page_num = 10
# 3. 定義起始位置和終止位置
start_page = (current_page - 1) * per_page_num
end_page = current_page * per_page_num
# 4. 統計數據的總條數
book_queryset = models.Book.objects.all()
all_count = book_queryset.count()
# 5. 求數據到底需要多少個頁面才能展示完
page_num, more = divmod(all_count, per_page_num)
if more:
page_num += 1
# page_num就決定了需要多少個頁碼
page_html = ''
xxx = current_page # 這里取出來當前選中的頁碼賦給xxx,以便於后面判斷當前頁面
if current_page < 6: # 如果當前頁面頁碼小於6,就不能再往前數五個頁面,所以要為其重置為6
current_page=6
for i in range(current_page-5,current_page+6): # 這里我們希望分頁器是以選中頁面的頁碼為中心,然后左右各有五個頁面的頁碼供選擇
if xxx == i:
page_html+='<li class="active"><a href="?page=%s">%s</a></li>' # 這里可以設置當前頁面的頁碼為高亮的形式
else:
page_html+='<li><a href="?page=%s">%s</a></li>'
book_queryset = models.Book.objects.all()[start_page:end_page]
return render(request, 'index.html', locals())
# 然后,在index.html里面,我們如下寫,這里分頁的格式是來自於bootstrap,我們修改后即可使用,
<body>
<nav aria-label="Page navigation">
<ul class="pagination">
<li>
<a href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{{ page_html|safe }}{#這里就是我們所寫的最關鍵的一句,|safe的意思是取消轉義,即后端傳過來的符合前端標簽要求的語句可以被表現出其應有的形式,而不只是字符串形式#}
<li>
<a href="#" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
</body>
以上就是我們手動實現的一個簡略的分頁器,當然還有好多功能沒有完善,不過我們了解分頁器的思路即可,因為實際生產中我們並不需要手動去寫分頁器,這里有一個較完整的現成的分頁器代碼,我們將其記錄下來,會調用即可
# 在app01下面新建一個utils文件夾,然后新建一個mypage.py文件,將以下代碼復制粘貼即可
class Pagination(object):
def __init__(self, current_page, all_count, per_page_num=10, pager_count=11):
"""
封裝分頁相關數據
:param current_page: 當前頁
:param all_count: 數據庫中的數據總條數
:param per_page_num: 每頁顯示的數據條數
:param pager_count: 最多顯示的頁碼個數
用法:
queryset = model.objects.all()
page_obj = Pagination(current_page,all_count)
page_data = queryset[page_obj.start:page_obj.end]
獲取數據用page_data而不再使用原始的queryset
獲取前端分頁樣式用page_obj.page_html
"""
try:
current_page = int(current_page)
except Exception as e:
current_page = 1
if current_page < 1:
current_page = 1
self.current_page = current_page
self.all_count = all_count
self.per_page_num = per_page_num
# 總頁碼
all_pager, tmp = divmod(all_count, per_page_num)
if tmp:
all_pager += 1
self.all_pager = all_pager
self.pager_count = pager_count
self.pager_count_half = int((pager_count - 1) / 2)
@property
def start(self):
return (self.current_page - 1) * self.per_page_num
@property
def end(self):
return self.current_page * self.per_page_num
def page_html(self):
# 如果總頁碼 < 11個:
if self.all_pager <= self.pager_count:
pager_start = 1
pager_end = self.all_pager + 1
# 總頁碼 > 11
else:
# 當前頁如果<=頁面上最多顯示11/2個頁碼
if self.current_page <= self.pager_count_half:
pager_start = 1
pager_end = self.pager_count + 1
# 當前頁大於5
else:
# 頁碼翻到最后
if (self.current_page + self.pager_count_half) > self.all_pager:
pager_end = self.all_pager + 1
pager_start = self.all_pager - self.pager_count + 1
else:
pager_start = self.current_page - self.pager_count_half
pager_end = self.current_page + self.pager_count_half + 1
page_html_list = []
# 添加前面的nav和ul標簽
page_html_list.append('''
<nav aria-label='Page navigation>'
<ul class='pagination'>
''')
first_page = '<li><a href="?page=%s">首頁</a></li>' % (1)
page_html_list.append(first_page)
if self.current_page <= 1:
prev_page = '<li class="disabled"><a href="#">上一頁</a></li>'
else:
prev_page = '<li><a href="?page=%s">上一頁</a></li>' % (self.current_page - 1,)
page_html_list.append(prev_page)
for i in range(pager_start, pager_end):
if i == self.current_page:
temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
else:
temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
page_html_list.append(temp)
if self.current_page >= self.all_pager:
next_page = '<li class="disabled"><a href="#">下一頁</a></li>'
else:
next_page = '<li><a href="?page=%s">下一頁</a></li>' % (self.current_page + 1,)
page_html_list.append(next_page)
last_page = '<li><a href="?page=%s">尾頁</a></li>' % (self.all_pager,)
page_html_list.append(last_page)
# 尾部添加標簽
page_html_list.append('''
</nav>
</ul>
''')
return ''.join(page_html_list)
下面我們介紹怎樣去調用這個已經存在定義好的分頁器
# urls.py,寫對應路由
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^login/', views.login),
]
# views.py
from django.shortcuts import render,HttpResponse,redirect
from app01 import models
from app01.utils.mypage import Pagination # 這里導入剛才粘貼的已經定義好的分頁器類
# 使用封裝好的分頁器代碼
def login(request):
book_queryset = models.Book.objects.all() # 這里取到表內所有的數據,生成一個queryset對象
current_page = request.GET.get('page',1) # 從前端發來的get請求里面取出用戶點擊的page頁面,如果沒有默認就是1,也就是首頁
all_count = book_queryset.count() #這里對queryset對象進行計數,得到一共多少條記錄
# 1.實例化產生對象
page_obj = Pagination(current_page=current_page,all_count=all_count)# 我們只需要把記錄的總數量和當前頁面的page數傳進函數里面即可
# 2.對真實數據進行切片操作
page_queryset = book_queryset[page_obj.start:page_obj.end]
return render(request,'login.html',locals())
# login.html,前端的調用也非常簡單
<body>
{% for book_obj in page_queryset %} {#這里把后端傳來的queryset對象直接循環,顯示出來,即可以看到第*本書#}
<p>{{ book_obj.title }}</p>
{% endfor %}
{{ page_obj.page_html|safe }} {#這里是顯示分頁器的核心操作,加|safe是取消轉義#}
</body>
創建多對多表關系的常用方法
-
第一種
就是我們之前在建表的時候用到的,利用models.ManyToManyField方法來建立表和表之間的多對多關系,Django會自動生成第三張表.
但是這種方法的弊端在於,第三張表只有主鍵和兩張表的關系字段,且我們不能額外對其添加字段,所以其可擴展性較差,不利於項目后期的維護與開發.實例如下:
class Book(models.Model): ... author=models.ManyToManyField(to='Author') class Author(models.Model): ...
-
第二種
純手動創建,即我們不依賴於ORM自動幫我們創建表關系,而是自己手動創建,這種方法所建立的表可擴展性好,可以添加很多我們想要額外增加的功能,比如表數據創建的時間等.但是弊端在於很多ORM的查詢方法不能用,查詢的效率會比較低,而且代碼較復雜,開發效率也比較低,實例如下:
class Book(models.Model): ... class Author(models.Models): ... class Book2Author(models.Model): book_id = models.ForeignKey(to='Book') author_id = models.ForeignKey(to='Author') create_time = models.DateField(auto_now_add=True) ...
-
第三種
半自動的創建,即多對多的關系表還是我們自己手動創建,但是我們會告訴Django的ORM關系表的名字和位置,這樣我們既保留了ORM查詢的便利性,又保留了第三張關系表的可擴展性,可謂一舉兩得,但是唯一的缺點可能就是需要寫的代碼比較多,不過也無可厚非.
半自動的創建表關系所需要額外加的兩個參數是
through='關系表的名字',through_fields=('表名(外鍵所在的表)','表名(外鍵不在的那個表)')
,注意表名的順序,不能反class Book(models.Model): ... authors = models.ManyToManyField(to='Author', through='Book_Author', through_fields=('book','author')) class Author(models.Model): ... books = models.ManyToManyField(to='Book', through='Book2Author', through_fields=('author', 'book')) class Book_Author(models.Model): book = models.ForeignKey(to='Book') author = models.ForeignKey(to='Author') ...
form校驗組件的應用
我們在寫一些大大小小的項目的時候,多多少少都會用到校驗的功能,比如用戶注冊賬號的時候,驗證用戶名是否存在,用戶密碼兩次輸入是否相同,都需要用到校驗.不管是前端還是后端,都有必要加上數據校驗的功能,一方面是為了數據庫的安全,另外一方面也可以阻止一些非法數據的流入.不過相比之下,后端的校驗比前端校驗更有必要,因為前端頁面是寫給大眾看的,很多東西都是公開的,可以直接修改,對數據的校驗有很大的阻力,所以我們就更要重視在后端對數據的校驗.
那么form組件就可以幫助我們來完成數據的校驗工作,當然,他不止能完成數據的校驗,還可以完成頁面的渲染和錯誤信息的展示,可謂十分強大.
在對form進行說明之前,我們首先要在views.py里面建立一個類,以便於后面的調用和測試
# views.py
from django import forms
class MyRegForm(forms.Form):
sername = forms.CharField(min_length=3,max_length=8)
password = forms.CharField(min_length=3,max_length=8)
email = forms.EmailField()
# 是不是覺得以上定義的類有些眼熟?確實,跟在models.py里面定義表結構十分相似
渲染頁面
渲染頁面的實際意義是我們在后端用form校驗過數據之后把數據發送到前端,然后前端通過一些形式來展示出來,forms組件會自動幫我們渲染用戶輸入(或者是選擇,下拉框等)的標簽,但是提交按鈕以及一些別的數據並不會自動幫我們渲染,還是需要我們手動去設置.forms常用的三種渲染頁面的方式如下:
# formm.html
<body>
<p>
第一種渲染前端頁面的方式:封裝程度非常高,但是標簽樣式及參數不方便調整,可擴展性較差
{{ form_obj.as_p }}
{{ form_obj.as_ul }}
</p>
<p>第二種渲染頁面的方式:擴展性較高,需要我們手寫的代碼量比較多</p>
<p>
{{ form_obj.username.label }}{{ form_obj.username }}{#.lable可以看到其標簽,.username則是顯示出其真實的內容,這里是一個input輸入框#}
</p>
<p>
{{ form_obj.password.label }}{{ form_obj.password }}
</p>
<p>
{{ form_obj.email.label }}{{ form_obj.email }}
</p>
<p>第三種渲染前端頁面的方式:代碼量和擴展性都很高(推薦使用)</p>
<form action="" method="post" novalidate> {#這里添加novalidate參數可以取消前端幫我們做校驗,以便於我們只在后端做校驗#}
{% for foo in form_obj %}
<p>
{{ foo.label }}:{{ foo }}
<span style="color: red">{{ foo.errors.0 }}</span>
</p>
{% endfor %}
<input type="submit">
</form>
</body>
展示錯誤信息
在forms組件里面展示錯誤信息非常簡單,只需要用對象點errors.0就可以了,如下:
#formm.html
<body>
<form action="" method="post" novalidate>
{% for foo in form_obj %}
<p>
{{ foo.label }}:{{ foo }}
<span style="color: red">{{ foo.errors.0 }}</span> {#這里就是真正展示出錯誤的地方,即對象foo.errors.0 #}
</p>
{% endfor %}
<input type="submit">
</form>
</body>
當然以上的報錯都是英文顯示的,我們可以手動重寫來實現報錯用中文來顯示,具體就是在之前我們在views.py里定義的類MyRegForm,在里面加參數error_message={}即可,內部我們可以以報錯類型來重寫報錯內容,直接在冒號后面寫即可,
from django import forms
class MyRegForm(forms.Form):
sername = forms.CharField(min_length=3,max_length=8,label='用戶名',
error_messages={
'min_length':'用戶名最短三位', # 這里用戶名小於三位的話就會報這個錯,報錯內容被我們重寫之后就會這樣報錯,以下同理
'max_length':'用戶名最長八位',
'required':'用戶名不能為空'
},initial='我是初始值',required=False,# required賦值false的話該項不填也不會報錯,即允許不填,實際運用就是可以用在非必填項
widget= widgets.TextInput(attrs={'class':'form-control others'}))# widget可以改變該框的type屬性
password = forms.CharField(min_length=3,max_length=8)
email = forms.EmailField()
校驗數據
手動校驗(is_valid,cleaned_data,errors)
手動校驗即我們手動調用幾個函數來觀察數據是否通過校驗,比如is_valid,cleaned_data和errors,測試代碼如下:
# 這里我們測試的時候在左下角的Python Console里面測試,可以實時看到結果,比較方便
from app01 import views
#1. 給自定義的類定義一個字典
obj = views.MyRegForm({'username':'jason','password':'123','email':'12'})
#2. is_vaild()數據全部符校驗標准合才會返回True,但凡有不符合標准的都會返回False
obj.is_valid()
Out[5]: False
#3. cleaned_data可以查看查看所有符合條件的數據
obj.cleaned_data
Out[9]: {'username': 'jason', 'password': '123'}
#4. errors可以查看不符合條件的數據以及報錯的原因
obj.errors
Out[10]: {'email': ['Enter a valid email address.']}
#5. 校驗數據的時候,默認情況下類里面所有的字段都必須傳值
obj = views.MyRegForm({'username':'jason','password':'123'})
obj.is_valid()
Out[12]: False
obj.errors
Out[13]: {'email': ['This field is required.']}
#6. 默認情況下可以多傳,多傳后面的數據會舍棄,但是絕對不能少傳,少傳就會不符合校驗標准,出現False
obj=views.MyRegForm({'username':'jason','password':'1233','email':'123@qq.com','xxx':'ooo'})
obj.is_valid()
Out[15]: True
鈎子函數的校驗
對於數據的字段來說,定義的時候限制其格式是一方面,另外一方面我們可以用鈎子函數來對其做額外的校驗,這種方式在對於大項目的后期修改,維護和二次開發上有較大的應用.
局部鈎子
當我們需要對數據庫的表中的某一個字段的數據進行額外的校驗的時候,局部鈎子就是非常好的選擇,示例如下:
class MyRegForm(forms.Form):
def clean_username(self):# 局部鈎子要寫在MyRegForm總類的下面
username = self.cleaned_data.get('username')
if '黃' in username:# 檢測所有的用戶姓名,帶'黃'字的不符合標准,加入報錯信息
self.add_error('username','這本書的名字不符合標准')
return username # 最后要返回我們校驗的字段的數據,不然會看不到最后結果
全局鈎子
上面局部鈎子是針對單個字段進行二次校驗,所以全局鈎子就是可以根據多個字段進行校驗,比如,用戶注冊的時候我們用來校驗其輸入的兩次密碼是否一致,示例如下:
class MyRegForm(forms.Form):
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password','兩次密碼不一致')
return self.cleaned_data
正則校驗
我們還有第三種校驗數據的方式,即正則校驗.
其實正則校驗也是在我們定義類的時候就定義好的,與前面手動校驗的前提比較相似
# views.py 這里是對於159開頭的手機號的校驗,RegexValidator即為正則校驗的關鍵字
from django import forms
from django.forms import Form
from django.core.validators import RegexValidator
class MyForm(Form):
user = forms.CharField(
validators=[RegexValidator(r'^[0-9]+$', '請輸入數字'), RegexValidator(r'^159[0-9]+$', '數字必須以159開頭')],
)
常用字段
以下為幾個常用字段,需要使用時直接復制即可.
# 單選框select
gender = forms.ChoiceField(
choices=((1, "男"), (2, "女"), (3, "保密")),
label="性別",
initial=3,
widget=widgets.RadioSelect()
)
# 多選框select
hobby1 = forms.MultipleChoiceField(
choices=((1, "籃球"), (2, "足球"), (3, "雙色球"),),
label="愛好",
initial=[1, 3],
widget=widgets.SelectMultiple()
)
# 單選heckbox
keep = forms.ChoiceField(
label="是否記住密碼",
initial="checked",
widget=forms.widgets.CheckboxInput()
)
# 多選checkbox
hobby2 = forms.MultipleChoiceField(
choices=((1, "籃球"), (2, "足球"), (3, "雙色球"),),
label="愛好",
initial=[1, 3],
widget=forms.widgets.CheckboxSelectMultiple()
)