在上一個章節中,我們提到了Django是基於MVC架構的Web框架,MVC架構追求的是“模型”和“檢視”的解耦合。所謂“模型”說得更直白一些就是資料(的表示),所以通常也被稱作“資料模型”。在實際的專案中,資料模型通常透過資料庫實現持久化操作,而關係型資料庫在過去和當下都是持久化的首選方案,下面我們透過完成一個投票專案來講解和模型相關的知識點。投票專案的首頁會展示某線上教育平臺所有的學科;點選學科可以檢視到該學科的老師及其資訊;使用者登入後在檢視老師的頁面為老師投票,可以投贊成票和反對票;未登入的使用者可以透過登入頁進行登入;尚未註冊的使用者可以透過註冊頁輸入個人資訊進行註冊。在這個專案中,我們使用MySQL資料庫來實現資料持久化操作。
我們首先建立Django專案vote
併為其新增虛擬環境和依賴項。接下來,在專案下建立名為polls
的應用和儲存模板頁的資料夾tempaltes
,專案資料夾的結構如下所示。
根據上面描述的專案需求,我們準備了四個靜態頁面,分別是展示學科的頁面subjects.html
,顯示學科老師的頁面teachers.html
,登入頁面login.html
,註冊頁面register.html
,稍後我們會將靜態頁修改為Django專案所需的模板頁。
-
在MySQL中建立資料庫,建立使用者,授權使用者訪問該資料庫。
create database vote default charset utf8; create user 'hellokitty'@'%' identified by 'Hellokitty.618'; grant all privileges on vote.* to 'hellokitty'@'%'; flush privileges;
-
在MySQL中建立儲存學科和老師資訊的二維表(儲存使用者資訊的表稍後處理)。
use vote; -- 建立學科表 create table `tb_subject` ( `no` integer auto_increment comment '學科編號', `name` varchar(50) not null comment '學科名稱', `intro` varchar(1000) not null default '' comment '學科介紹', `is_hot` boolean not null default 0 comment '是不是熱門學科', primary key (`no`) ); -- 建立老師表 create table `tb_teacher` ( `no` integer auto_increment comment '老師編號', `name` varchar(20) not null comment '老師姓名', `sex` boolean not null default 1 comment '老師性別', `birth` date not null comment '出生日期', `intro` varchar(1000) not null default '' comment '老師介紹', `photo` varchar(255) not null default '' comment '老師照片', `gcount` integer not null default 0 comment '好評數', `bcount` integer not null default 0 comment '差評數', `sno` integer not null comment '所屬學科', primary key (`no`), foreign key (`sno`) references `tb_subject` (`no`) );
-
在虛擬環境中安裝連線MySQL資料庫所需的依賴項。
pip install mysqlclient
說明:如果因為某些原因無法安裝
mysqlclient
三方庫,可以使用它的替代品pymysql
,pymysql
是用純Python開發的連線MySQL的Python庫,安裝更容易成功,但是需要在Django專案資料夾的__init__.py
中新增如下所示的程式碼。import pymysql pymysql.install_as_MySQLdb()
如果使用Django 2.2及以上版本,還會遇到PyMySQL跟Django框架的相容性問題,相容性問題會導致專案無法執行,需要按照GitHub上PyMySQL倉庫Issues中提供的方法進行處理。總體來說,使用
pymysql
會比較麻煩,強烈建議大家首選安裝mysqlclient
。 -
修改專案的settings.py檔案,首先將我們建立的應用
polls
新增已安裝的專案(INSTALLED_APPS
)中,然後配置MySQL作為持久化方案。INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'polls', ] DATABASES = { 'default': { # 資料庫引擎配置 'ENGINE': 'django.db.backends.mysql', # 資料庫的名字 'NAME': 'vote', # 資料庫伺服器的IP地址(本機可以寫localhost或127.0.0.1) 'HOST': 'localhost', # 啟動MySQL服務的埠號 'PORT': 3306, # 資料庫使用者名稱和口令 'USER': 'hellokitty', 'PASSWORD': 'Hellokitty.618', # 資料庫使用的字符集 'CHARSET': 'utf8', # 資料庫時間日期的時區設定 'TIME_ZONE': 'Asia/Chongqing', } }
在配置ENGINE屬性時,常用的可選值包括:
'django.db.backends.sqlite3'
:SQLite嵌入式資料庫。'django.db.backends.postgresql'
:BSD許可證下發行的開源關係型資料庫產品。'django.db.backends.mysql'
:甲骨文公司經濟高效的資料庫產品。'django.db.backends.oracle'
:甲骨文公司關係型資料庫旗艦產品。
其他的配置可以參考官方文件中資料庫配置的部分。
-
Django框架提供了ORM來解決資料持久化問題,ORM翻譯成中文叫“物件關係對映”。因為Python是面向物件的程式語言,我們在Python程式中使用物件模型來儲存資料,而關係型資料庫使用關係模型,用二維表來儲存資料,這兩種模型並不匹配。使用ORM是為了實現物件模型到關係模型的雙向轉換,這樣就不用在Python程式碼中書寫SQL語句和遊標操作,因為這些都會由ORM自動完成。利用Django的ORM,我們可以直接將剛才建立的學科表和老師表變成Django中的模型類。
python manage.py inspectdb > polls/models.py
我們可以對自動生成的模型類稍作調整,程式碼如下所示。
from django.db import models class Subject(models.Model): no = models.AutoField(primary_key=True, verbose_name='編號') name = models.CharField(max_length=50, verbose_name='名稱') intro = models.CharField(max_length=1000, verbose_name='介紹') is_hot = models.BooleanField(verbose_name='是否熱門') class Meta: managed = False db_table = 'tb_subject' class Teacher(models.Model): no = models.AutoField(primary_key=True, verbose_name='編號') name = models.CharField(max_length=20, verbose_name='姓名') sex = models.BooleanField(default=True, verbose_name='性別') birth = models.DateField(verbose_name='出生日期') intro = models.CharField(max_length=1000, verbose_name='個人介紹') photo = models.ImageField(max_length=255, verbose_name='照片') good_count = models.IntegerField(default=0, db_column='gcount', verbose_name='好評數') bad_count = models.IntegerField(default=0, db_column='bcount', verbose_name='差評數') subject = models.ForeignKey(Subject, models.DO_NOTHING, db_column='sno') class Meta: managed = False db_table = 'tb_teacher'
說明:模型類都直接或間接繼承自
Model
類,模型類跟關係型資料庫的二維表對應,模型物件跟表中的記錄對應,模型物件的屬性跟表中的欄位對應。如果對上面模型類的屬性定義不是特別理解,可以看看本文後面提供的“模型定義參考”部分的內容。
有了Django框架的ORM,我們可以直接使用面向物件的方式來實現對資料的CRUD(增刪改查)操作。我們可以在PyCharm的終端中輸入下面的命令進入到Django專案的互動式環境,然後嘗試對模型的操作。
python manage.py shell
from polls.models import Subject
subject1 = Subject(name='Python全棧開發', intro='當下最熱門的學科', is_hot=True)
subject1.save()
subject2 = Subject(name='全棧軟體測試', intro='學習自動化測試的學科', is_hot=False)
subject2.save()
subject3 = Subject(name='JavaEE分散式開發', intro='基於Java語言的伺服器應用開發', is_hot=True)
subject = Subject.objects.get(no=2)
subject.delete()
subject = Subject.objects.get(no=1)
subject.name = 'Python全棧+人工智慧'
subject.save()
- 查詢所有物件。
Subjects.objects.all()
- 過濾資料。
# 查詢名稱為“Python全棧+人工智慧”的學科
Subject.objects.filter(name='Python全棧+人工智慧')
# 查詢名稱包含“全棧”的學科(模糊查詢)
Subject.objects.filter(name__contains='全棧')
Subject.objects.filter(name__startswith='全棧')
Subject.objects.filter(name__endswith='全棧')
# 查詢所有熱門學科
Subject.objects.filter(is_hot=True)
# 查詢編號大於3小於10的學科
Subject.objects.filter(no__gt=3).filter(no__lt=10)
Subject.objects.filter(no__gt=3, no__lt=10)
# 查詢編號在3到7之間的學科
Subject.objects.filter(no__ge=3, no__le=7)
Subject.objects.filter(no__range=(3, 7))
- 查詢單個物件。
# 查詢主鍵為1的學科
Subject.objects.get(pk=1)
Subject.objects.get(no=1)
Subject.objects.filter(no=1).first()
Subject.objects.filter(no=1).last()
- 排序。
# 查詢所有學科按編號升序排列
Subject.objects.order_by('no')
# 查詢所有部門按部門編號降序排列
Subject.objects.order_by('-no')
- 切片(分頁查詢)。
# 按編號從小到大查詢前3個學科
Subject.objects.order_by('no')[:3]
- 計數。
# 查詢一共有多少個學科
Subject.objects.count()
- 高階查詢。
# 查詢編號為1的學科的老師
Teacher.objects.filter(subject__no=1)
Subject.objects.get(pk=1).teacher_set.all()
# 查詢學科名稱有“全棧”二字的學科的老師
Teacher.objects.filter(subject__name__contains='全棧')
說明1:由於老師與學科之間存在多對一外來鍵關聯,所以能透過學科反向查詢到該學科的老師(從一對多關係中“一”的一方查詢“多”的一方),反向查詢屬性預設的名字是
類名小寫_set
(如上面例子中的teacher_set
),當然也可以在建立模型時透過ForeingKey
的related_name
屬性指定反向查詢屬性的名字。如果不希望執行反向查詢可以將related_name
屬性設定為'+'
或者以'+'
開頭的字串。
說明2:ORM查詢多個物件時會返回QuerySet物件,QuerySet使用了惰性查詢,即在建立QuerySet物件的過程中不涉及任何資料庫活動,等真正用到物件時(對QuerySet求值)才向資料庫傳送SQL語句並獲取對應的結果,這一點在實際開發中需要引起注意!
說明3:如果希望更新多條資料,不用先逐一獲取模型物件再修改物件屬性,可以直接使用QuerySet物件的
update()
方法一次性更新多條資料。
在建立好模型類之後,可以透過Django框架自帶的後臺管理應用(admin
應用)實現對模型的管理。雖然實際應用中,這個後臺可能並不能滿足我們的需求,但是在學習Django框架時,我們可以利用admin
應用來管理我們的模型,同時也透過它來了解一個專案的後臺管理系統需要哪些功能。使用Django自帶的admin
應用步驟如下所示。
-
將
admin
應用所需的表遷移到資料庫中。admin
應用本身也需要資料庫的支援,而且在admin
應用中已經定義好了相關的資料模型類,我們只需要透過模型遷移操作就能自動在資料庫中生成所需的二維表。python manage.py migrate
-
建立訪問
admin
應用的超級使用者賬號,這裡需要輸入使用者名稱、郵箱和口令。python manage.py createsuperuser
說明:輸入口令時沒有回顯也不能退格,需要一氣呵成完成輸入。
-
執行專案,在瀏覽器中訪問
http://127.0.0.1:8000/admin
,輸入剛才建立的超級使用者賬號和密碼進行登入。登入後進入管理員操作平臺。
注意,我們暫時還沒能在
admin
應用中看到之前建立的模型類,為此需要在polls
應用的admin.py
檔案中對需要管理的模型進行註冊。 -
註冊模型類。
from django.contrib import admin from polls.models import Subject, Teacher admin.site.register(Subject) admin.site.register(Teacher)
註冊模型類後,就可以在後臺管理系統中看到它們。
-
對模型進行CRUD操作。
可以在管理員平臺對模型進行C(新增)、R(檢視)、U(更新)、D(刪除)操作,如下圖所示。
-
註冊模型管理類。
可能大家已經注意到了,剛才在後臺檢視部門資訊的時候,顯示的部門資訊並不直觀,為此我們再修改
admin.py
檔案,透過註冊模型管理類,可以在後臺管理系統中更好的管理模型。from django.contrib import admin from polls.models import Subject, Teacher class SubjectModelAdmin(admin.ModelAdmin): list_display = ('no', 'name', 'intro', 'is_hot') search_fields = ('name', ) ordering = ('no', ) class TeacherModelAdmin(admin.ModelAdmin): list_display = ('no', 'name', 'sex', 'birth', 'good_count', 'bad_count', 'subject') search_fields = ('name', ) ordering = ('no', ) admin.site.register(Subject, SubjectModelAdmin) admin.site.register(Teacher, TeacherModelAdmin)
為了更好的檢視模型,我們為
Subject
類新增__str__
魔法方法,並在該方法中返回學科名字。這樣在如上圖所示的檢視老師的頁面上顯示老師所屬學科時,就不再是Subject object(1)
這樣晦澀的資訊,而是學科的名稱。
-
修改
polls/views.py
檔案,編寫檢視函式實現對學科頁和老師頁的渲染。from django.shortcuts import render, redirect from polls.models import Subject, Teacher def show_subjects(request): subjects = Subject.objects.all().order_by('no') return render(request, 'subjects.html', {'subjects': subjects}) def show_teachers(request): try: sno = int(request.GET.get('sno')) teachers = [] if sno: subject = Subject.objects.only('name').get(no=sno) teachers = Teacher.objects.filter(subject=subject).order_by('no') return render(request, 'teachers.html', { 'subject': subject, 'teachers': teachers }) except (ValueError, Subject.DoesNotExist): return redirect('/')
-
修改
templates/subjects.html
和templates/teachers.html
模板頁。subjects.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>學科資訊</title> <style> #container { width: 80%; margin: 10px auto; } .user { float: right; margin-right: 10px; } .user>a { margin-right: 10px; } #main>dl>dt { font-size: 1.5em; font-weight: bold; } #main>dl>dd { font-size: 1.2em; } a { text-decoration: none; color: darkcyan; } </style> </head> <body> <div id="container"> <div class="user"> <a href="login.html">使用者登入</a> <a href="register.html">快速註冊</a> </div> <h1>扣丁學堂所有學科</h1> <hr> <div id="main"> {% for subject in subjects %} <dl> <dt> <a href="/teachers/?sno={{ subject.no }}">{{ subject.name }}</a> {% if subject.is_hot %} <img src="/static/images/hot-icon-small.png"> {% endif %} </dt> <dd>{{ subject.intro }}</dd> </dl> {% endfor %} </div> </div> </body> </html>
teachers.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>老師資訊</title> <style> #container { width: 80%; margin: 10px auto; } .teacher { width: 100%; margin: 0 auto; padding: 10px 0; border-bottom: 1px dashed gray; overflow: auto; } .teacher>div { float: left; } .photo { height: 140px; border-radius: 75px; overflow: hidden; margin-left: 20px; } .info { width: 75%; margin-left: 30px; } .info div { clear: both; margin: 5px 10px; } .info span { margin-right: 25px; } .info a { text-decoration: none; color: darkcyan; } </style> </head> <body> <div id="container"> <h1>{{ subject.name }}學科的老師資訊</h1> <hr> {% if not teachers %} <h2>暫無該學科老師資訊</h2> {% endif %} {% for teacher in teachers %} <div class="teacher"> <div class="photo"> <img src="/static/images/{{ teacher.photo }}" height="140" alt=""> </div> <div class="info"> <div> <span><strong>姓名:{{ teacher.name }}</strong></span> <span>性別:{{ teacher.sex | yesno:'男,女' }}</span> <span>出生日期:{{ teacher.birth | date:'Y年n月j日'}}</span> </div> <div class="intro">{{ teacher.intro }}</div> <div class="comment"> <a href="">好評</a> (<strong>{{ teacher.good_count }}</strong>) <a href="">差評</a> <strong>{{ teacher.bad_count }}</strong>) </div> </div> </div> {% endfor %} <a href="/">返回首頁</a> </div> </body> </html>
-
修改
vote/urls.py
檔案,實現對映URL。from django.contrib import admin from django.urls import path from polls.views import show_subjects, show_teachers urlpatterns = [ path('admin/', admin.site.urls), path('', show_subjects), path('teachers/', show_teachers), ]
到此為止,頁面上需要的圖片(靜態資源)還沒有能夠正常展示,我們在下一章節中為大家介紹如何處理模板頁上的需要的靜態資源。
- 正確的為模型和關係欄位命名。
- 設定適當的
related_name
屬性。 - 用
OneToOneField
代替ForeignKeyField(unique=True)
。 - 透過“遷移操作”(migrate)來新增模型。
- 用NoSQL來應對需要降低正規化級別的場景。
- 如果布林型別可以為空要使用
NullBooleanField
。 - 在模型中放置業務邏輯。
- 用
<ModelName>.DoesNotExists
取代ObjectDoesNotExists
。 - 在資料庫中不要出現無效資料。
- 不要對
QuerySet
呼叫len()
函式。 - 將
QuerySet
的exists()
方法的返回值用於if
條件。 - 用
DecimalField
來儲存貨幣相關資料而不是FloatField
。 - 定義
__str__
方法。 - 不要將資料檔案放在同一個目錄中。
說明:以上內容來自於STEELKIWI網站的Best Practice working with Django models in Python,有興趣的小夥伴可以閱讀原文。
對欄位名稱的限制
- 欄位名不能是Python的保留字,否則會導致語法錯誤
- 欄位名不能有多個連續下劃線,否則影響ORM查詢操作
Django模型欄位類
欄位類 | 說明 |
---|---|
AutoField |
自增ID欄位 |
BigIntegerField |
64位有符號整數 |
BinaryField |
儲存二進位制資料的欄位,對應Python的bytes 型別 |
BooleanField |
儲存True 或False |
CharField |
長度較小的字串 |
DateField |
儲存日期,有auto_now 和auto_now_add 屬性 |
DateTimeField |
儲存日期和日期,兩個附加屬性同上 |
DecimalField |
儲存固定精度小數,有max_digits (有效位數)和decimal_places (小數點後面)兩個必要的引數 |
DurationField |
儲存時間跨度 |
EmailField |
與CharField 相同,可以用EmailValidator 驗證 |
FileField |
檔案上傳欄位 |
FloatField |
儲存浮點數 |
ImageField |
其他同FileFiled ,要驗證上傳的是不是有效影象 |
IntegerField |
儲存32位有符號整數。 |
GenericIPAddressField |
儲存IPv4或IPv6地址 |
NullBooleanField |
儲存True 、False 或null 值 |
PositiveIntegerField |
儲存無符號整數(只能儲存正數) |
SlugField |
儲存slug(簡短標註) |
SmallIntegerField |
儲存16位有符號整數 |
TextField |
儲存資料量較大的文字 |
TimeField |
儲存時間 |
URLField |
儲存URL的CharField |
UUIDField |
儲存全域性唯一識別符號 |
通用欄位屬性
選項 | 說明 |
---|---|
null |
資料庫中對應的欄位是否允許為NULL ,預設為False |
blank |
後臺模型管理驗證資料時,是否允許為NULL ,預設為False |
choices |
設定欄位的選項,各元組中的第一個值是設定在模型上的值,第二值是人類可讀的值 |
db_column |
欄位對應到資料庫表中的列名,未指定時直接使用欄位的名稱 |
db_index |
設定為True 時將在該欄位建立索引 |
db_tablespace |
為有索引的欄位設定使用的表空間,預設為DEFAULT_INDEX_TABLESPACE |
default |
欄位的預設值 |
editable |
欄位在後臺模型管理或ModelForm 中是否顯示,預設為True |
error_messages |
設定欄位丟擲異常時的預設訊息的字典,其中的鍵包括null 、blank 、invalid 、invalid_choice 、unique 和unique_for_date |
help_text |
表單小元件旁邊顯示的額外的幫助文字。 |
primary_key |
將欄位指定為模型的主鍵,未指定時會自動新增AutoField 用於主鍵,只讀。 |
unique |
設定為True 時,表中欄位的值必須是唯一的 |
verbose_name |
欄位在後臺模型管理顯示的名稱,未指定時使用欄位的名稱 |
ForeignKey
屬性
limit_choices_to
:值是一個Q物件或返回一個Q物件,用於限制後臺顯示哪些物件。related_name
:用於獲取關聯物件的關聯管理器物件(反向查詢),如果不允許反向,該屬性應該被設定為'+'
,或者以'+'
結尾。to_field
:指定關聯的欄位,預設關聯物件的主鍵欄位。db_constraint
:是否為外來鍵建立約束,預設值為True
。on_delete
:外來鍵關聯的物件被刪除時對應的動作,可取的值包括django.db.models
中定義的:CASCADE
:級聯刪除。PROTECT
:丟擲ProtectedError
異常,阻止刪除引用的物件。SET_NULL
:把外來鍵設定為null
,當null
屬性被設定為True
時才能這麼做。SET_DEFAULT
:把外來鍵設定為預設值,提供了預設值才能這麼做。
ManyToManyField
屬性
symmetrical
:是否建立對稱的多對多關係。through
:指定維持多對多關係的中間表的Django模型。throughfields
:定義了中間模型時可以指定建立多對多關係的欄位。db_table
:指定維持多對多關係的中間表的表名。
選項 | 說明 |
---|---|
abstract |
設定為True時模型是抽象父類 |
app_label |
如果定義模型的應用不在INSTALLED_APPS中可以用該屬性指定 |
db_table |
模型使用的資料表名稱 |
db_tablespace |
模型使用的資料表空間 |
default_related_name |
關聯物件回指這個模型時預設使用的名稱,預設為<model_name>_set |
get_latest_by |
模型中可排序欄位的名稱。 |
managed |
設定為True時,Django在遷移中建立資料表並在執行flush管理命令時把表移除 |
order_with_respect_to |
標記物件為可排序的 |
ordering |
物件的預設排序 |
permissions |
建立物件時寫入許可權表的額外許可權 |
default_permissions |
預設為('add', 'change', 'delete') |
unique_together |
設定組合在一起時必須獨一無二的欄位名 |
index_together |
設定一起建立索引的多個欄位名 |
verbose_name |
為物件設定人類可讀的名稱 |
verbose_name_plural |
設定物件的複數名稱 |
exact
/iexact
:精確匹配/忽略大小寫的精確匹配查詢contains
/icontains
/startswith
/istartswith
/endswith
/iendswith
:基於like
的模糊查詢in
:集合運算gt
/gte
/lt
/lte
:大於/大於等於/小於/小於等於關係運算range
:指定範圍查詢(SQL中的between…and…
)year
/month
/day
/week_day
/hour
/minute
/second
:查詢時間日期isnull
:查詢空值(True)或非空值(False)search
:基於全文索引的全文檢索(一般很少使用)regex
/iregex
:基於正則表示式的模糊匹配查詢