模型¶
模型是關於您的資料的單一、明確的資訊來源。它包含您所儲存資料的基本欄位和行為。一般來說,每個模型都會對應到單一的資料庫表格。
基本概念
每個模型都是一個 Python 類別,它繼承了
django.db.models.Model
。模型的每個屬性都代表一個資料庫欄位。
有了這些,Django 會為您提供自動產生的資料庫存取 API;請參閱建立查詢。
快速範例¶
此範例模型定義了一個 Person
,它具有 first_name
和 last_name
。
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
first_name
和 last_name
是模型的欄位。每個欄位都指定為類別屬性,並且每個屬性都會對應到一個資料庫欄位。
上面的 Person
模型將會建立一個如下的資料庫表格
CREATE TABLE myapp_person (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"first_name" varchar(30) NOT NULL,
"last_name" varchar(30) NOT NULL
);
一些技術註解
使用模型¶
一旦您定義了您的模型,您需要告訴 Django 您將使用這些模型。透過編輯您的設定檔並變更 INSTALLED_APPS
設定,加入包含您的 models.py
的模組名稱來完成此操作。
例如,如果您的應用程式的模型位於 myapp.models
模組中(由 manage.py startapp
指令碼為應用程式建立的套件結構),INSTALLED_APPS
應該讀取如下內容,部分內容如下:
INSTALLED_APPS = [
# ...
"myapp",
# ...
]
當您將新的應用程式加入到 INSTALLED_APPS
時,請務必執行 manage.py migrate
,您可以選擇先使用 manage.py makemigrations
來為它們建立遷移。
欄位¶
模型中最重要的部分 – 也是模型中唯一需要的部分 – 是它定義的資料庫欄位清單。欄位由類別屬性指定。請小心不要選擇與 模型 API 衝突的欄位名稱,例如 clean
、save
或 delete
。
範例
from django.db import models
class Musician(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
instrument = models.CharField(max_length=100)
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_date = models.DateField()
num_stars = models.IntegerField()
欄位類型¶
您模型中的每個欄位都應該是適當的 Field
類別的實例。Django 使用欄位類別類型來判斷幾件事
欄位類型,這會告訴資料庫要儲存什麼類型的資料(例如,
INTEGER
、VARCHAR
、TEXT
)。在呈現表單欄位時要使用的預設 HTML widget (例如,
<input type="text">
、<select>
)。最小驗證需求,用於 Django 管理介面和自動產生的表單中。
Django 附帶了數十種內建欄位類型;您可以在模型欄位參考中找到完整的清單。如果 Django 的內建欄位無法滿足您的需求,您可以輕鬆撰寫自己的欄位;請參閱如何建立自訂模型欄位。
欄位選項¶
每個欄位都接受一組特定的欄位特定引數(記錄在模型欄位參考中)。例如,CharField
(及其子類別)需要一個 max_length
引數,該引數指定用於儲存資料的 VARCHAR
資料庫欄位的大小。
還有一個所有欄位類型都可以使用的通用引數集。全部都是可選的。它們在參考中有完整的說明,但這裡快速總結一下最常用的引數
null
如果為
True
,Django 會將空值儲存為資料庫中的NULL
。預設值為False
。blank
如果為
True
,則允許欄位為空白。預設值為False
。請注意,這與
null
不同。null
純粹與資料庫相關,而blank
則與驗證相關。如果欄位具有blank=True
,表單驗證將允許輸入空值。如果欄位具有blank=False
,則欄位將為必填。choices
一個 2 值元組的序列、一個對應、一個列舉類型,或一個可呼叫的(不期望任何引數並傳回先前任何格式)函數,用於作為此欄位的選項。如果給定此選項,預設的表單 widget 將會是一個選取方塊,而不是標準的文字欄位,並且會將選項限制為給定的選項。
選項清單如下所示
YEAR_IN_SCHOOL_CHOICES = [ ("FR", "Freshman"), ("SO", "Sophomore"), ("JR", "Junior"), ("SR", "Senior"), ("GR", "Graduate"), ]
注意
每次
choices
的順序變更時,都會建立新的遷移。每個元組中的第一個元素是將儲存在資料庫中的值。第二個元素會由欄位的表單 widget 顯示。
給定一個模型實例,可以使用
get_FOO_display()
方法存取具有choices
的欄位的顯示值。例如from django.db import models class Person(models.Model): SHIRT_SIZES = { "S": "Small", "M": "Medium", "L": "Large", } name = models.CharField(max_length=60) shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES)
>>> p = Person(name="Fred Flintstone", shirt_size="L") >>> p.save() >>> p.shirt_size 'L' >>> p.get_shirt_size_display() 'Large'
您也可以使用列舉類別以簡潔的方式定義
choices
from django.db import models class Runner(models.Model): MedalType = models.TextChoices("MedalType", "GOLD SILVER BRONZE") name = models.CharField(max_length=60) medal = models.CharField(blank=True, choices=MedalType, max_length=10)
在模型欄位參考中提供了更多範例。
在 Django 5.0 中變更新增了對對應和可呼叫的函數的支援。
default
欄位的預設值。這可以是值或可呼叫的物件。如果是可呼叫的,則每次建立新物件時都會呼叫它。
db_default
欄位的資料庫計算預設值。這可以是字面值或資料庫函數。
如果同時設定了
db_default
和Field.default
,則在 Python 程式碼中建立實例時,default
將優先。db_default
仍然會在資料庫層級設定,並且會在 ORM 之外插入列,或在遷移中新增新欄位時使用。help_text
要與表單 widget 一起顯示的額外「說明」文字。即使您的欄位未使用在表單上,它對於文件也很有用。
primary_key
如果為
True
,則此欄位為模型的主鍵。如果您未在模型的任何欄位中指定
primary_key=True
,Django 會自動新增一個IntegerField
來保存主鍵,因此您不需要在任何欄位上設定primary_key=True
,除非您想要覆寫預設的主鍵行為。更多資訊請參閱 自動主鍵欄位。主鍵欄位是唯讀的。如果您更改現有物件的主鍵值然後儲存,則會建立一個與舊物件並存的新物件。例如:
from django.db import models class Fruit(models.Model): name = models.CharField(max_length=100, primary_key=True)
>>> fruit = Fruit.objects.create(name="Apple") >>> fruit.name = "Pear" >>> fruit.save() >>> Fruit.objects.values_list("name", flat=True) <QuerySet ['Apple', 'Pear']>
唯一
如果為
True
,則此欄位在整個表格中必須是唯一的。
再次強調,這些只是最常見欄位選項的簡短描述。完整細節請見通用模型欄位選項參考。
自動主鍵欄位¶
預設情況下,Django 會為每個模型提供一個自動遞增的主鍵,其類型由每個應用程式在 AppConfig.default_auto_field
中指定,或在 DEFAULT_AUTO_FIELD
設定中全域指定。例如:
id = models.BigAutoField(primary_key=True)
如果您想指定自訂主鍵,請在您的其中一個欄位上指定 primary_key=True
。如果 Django 發現您已明確設定 Field.primary_key
,則它不會新增自動的 id
欄位。
每個模型都需要恰好一個欄位具有 primary_key=True
(明確宣告或自動新增)。
詳細欄位名稱¶
除了 ForeignKey
、ManyToManyField
和 OneToOneField
之外,每個欄位類型都接受一個可選的第一個位置參數 – 詳細名稱。如果未提供詳細名稱,Django 會自動使用欄位的屬性名稱建立它,並將底線轉換為空格。
在此範例中,詳細名稱為 "person's first name"
first_name = models.CharField("person's first name", max_length=30)
在此範例中,詳細名稱為 "first name"
first_name = models.CharField(max_length=30)
ForeignKey
、ManyToManyField
和 OneToOneField
要求第一個參數為模型類別,因此請使用 verbose_name
關鍵字參數。
poll = models.ForeignKey(
Poll,
on_delete=models.CASCADE,
verbose_name="the related poll",
)
sites = models.ManyToManyField(Site, verbose_name="list of sites")
place = models.OneToOneField(
Place,
on_delete=models.CASCADE,
verbose_name="related place",
)
慣例是不將 verbose_name
的第一個字母大寫。Django 會在需要時自動將第一個字母大寫。
關聯性¶
顯然,關聯式資料庫的威力在於將表格彼此關聯。Django 提供了定義三種最常見資料庫關聯類型的方法:多對一、多對多和一對一。
多對一關聯性¶
若要定義多對一關聯性,請使用 django.db.models.ForeignKey
。您可以使用它,就像使用任何其他 Field
類型一樣:將其作為模型的類別屬性包含在內。
ForeignKey
需要一個位置參數:模型關聯的類別。
例如,如果 Car
模型有一個 Manufacturer
- 也就是說,一個 Manufacturer
製造多輛汽車,但每輛 Car
只有一個 Manufacturer
- 請使用以下定義
from django.db import models
class Manufacturer(models.Model):
# ...
pass
class Car(models.Model):
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
# ...
您也可以建立 遞迴關聯(具有與自身多對一關係的物件)和 與尚未定義的模型之間的關聯;詳細資訊請參閱 模型欄位參考。
建議但不強制要求 ForeignKey
欄位(上述範例中的 manufacturer
)的名稱是模型的名稱(小寫)。您可以隨意命名欄位。例如:
class Car(models.Model):
company_that_makes_it = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
)
# ...
參見
ForeignKey
欄位接受許多額外的參數,這些參數在 模型欄位參考 中有說明。這些選項有助於定義關係應如何運作;所有選項都是可選的。
有關存取反向關聯物件的詳細資訊,請參閱 反向追蹤關聯物件範例。
範例程式碼請參閱 多對一關係模型範例。
多對多關聯性¶
若要定義多對多關聯性,請使用 ManyToManyField
。您可以使用它,就像使用任何其他 Field
類型一樣:將其作為模型的類別屬性包含在內。
ManyToManyField
需要一個位置參數:模型關聯的類別。
例如,如果一個 Pizza
具有多個 Topping
物件 – 也就是說,一個 Topping
可以出現在多個披薩上,並且每個 Pizza
都有多個配料 – 以下是如何表示它:
from django.db import models
class Topping(models.Model):
# ...
pass
class Pizza(models.Model):
# ...
toppings = models.ManyToManyField(Topping)
與 ForeignKey
一樣,您也可以建立 遞迴關聯(具有與自身多對多關係的物件)和 與尚未定義的模型之間的關聯。
建議但不強制要求 ManyToManyField
(上述範例中的 toppings
)的名稱是描述相關模型物件集合的複數。
哪個模型具有 ManyToManyField
並不重要,但您應該只將其放在其中一個模型中 - 而不是兩個模型中。
一般來說,ManyToManyField
實例應該放在表單上將要編輯的物件中。在上面的範例中,toppings
在 Pizza
中(而不是 Topping
具有 pizzas
ManyToManyField
),因為認為披薩有多種配料比認為配料出現在多個披薩上更自然。以它上面的設置方式,Pizza
表單會讓使用者選擇配料。
參見
如需完整範例,請參閱多對多關係模型範例。
ManyToManyField
欄位也接受許多額外的參數,這些參數在 模型欄位參考 中有說明。這些選項有助於定義關係應如何運作;所有選項都是可選的。
多對多關係的額外欄位¶
當您只處理多對多關係(例如混合搭配披薩和配料)時,標準的 ManyToManyField
就是您所需要的。但是,有時您可能需要將資料與兩個模型之間的關聯建立關聯。
舉例來說,考慮一個追蹤音樂家所屬樂團的應用程式。一個人與他所屬的樂團之間存在多對多關係,因此您可以使用 ManyToManyField
來表示這種關係。但是,您可能想收集有關成員資格的許多細節,例如一個人加入樂團的日期。
對於這些情況,Django 允許您指定將用於管理多對多關係的模型。然後,您可以將額外的欄位放在中間模型上。中間模型透過使用 ManyToManyField
的 through
參數,指向將作為中介的模型。對於我們的音樂家範例,程式碼看起來會像這樣:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through="Membership")
def __str__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
當您設定中間模型時,您會明確指定到參與多對多關係的模型的外鍵。此明確宣告定義了兩個模型之間的關聯方式。
中間模型有一些限制:
您的中間模型必須包含一個 - 而且 *只能* 有一個 - 到來源模型的外鍵(在我們的範例中,這會是
Group
),或者您必須使用ManyToManyField.through_fields
明確指定 Django 應該用於關係的外鍵。如果您有多個外鍵,且未指定through_fields
,則會引發驗證錯誤。類似的限制也適用於目標模型的外鍵(在我們的範例中,這會是Person
)。對於透過中間模型與自身存在多對多關係的模型,允許使用到同一個模型的兩個外鍵,但它們將被視為多對多關係的兩個(不同)端。但是,如果外鍵 *超過* 兩個,則您也必須如上所述指定
through_fields
,否則將引發驗證錯誤。
現在您已經設定了您的 ManyToManyField
以使用您的中間模型(在此範例中為 Membership
),您就可以開始建立一些多對多關係了。您需要透過建立中間模型的實例來執行此操作。
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(
... person=ringo,
... group=beatles,
... date_joined=date(1962, 8, 16),
... invite_reason="Needed a new drummer.",
... )
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(
... person=paul,
... group=beatles,
... date_joined=date(1960, 8, 1),
... invite_reason="Wanted to form a band.",
... )
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
您也可以使用 add()
、 create()
或 set()
來建立關係,只要您為任何必需欄位指定 through_defaults
即可。
>>> beatles.members.add(john, through_defaults={"date_joined": date(1960, 8, 1)})
>>> beatles.members.create(
... name="George Harrison", through_defaults={"date_joined": date(1960, 8, 1)}
... )
>>> beatles.members.set(
... [john, paul, ringo, george], through_defaults={"date_joined": date(1960, 8, 1)}
... )
您可能更喜歡直接建立中間模型的實例。
如果中間模型定義的自訂 through 資料表未在 (model1, model2)
配對上強制唯一性,而允許多個值,則 remove()
呼叫將會移除所有中間模型實例。
>>> Membership.objects.create(
... person=ringo,
... group=beatles,
... date_joined=date(1968, 9, 4),
... invite_reason="You've been gone for a month and we miss you.",
... )
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
>>> # This deletes both of the intermediate model instances for Ringo Starr
>>> beatles.members.remove(ringo)
>>> beatles.members.all()
<QuerySet [<Person: Paul McCartney>]>
可以使用 clear()
方法來移除實例的所有多對多關係。
>>> # Beatles have broken up
>>> beatles.members.clear()
>>> # Note that this deletes the intermediate model instances
>>> Membership.objects.all()
<QuerySet []>
建立多對多關係後,您可以發出查詢。就像普通的多對多關係一樣,您可以使用多對多相關模型的屬性進行查詢。
# Find all the groups with a member whose name starts with 'Paul'
>>> Group.objects.filter(members__name__startswith="Paul")
<QuerySet [<Group: The Beatles>]>
當您使用中間模型時,您也可以查詢其屬性。
# Find all the members of the Beatles that joined after 1 Jan 1961
>>> Person.objects.filter(
... group__name="The Beatles", membership__date_joined__gt=date(1961, 1, 1)
... )
<QuerySet [<Person: Ringo Starr]>
如果您需要存取成員資格的資訊,您可以直接查詢 Membership
模型來執行此操作。
>>> ringos_membership = Membership.objects.get(group=beatles, person=ringo)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'
存取相同資訊的另一種方式是從 Person
物件查詢 多對多反向關係。
>>> ringos_membership = ringo.membership_set.get(group=beatles)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'
一對一關係¶
若要定義一對一關係,請使用 OneToOneField
。您像使用任何其他 Field
類型一樣使用它:透過將其作為模型的類別屬性包含進去。
當物件以某種方式「延伸」另一個物件時,這在物件的主鍵上最有用。
OneToOneField
需要一個位置引數:與模型相關的類別。
例如,如果您要建置「地點」的資料庫,您會在資料庫中建置相當標準的東西,例如地址、電話號碼等。然後,如果您想在地點的基礎上建置餐廳資料庫,而不是重複自己並在 Restaurant
模型中複製這些欄位,您可以讓 Restaurant
具有到 Place
的 OneToOneField
(因為餐廳「是一個」地點;實際上,為了處理這個問題,您通常會使用 繼承,其中涉及隱式的一對一關係)。
與 ForeignKey
一樣,可以定義 遞迴關係,並且可以建立 對尚未定義模型的參考。
參見
有關完整範例,請參閱 一對一關係模型範例。
OneToOneField
欄位也接受可選的 parent_link
引數。
OneToOneField
類別過去會自動成為模型上的主鍵。現在不再是這樣了(儘管您可以根據需要手動傳入 primary_key
引數)。因此,現在有可能在單一模型上有多個 OneToOneField
類型的欄位。
跨檔案的模型¶
將模型與另一個應用程式中的模型關聯完全沒問題。若要執行此操作,請在定義模型之檔案的頂端匯入相關模型。然後,在任何需要的地方參照其他模型類別。例如:
from django.db import models
from geography.models import ZipCode
class Restaurant(models.Model):
# ...
zip_code = models.ForeignKey(
ZipCode,
on_delete=models.SET_NULL,
blank=True,
null=True,
)
欄位名稱限制¶
Django 對模型欄位名稱施加了一些限制:
欄位名稱不能是 Python 保留字,因為這會導致 Python 語法錯誤。例如:
class Example(models.Model): pass = models.IntegerField() # 'pass' is a reserved word!
欄位名稱不能連續包含多個底線,因為這是 Django 查詢查找語法的運作方式。例如:
class Example(models.Model): foo__bar = models.IntegerField() # 'foo__bar' has two underscores!
由於類似的原因,欄位名稱不能以底線結尾。
不過,這些限制可以解決,因為您的欄位名稱不一定必須與您的資料庫資料行名稱相符。請參閱 db_column
選項。
SQL 保留字(例如 join
、 where
或 select
)*允許* 作為模型欄位名稱,因為 Django 會在每個基礎 SQL 查詢中逸出所有資料庫資料表名稱和資料行名稱。它使用您特定資料庫引擎的引用語法。
自訂欄位類型¶
如果現有的模型欄位之一無法用於滿足您的目的,或者如果您希望利用一些較不常見的資料庫資料行類型,您可以建立自己的欄位類別。建立您自己的欄位的完整涵蓋範圍,請參閱 如何建立自訂模型欄位。
Meta
選項¶
使用內部 class Meta
給您的模型中繼資料,如下所示:
from django.db import models
class Ox(models.Model):
horn_length = models.IntegerField()
class Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"
模型元數據是「任何非欄位的東西」,例如排序選項 (ordering
)、資料庫表格名稱 (db_table
),或人類可讀的單數和複數名稱 (verbose_name
和 verbose_name_plural
)。這些都不是必需的,而且將 class Meta
添加到模型是完全可選的。
所有可能的 Meta
選項的完整列表可以在 模型選項參考 中找到。
模型屬性¶
模型方法¶
在模型上定義自訂方法,以將自訂的「列級」功能添加到您的物件中。雖然 Manager
方法旨在執行「表格範圍」的操作,但模型方法應作用於特定的模型實例。
這是將業務邏輯保存在一個地方(即模型)的寶貴技術。
例如,此模型有一些自訂方法
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
birth_date = models.DateField()
def baby_boomer_status(self):
"Returns the person's baby-boomer status."
import datetime
if self.birth_date < datetime.date(1945, 8, 1):
return "Pre-boomer"
elif self.birth_date < datetime.date(1965, 1, 1):
return "Baby boomer"
else:
return "Post-boomer"
@property
def full_name(self):
"Returns the person's full name."
return f"{self.first_name} {self.last_name}"
此範例中的最後一個方法是 屬性。
模型實例參考 包含自動給每個模型的方法的完整列表。您可以覆寫其中大多數方法 – 請參閱下方的覆寫預定義的模型方法 – 但有幾個您幾乎總是想要定義的方法
__str__()
一個 Python「魔術方法」,會傳回任何物件的字串表示形式。這是當模型實例需要被強制轉換並顯示為純字串時,Python 和 Django 將使用的內容。最值得注意的是,當您在互動式主控台中或在管理介面中顯示物件時,會發生這種情況。
您應該總是想要定義此方法;預設的方法完全沒有幫助。
get_absolute_url()
這會告訴 Django 如何計算物件的 URL。Django 在其管理介面中以及任何需要找出物件 URL 時使用此方法。
任何具有唯一識別 URL 的物件都應定義此方法。
覆寫預定義的模型方法¶
還有另一組 模型方法,封裝了您想要自訂的大量資料庫行為。尤其是您經常會想要變更 save()
和 delete()
的運作方式。
您可以自由覆寫這些方法(以及任何其他模型方法)以變更行為。
覆寫內建方法的經典用例是,如果您希望在每次儲存物件時都發生某些事情。例如(請參閱 save()
以取得它接受的參數的文件)
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, **kwargs):
do_something()
super().save(**kwargs) # Call the "real" save() method.
do_something_else()
您也可以阻止儲存
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, **kwargs):
if self.name == "Yoko Ono's blog":
return # Yoko shall never have her own blog!
else:
super().save(**kwargs) # Call the "real" save() method.
請務必記得呼叫超類別方法 – 即 super().save(**kwargs)
的用法 – 以確保物件仍會儲存到資料庫中。如果您忘記呼叫超類別方法,則不會發生預設行為,並且不會接觸到資料庫。
您也必須傳遞可以傳遞給模型方法的引數 – 這就是 **kwargs
位元的作用。Django 會不時擴展內建模型方法的功能,加入新的關鍵字引數。如果您在方法定義中使用 **kwargs
,則可以保證您的程式碼會在新增這些引數時自動支援它們。
如果您希望在 save()
方法中更新欄位值,您可能也會希望將此欄位新增到 update_fields
關鍵字引數。這將確保在指定 update_fields
時儲存欄位。例如
from django.db import models
from django.utils.text import slugify
class Blog(models.Model):
name = models.CharField(max_length=100)
slug = models.TextField()
def save(self, **kwargs):
self.slug = slugify(self.name)
if (
update_fields := kwargs.get("update_fields")
) is not None and "name" in update_fields:
kwargs["update_fields"] = {"slug"}.union(update_fields)
super().save(**kwargs)
請參閱 指定要儲存的欄位 以取得更多詳細資料。
覆寫的模型方法不會在大量操作中被呼叫
請注意,當使用 QuerySet 大量刪除物件或作為級聯 刪除
的結果時,不一定會呼叫物件的 delete()
方法。為了確保執行自訂刪除邏輯,您可以使用 pre_delete
和/或 post_delete
訊號。
不幸的是,當大量建立
或大量更新
物件時,沒有變通辦法,因為不會呼叫 save()
、pre_save
和 post_save
。
執行自訂 SQL¶
另一種常見模式是在模型方法和模組級方法中撰寫自訂 SQL 陳述式。如需使用原始 SQL 的更多詳細資料,請參閱使用原始 SQL的文件。
模型繼承¶
Django 中的模型繼承幾乎與 Python 中一般類別繼承的方式相同,但仍然應該遵循頁面開頭的基本原則。這表示基底類別應子類別化 django.db.models.Model
。
您唯一需要做出的決定是,您是否希望父模型本身是模型(具有自己的資料庫表格),還是父模型只是通用資訊的持有者,這些資訊只會透過子模型可見。
在 Django 中,有三種可能的繼承樣式。
通常,您只會希望使用父類別來保存您不想為每個子模型重複輸入的資訊。此類別不會單獨使用,因此 抽象基底類別 正是您想要的。
如果您正在子類別化現有的模型(可能是來自另一個應用程式的東西),並希望每個模型都有自己的資料庫表格,則 多表格繼承 是正確的方法。
最後,如果您只想修改模型的 Python 級行為,而不以任何方式變更模型的欄位,則可以使用 代理模型。
抽象基底類別¶
當您想要將一些通用資訊放入多個其他模型中時,抽象基底類別很有用。您撰寫您的基底類別,並在 Meta 類別中放入 abstract=True
。然後,此模型將不會用於建立任何資料庫表格。相反地,當它用作其他模型的基底類別時,它的欄位將會新增至子類別的欄位中。
一個範例
from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
class Student(CommonInfo):
home_group = models.CharField(max_length=5)
Student
模型將具有三個欄位:name
、age
和 home_group
。CommonInfo
模型不能用作一般的 Django 模型,因為它是抽象基底類別。它不會產生資料庫表格或具有管理員,而且不能直接執行個體化或儲存。
從抽象基底類別繼承的欄位可以使用另一個欄位或值覆寫,或使用 None
移除。
對於許多用途而言,此類型的模型繼承正是您想要的。它提供一種在 Python 級別分解通用資訊的方法,同時仍然只在資料庫級別為每個子模型建立一個資料庫表格。
Meta
繼承¶
當建立一個抽象基底類別時,Django 會將你在基底類別中宣告的任何 Meta 內部類別,作為一個屬性提供使用。如果子類別沒有宣告自己的 Meta 類別,它將繼承父類別的 Meta。如果子類別想要擴展父類別的 Meta 類別,它可以將其子類化。例如:
from django.db import models
class CommonInfo(models.Model):
# ...
class Meta:
abstract = True
ordering = ["name"]
class Student(CommonInfo):
# ...
class Meta(CommonInfo.Meta):
db_table = "student_info"
Django 會對抽象基底類別的 Meta 類別進行一項調整:在安裝 Meta 屬性之前,它會設定 abstract=False
。這表示抽象基底類別的子類別不會自動變成抽象類別本身。若要建立一個繼承自另一個抽象基底類別的抽象基底類別,你需要在子類別上明確設定 abstract=True
。
有些屬性並不適合包含在抽象基底類別的 Meta 類別中。例如,包含 db_table
將表示所有的子類別(那些沒有指定自己的 Meta 的子類別)將使用相同的資料庫表格,這幾乎肯定不是你想要的。
由於 Python 繼承的運作方式,如果子類別繼承自多個抽象基底類別,預設只會繼承第一個列出的類別的 Meta 選項。若要從多個抽象基底類別繼承 Meta 選項,你必須明確宣告 Meta 的繼承。例如:
from django.db import models
class CommonInfo(models.Model):
name = models.CharField(max_length=100)
age = models.PositiveIntegerField()
class Meta:
abstract = True
ordering = ["name"]
class Unmanaged(models.Model):
class Meta:
abstract = True
managed = False
class Student(CommonInfo, Unmanaged):
home_group = models.CharField(max_length=5)
class Meta(CommonInfo.Meta, Unmanaged.Meta):
pass
多表格繼承¶
Django 支援的第二種模型繼承類型是當階層中的每個模型本身都是一個模型時。每個模型都對應到自己的資料庫表格,並且可以單獨查詢和建立。繼承關係會在子模型和其每個父模型之間引入連結(透過自動建立的 OneToOneField
)。例如:
from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
Place
的所有欄位也會在 Restaurant
中提供使用,儘管資料將會存在於不同的資料庫表格中。所以這兩種都是可行的:
>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")
如果你有一個同時也是 Restaurant
的 Place
,你可以使用模型名稱的小寫版本,從 Place
物件取得 Restaurant
物件:
>>> p = Place.objects.get(id=12)
# If p is a Restaurant object, this will give the child class:
>>> p.restaurant
<Restaurant: ...>
然而,如果上面範例中的 p
不是 Restaurant
(它是直接作為 Place
物件建立的,或是某些其他類別的父類別),參考 p.restaurant
將會引發 Restaurant.DoesNotExist
例外。
在 Restaurant
上自動建立的 OneToOneField
將其連結到 Place
,看起來像這樣:
place_ptr = models.OneToOneField(
Place,
on_delete=models.CASCADE,
parent_link=True,
primary_key=True,
)
你可以透過在 Restaurant
上宣告你自己的 OneToOneField
,並使用 parent_link=True
來覆寫該欄位。
Meta
和多表格繼承¶
在多表格繼承的情況下,子類別繼承其父類別的 Meta 類別是沒有意義的。所有的 Meta 選項都已經應用到父類別上,而再次應用它們通常只會導致矛盾的行為(這與抽象基底類別的情況相反,其中基底類別本身不存在)。
因此,子模型無法存取其父類別的 Meta 類別。然而,在一些有限的情況下,子類別會繼承父類別的行為:如果子類別沒有指定 ordering
屬性或 get_latest_by
屬性,它將會從其父類別繼承這些屬性。
如果父類別具有排序方式,而你不希望子類別有任何自然排序方式,你可以明確地停用它:
class ChildModel(ParentModel):
# ...
class Meta:
# Remove parent's ordering effect
ordering = []
繼承和反向關係¶
由於多表繼承使用隱含的 OneToOneField
來連結子類別和父類別,因此可以像上面的範例一樣,從父類別向下移動到子類別。然而,這會用掉 related_name
的預設值,該值用於 ForeignKey
和 ManyToManyField
關聯。 如果您在父模型子類別上放置這些類型的關聯,則必須在每個此類欄位上指定 related_name
屬性。 如果您忘記,Django 會引發驗證錯誤。
例如,再次使用上面的 Place
類別,讓我們建立另一個帶有 ManyToManyField
的子類別
class Supplier(Place):
customers = models.ManyToManyField(Place)
這會導致錯誤
Reverse query name for 'Supplier.customers' clashes with reverse query
name for 'Supplier.place_ptr'.
HINT: Add or change a related_name argument to the definition for
'Supplier.customers' or 'Supplier.place_ptr'.
將 related_name
新增到 customers
欄位,如下所示,將解決該錯誤:models.ManyToManyField(Place, related_name='provider')
。
指定父連結欄位¶
如前所述,Django 會自動建立一個 OneToOneField
,將您的子類別連結回任何非抽象父模型。如果您想控制連結回父類別的屬性名稱,您可以建立自己的 OneToOneField
並設定 parent_link=True
以指示您的欄位是連結回父類別的連結。
代理模型¶
當使用 多表繼承 時,會為模型的每個子類別建立一個新的資料庫表格。這通常是期望的行為,因為子類別需要一個地方來儲存基本類別上不存在的任何其他資料欄位。但是,有時您只想變更模型的 Python 行為 - 可能要變更預設的管理員,或新增一個新方法。
這就是代理模型繼承的用途:為原始模型建立代理。您可以建立、刪除和更新代理模型的實例,並且所有資料都會被儲存,就像您正在使用原始(非代理)模型一樣。不同之處在於,您可以在代理中變更預設的模型排序或預設的管理員等,而無需變更原始模型。
代理模型會像普通模型一樣宣告。您通過將 Meta
類別的 proxy
屬性設定為 True
來告訴 Django 它是一個代理模型。
例如,假設您想向 Person
模型新增一個方法。您可以這樣做
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
class MyPerson(Person):
class Meta:
proxy = True
def do_something(self):
# ...
pass
MyPerson
類別與其父類別 Person
類別在同一個資料庫表格上操作。特別是,任何新的 Person
實例也可以通過 MyPerson
存取,反之亦然
>>> p = Person.objects.create(first_name="foobar")
>>> MyPerson.objects.get(first_name="foobar")
<MyPerson: foobar>
您也可以使用代理模型在模型上定義不同的預設排序。您可能不總是想排序 Person
模型,但在您使用代理時,通常會依 last_name
屬性排序
class OrderedPerson(Person):
class Meta:
ordering = ["last_name"]
proxy = True
現在,普通的 Person
查詢將不會排序,而 OrderedPerson
查詢將依 last_name
排序。
代理模型繼承 Meta
屬性,與一般模型相同的方式。
QuerySet
仍然會傳回所請求的模型¶
沒有辦法讓 Django 在您查詢 Person
物件時傳回,例如,MyPerson
物件。用於 Person
物件的 queryset 將傳回這些類型的物件。代理物件的重點是,依賴原始 Person
的程式碼將使用它們,而您自己的程式碼可以使用您包含的擴充功能(無論如何,沒有其他程式碼依賴這些擴充功能)。這不是將 Person
(或任何其他)模型替換為您自己建立的東西的方法。
基本類別限制¶
代理模型必須繼承自正好一個非抽象模型類別。您不能從多個非抽象模型繼承,因為代理模型不會在不同資料庫表格中的列之間提供任何連線。代理模型可以從任意數量的抽象模型類別繼承,前提是它們沒有定義任何模型欄位。代理模型也可以從任何數量共享通用非抽象父類別的代理模型繼承。
代理模型管理員¶
如果您沒有在代理模型上指定任何模型管理員,它會從其模型父類別繼承管理員。如果您在代理模型上定義管理員,它將成為預設值,儘管父類別上定義的任何管理員仍然可用。
繼續我們上面的範例,您可以像這樣變更查詢 Person
模型時使用的預設管理員
from django.db import models
class NewManager(models.Manager):
# ...
pass
class MyPerson(Person):
objects = NewManager()
class Meta:
proxy = True
如果您想將新的管理員新增到代理,而無需替換現有的預設值,您可以使用 自訂管理員 文件中描述的技術:建立一個包含新管理員的基本類別,並在主要基本類別之後繼承該類別
# Create an abstract class for the new manager.
class ExtraManagers(models.Model):
secondary = NewManager()
class Meta:
abstract = True
class MyPerson(Person, ExtraManagers):
class Meta:
proxy = True
您可能不會經常需要執行此操作,但是,當您需要時,這是可能的。
代理繼承和非受管理模型之間的差異¶
代理模型繼承可能看起來與使用模型的 Meta
類別上的 managed
屬性建立非受管理模型非常相似。
透過仔細設定 Meta.db_table
,您可以建立一個遮蔽現有模型並向其新增 Python 方法的非受管理模型。但是,這會非常重複且脆弱,因為如果您進行任何變更,您需要使兩個副本保持同步。
另一方面,代理模型旨在表現得與它們所代理的模型完全一樣。它們始終與父模型同步,因為它們直接繼承其欄位和管理員。
一般規則是
如果您要鏡像現有模型或資料庫表格,並且不想要所有原始資料庫表格列,請使用
Meta.managed=False
。此選項通常對於建模資料庫視圖和不受 Django 控制的表格很有用。如果您想要變更模型的純 Python 行為,但保留與原始模型相同的所有欄位,請使用
Meta.proxy=True
。這樣設定後,代理模型在儲存資料時將會是原始模型的儲存結構的精確副本。
多重繼承¶
就像 Python 的子類別化一樣,Django 模型可以從多個父模型繼承。請記住,會套用一般的 Python 名稱解析規則。特定名稱(例如 Meta)出現的第一個基本類別將是被使用的類別;例如,這表示如果多個父類別包含 Meta 類別,則只會使用第一個,而所有其他的都會被忽略。
一般來說,您不需要從多個父類別繼承。這種方法有用的主要使用案例是用於「混入」類別:將特定的額外欄位或方法新增到繼承混入的每個類別。盡量使您的繼承層次結構盡可能簡單和直接,這樣您就不必費力地弄清楚特定資訊的來源。
請注意,從具有共同 id
主索引鍵欄位的多個模型繼承會引發錯誤。要正確使用多重繼承,您可以在基本模型中使用明確的 AutoField
class Article(models.Model):
article_id = models.AutoField(primary_key=True)
...
class Book(models.Model):
book_id = models.AutoField(primary_key=True)
...
class BookReview(Book, Article):
pass
或者使用一個共同的祖先來持有 AutoField
。這需要從每個父模型到共同的祖先使用明確的 OneToOneField
,以避免自動產生並由子項繼承的欄位之間發生衝突。
class Piece(models.Model):
pass
class Article(Piece):
article_piece = models.OneToOneField(
Piece, on_delete=models.CASCADE, parent_link=True
)
...
class Book(Piece):
book_piece = models.OneToOneField(Piece, on_delete=models.CASCADE, parent_link=True)
...
class BookReview(Book, Article):
pass
不允許「隱藏」欄位名稱¶
在一般的 Python 類別繼承中,子類別可以覆寫父類別的任何屬性。在 Django 中,模型欄位通常不允許這樣做。如果一個非抽象模型基底類別有一個名為 author
的欄位,你不能在任何繼承自該基底類別的類別中建立另一個模型欄位或定義一個名為 author
的屬性。
此限制不適用於從抽象模型繼承的模型欄位。這些欄位可以使用另一個欄位或值覆寫,或者透過設定 field_name = None
來移除。
警告
模型管理器是從抽象基底類別繼承的。覆寫一個被繼承的 Manager
引用的繼承欄位可能會導致難以察覺的錯誤。請參閱 自訂管理器和模型繼承。
注意
有些欄位會在模型上定義額外的屬性,例如,ForeignKey
會定義一個額外的屬性,其欄位名稱後附加 _id
,以及在外鍵模型上的 related_name
和 related_query_name
。
除非變更或移除定義額外屬性的欄位,使其不再定義額外屬性,否則無法覆寫這些額外屬性。
覆寫父模型中的欄位會導致在初始化新實例(指定在 Model.__init__
中初始化哪個欄位)和序列化等領域出現困難。這些功能是正常的 Python 類別繼承不需要以完全相同的方式處理的,因此 Django 模型繼承和 Python 類別繼承之間的差異並非任意的。
此限制僅適用於 Field
實例的屬性。如果需要,可以覆寫一般的 Python 屬性。它也僅適用於 Python 所看到的屬性名稱:如果你是手動指定資料庫欄名稱,則可以在子模型和祖先模型中,針對多表繼承使用相同的欄名稱(它們是兩個不同資料庫表中的欄位)。
如果你在任何祖先模型中覆寫任何模型欄位,Django 將會引發 FieldError
。
請注意,由於在類別定義期間解析欄位的方式,從多個抽象父模型繼承的模型欄位會以嚴格的深度優先順序解析。這與標準的 Python MRO 相反,後者在菱形繼承的情況下會以廣度優先的方式解析。這種差異只會影響複雜的模型層次結構,你應該(按照上面的建議)盡量避免這種情況。
在套件中組織模型¶
manage.py startapp
命令會建立一個應用程式結構,其中包含一個 models.py
檔案。如果你有很多模型,將它們組織在單獨的檔案中可能會很有用。
要這樣做,請建立一個 models
套件。移除 models.py
並建立一個 myapp/models/
目錄,其中包含一個 __init__.py
檔案以及用於儲存模型的檔案。你必須在 __init__.py
檔案中匯入模型。
例如,如果你的 models
目錄中有 organic.py
和 synthetic.py
。
myapp/models/__init__.py
¶from .organic import Person
from .synthetic import Robot
明確地匯入每個模型,而不是使用 from .models import *
具有以下優點:不會使命名空間混亂、使程式碼更易讀,並保持程式碼分析工具的可用性。
參見
- 模型參考
涵蓋所有模型相關的 API,包括模型欄位、相關物件和
QuerySet
。