作者:Hubery 時間:2018.8.31
接上文: Django2 web實戰01-啟動專案
給專案新增Person和model關係
我們將給專案新增model間的關係。movie中的人物關係可以構成一個很複雜的資料模型。 同一個人,可以是演員actor,編劇writer,導演director等角色。即使脫離劇組和創作團隊,簡化一點, 資料模型也將包括 通過ForiengKey欄位形成的一對多的關係, 通過nyToManyField欄位形成的多對多的關係; 通過在ManyToManyField中使用through類來形成的多對多的關係,用來新增額外資訊;
順勢,
- 建立一個Person模型
- 新增一個ForeignKey欄位,從Movie到Person,追蹤導演director
- 新增一個ManyToMany欄位,從Movie到Person,追蹤編劇writers
- 新增一個ManyToMany欄位,通過一個through類(演員Actor)來追蹤,誰在電影中演出以及什麼角色。
- 改動model之後,進行資料庫遷移
- 往電影movie詳情模版中新增導演director,編劇writer,演員actors
- 新增一個PersonDetail檢視,顯示出電影是誰導演的,誰編劇的,以及誰出演的。
新增一個關係模型
我們需要一個Person類,來描述和儲存電影中的人物person 編輯core/models.py
from django.db import models
class Movie(models.Model):
NOT_RATED = 0
RATED_G = 1
RATED_PG = 2
RATED_R = 3
RATINGS = (
(NOT_RATED, 'NR - 沒有評分'),
(RATED_G, 'G - 普通觀眾'),
(RATED_PG, 'PG - 父母的引導和規範'),
(RATED_R, 'R - 限制級'),
)
title = models.CharField(max_length=140)
plot = models.TextField()
year = models.PositiveIntegerField()
rating = models.IntegerField(
choices=RATINGS,
default=NOT_RATED
)
runtime = models.PositiveIntegerField()
website = models.URLField(blank=True)
class Meta:
ordering = ('-year', 'title')
def __str__(self):
return '{} ({})'.format(self.title, self.year)
class Person(models.Model):
first_name = models.CharField(max_length=140)
last_name = models.CharField(max_length=140)
born = models.DateField()
died = models.DateField(null=True, blank=True)
class Meta:
ordering = (
'last_name', 'first_name'
)
def __str__(self):
if self.died:
return '{}, {} ({}-{})'.format(
self.last_name,
self.first_name,
self.born,
self.died)
return '{}, {} ({})'.format(
self.last_name,
self.first_name,
self.born)
複製程式碼
Person中,有DataField
欄位,用適當的資料庫列型別來記錄時間型別資料。
所有欄位都支援null引數,表示列欄位是否應該支援NULL型別的值。
died欄位設定了null=True,表明我們可以記錄此person是否died。
__str__
方法類似java的toString(),方便輸出物件內容。
現在,Person與Movies 有著各種各樣的關係。
不同型別的關係欄位
Django的ORM支援對映模型之間關係的欄位,包括一對多
,多對多
,以及包含內部模型的多對多
。
當兩個model間是一對多
關係, 用ForeignKey
欄位,該欄位可以生成一個兩資料庫表之間的約束。
若model中沒有ForeignKey欄位,Django會自動新增一個RelatedManager物件,作為屬性例項。
RelatedManager類使得查詢關係物件更簡單。
當兩個model間是多對多關係,兩者都可以使用ManyToManyField();Django會在另一方給你建立一個RelatedManager
。
你知道,關係型資料庫在兩表之間不能有多對多的關係。關係型資料庫需要一個擁有外來鍵的bridging橋接表,來訪問相關的表。假設我們不想新增任何屬性來描述這個關係,Django會自動建立和管理這個橋接表。
有時候我們想用額外的欄位
來描述一段多對多的關係,我們可以用包含through
模型的欄位:ManyToManyField(在UML中稱為association聯絡)。這個模型有一個ForeignKey,可以到達關係的每一邊,以及獲取任何想獲取的額外欄位。
模型關係 | 對應欄位 |
---|---|
一對多 | ForeignKey |
多對多 | ManyToManyField |
有內部模型的多對多 | ManyToManyField中使用through欄位 藉助其他模型描述模型關係 |
那,可以開始建立模型了,仔細體驗下之間的關係。
Director - ForeignKey
模型中,每個movie都有一個director導演,但每個導演可以拍過很多作品。那麼,用ForeignKey欄位來為movie新增一個導演director; 編輯core/models.py Movie類中新增這一段
director = models.ForeignKey(
to='Person',
related_name='directed',
on_delete=models.SET_NULL,
null=True,
blank=True
)
複製程式碼
- to='Person', Django的所有關係欄位,都可以使用字串引用以及相關模型的引用。這個引數是必須的。
- on_delete=models.SET_NULL,當刪除引用的模型(例項/行)時,Django需要知道接下來要執行什麼指令。SET_NULL,將會設定所有Movie例項當刪除Person時,director欄位設定為NULL。如果想級聯刪除,那麼用這個物件: models.CASCADE。
- related_name='directed', 可選引數,表明這是
其他model的RelatedManager例項的名字
。這個表示:讓我們查詢這個人所拍過的所有Movie例項。 如果沒有這個引數,那麼Person會有個叫movie_set的屬性。我們將會獲得多個不同的關係,Movie和Person(writer/director/actors),所以movie_set會變得隱晦不清,因此我們必須提供一個related_name
。
這是首次向已經存在的model中新增欄位。我們必須設定null=True或者提供一個預設引數。如果沒有,Django在執行migration的時候會強制我們這麼做。因為,當我們做資料庫遷移的時候,Django必須確保這個例項在資料庫表中存在。當資料庫新增這個欄位後,需要知道插入已經存在的行rows的資料是啥。上面程式碼中的director欄位,我們可以接受NULL值。
我們已經向Movie模型中新增了一個欄位director,向Person例項中新增了一個叫directed的屬性。這個directed是RelatedManager
型別。
RelatedManager是個很有用的類,類似於模型的預設Manager,objects,可以跨表自動處理之間的關係。相當於一個模型持有了另一個模型的引用/控制程式碼,可以操作另一個模型。
對比一下:
person.directed.create()
複製程式碼
Movie.objects.create()
複製程式碼
這倆方法都會建立一個Movie,但person.directed.create()會確保新Movie作為director.RelatedManager,同時還提供了add和remove方法,以便我們可以通過呼叫person.directed.add(movie)將一個Movie新增到一個directed的Person集合。 相同的,remove()方法差不多意思,會從關係中移除一個model。
Writers - ManyToManyField
兩個模型之間可能存在多對多的關係。比如,一個人可以寫多個movies,同樣一個movie可以有多個人來寫完。 向Movie模型中新增一個writers欄位,處理多對多: core/models.py
writers = models.ManyToManyField(
to='Person',
related_name='writing_credits',
blank=True
)
複製程式碼
一個ManyToManyField建立了一個多對多的關係,充當了RelatedManager角色,保證了使用者查詢和建立model。
再次用到了related_name,避免給Person一個movie_set屬性,直接給賦值一個writing_credits
屬性充當
一個RelatedManager
,否則可能會造成混亂。
上面程式碼中,兩端都有RelatedManager,所以這倆操作等效:
person.writing_credits.add(movie)
複製程式碼
movie.writers.add(person)
複製程式碼
Role - ManyToManyField 用一個through類
當我們想用內部模型
來描述兩個具有多對多關係的模型之間的關係時,這種型別就派上用場了。
Django允許我們通過建立一個模型來實現這一點,該模型描述了多對多關係中兩個模型之間的連線表
join table。
新增一個Role中間類,與Movie平級; Movie中新增一個actors
actors = models.ManyToManyField(
to='Person',
through='Role',
related_name='acting_credits',
blank=True
)
複製程式碼
class Role(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.DO_NOTHING)
person = models.ForeignKey(Person, on_delete=models.DO_NOTHING)
name = models.CharField(max_length=140)
class Meta:
unique_together = ('movie', 'person', 'name')
def __str__(self):
return "{} {} {}".format(self.movie_id, self.person_id, self.name)
複製程式碼
這看起來很像之前的ManyToManyField,除了我們同時設定了to和through引數。 Role模型看起來非常像是要涉及一個連線表,join table;有著與每一個表的多對多關係。同時還有個name欄位用來描述Role。
Role有一個獨一無二的約束。需要movie/person/name一起組成,在Role的內部類Meta上設定unique_together屬性。
此時,ManyToManyField會建立4個新的RelatedManager例項;
- movie.actors 是與Person相關的manager
- person.acting_credits 是與Movie相關的manager
- movie.role_set 是與Role相關的manager
- person.role_set 是與Role相關的manager
你可以用上面任意一個manager來查詢model,不過只有role_set managers才能建立model或者修改模型關係,因為role_set有內部類。如果你嘗試著執行movie.actors.add(person),Django會拋IntegrityError
異常,因為沒有任何方式可以給Role.name欄位賦值。然而,你可以這樣:movie.role_set.add(person=person, name='hubery')。
新增資料庫遷移
python manage.py makemigrations
python manage.py migrates
複製程式碼
接下來,我們就可以讓movie頁和movie中的人物相關聯。
建立PersonView 更新MovieList
新增一個PersonDetail檢視,使得movie_detail.html可以連結到。 為了建立該檢視,可以用4步來完成:
- 建立一個manager,限制資料庫查詢次數
- 建立view
- 建立template
- 建立URL來關聯view
建立一個自定義manager - PersonManager
PersonDetail檢視會列出 一個人在表演,寫作,或導演過的所有電影。在template中,我們會列印出每個演員表中的每部電影的名稱。 為了避免資料庫出現大量查詢,給models建立新的managers,返回QuerySets。 Django中,每次從一個關係中訪問一個屬性,Django都會通過查詢資料庫來獲取相關的資料項。 在一個Person出現在N個movies中的情況下,會出現N次資料庫查詢。我們可以通過prefetch_related()方法來避免這種情況。通過prefetch_related()方法,Django將會通過一個額外的查詢來獲取單個關係中的相關資料。 這其中有個問題,如果我們最終沒有使用預處理的資料,那麼這次查詢會浪費時間和記憶體。
接下來,建立一個PersonManager,有個新方法all_with_prefetch_movies(),讓PersonManager成為Person的預設manager: core/models.py
class PersonManager(models.Manager):
def all_with_prefetch_movies(self):
qs = self.get_queryset()
return qs.prefetch_related(
'directed',
'writing_credits',
'role_set__movie')
class Person(models.Model): objects = PersonManager()
複製程式碼
PersonManager提供了預設manager同樣的方法,應為其繼承自models.Manager。定義了一個新方法,用get_queryset()來獲取QuerySet,通知它來預獲取相關的model。 QuerySets是惰性的,在評估查詢集之前,不會與資料庫進行互動。
DetailView通過PK呼叫get()方法獲取model之前不會評估查詢。
prefetch_related()方法需要一次或多次查詢,查詢初始化後,會自動查詢相關models。當你從相關的QuerySet中查詢model時,Django不會去查詢它,因為它已經在QuerySet中。
一次查詢是一個Django的QuerySet用來表述模型中的欄位或RelatedManager。甚至可以通過將關係欄位或RelatedManager的名稱與相關模型欄位分割為兩個下劃線,實現跨越關係:
Movie.objects.all().filter(actors__last_name='Freeman',
actors__first_name='Morgan')
複製程式碼
這個呼叫會返回一個QuerySet,包含演員Morgan Freeman的所有Movie模型例項。
建立一個PersonDetail檢視和template
現在寫一個非常簡單的檢視, core/views.py
class PersonDetail(DetailView):
queryset = Person.objects.all_with_prefetch_movies()
複製程式碼
DetailView比較特殊,沒有提供mode屬性。 相反,我們給他傳入一個PersonManager類的QuerySet物件。當DetailView用filter()方法和get()方法來獲取model例項時,DetailView會從model的例項類名中,派生出模版template的名稱,就像我們在模型類中提供了模型類作為屬性一樣。
那, 建立一個template: core/template/core/person_detail.html
{% extends 'base.html' %}
{% block title %}
{{ object.first_name }}
{{ object.last_name }}
{% endblock %}
{% block main %}
<h1>{{ object }}</h1>
<h2>Actor</h2>
<ul>
{% for role in object.role_set.all %}
<li>
<a href="{% url 'core:MovieDetail' role.movie.id %}">
{{ role.movie }}
</a>
{{ role.name }}
</li>
{% endfor %}
</ul>
<h2>Writer</h2>
<ul>
{% for movie in object.writing_credits.all %}
<li>
<a href="{% url 'core:MovieDetail' movie.id %}">
{{ movie }}
</a>
</li>
{% endfor %}
</ul>
<h2>Director</h2>
<ul>
{% for movie in object.directed.all %}
<li>
<a href="{% url 'core:MovieDetail' movie.id %}">
{{ movie }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
複製程式碼
template 不需要特意做啥就能用預處理資料。
建立MovieManager
class MovieManager(models.Manager):
def all_with_related_persons(self):
qs = self.get_queryset()
qs = qs.select_related(
'director')
qs.prefetch_related(
'writers', 'actors')
return qs
複製程式碼
設定預設manager
class Movie(models.Model):
objects = MovieManager()
複製程式碼
MovieManager介紹了另外一個方法:select_related。與prefetch_related()方法很像,但只有一個關係模型存在時採用select_related ,比如只有一個ForeignKey欄位。
QuerySet方法 | 說明 |
---|---|
prefetch_related() | 當關系可能涉及多個模型時採用,如只有一個ForeignKEy |
select_related() | 只有一個關係模型存在時採用,如有MaynToMany或RelatedManager |
現在,可以直接使用查詢結果來更新MovieDetail,而不是直接使用model:
class MovieDetail(DetailView):
queryset = (
Movie.objects.all_with_related_persons())
複製程式碼
小結
建立Person模型,並在Movie和Person之間建立了許多種關係。 ForeignKey 建立一對多的關係; ManyToManyField 建立多對多關係; ManyToManyField 中通過提供through模型來建立多對多關係,用中介類來給多對多關係提供額外的資訊;
建立一個PersonDetail檢視來顯示Person模型例項;用一個自定義模型manager來控制資料庫的查詢次數。
天星技術團QQ:557247785
。