Django 中的 model 繼承和 Python 中的類繼承非常相似,只不過你要選擇具體的實現方式:讓父 model 擁有獨立的數據庫;還是讓父 model 只包含基本的公共信息,而這些信息只能由子 model 呈現。
Django中有三種繼承關系:
1.通常,你只是想用父 model 來保存那些你不想在子 model 中重復錄入的信息。父類是不使用的也就是不生成單獨的數據表,這種情況下使用抽象基類繼承 Abstract base classes。
2.如果你想從現有的Model繼承並讓每個Model都有自己的數據表,那么使用多重表繼承Multi-table inheritance。
3.最后,如果你只想在 model 中修改 Python-level 級的行為,而不涉及字段改變。 代理 model (Proxy models) 適用於這種場合。
Abstract base classes
如果你想把某些公共信息添加到很多 model 中,抽象基類就顯得非常有用。你編寫完基類之后,在 Meta 內嵌類中設置 abstract=True ,該類就不能創建任何數據表。然而如果將它做為其他 model 的基類,那么該類的字段就會被添加到子類中。抽象基類和子類如果含有同名字段,就會導致錯誤(Django 將拋出異常)。
1
2
3
4
5
6
7
8
9
|
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
)
|
sqlall結果:
1
2
3
4
5
6
|
CREATE TABLE
"myapp_student"
(
"id"
integer NOT NULL PRIMARY KEY,
"name"
varchar(
100
) NOT NULL,
"age"
integer unsigned NOT NULL,
"home_group"
varchar(
5
) NOT NULL
)
|
只為Student model 生成了數據表,而CommonInfo不能做為普通的 Django model 使用,因為它是一個抽象基類。他即不生成數據表,也沒有 manager ,更不能直接被實例化和保存。
對很多應用來說,這種繼承方式正是你想要的。它提供一種在 Python 語言層級上提取公共信息的方式,但在數據庫層級上,每個子類仍然只創建一個數據表,在JPA中稱作TABLE_PER_CLASS。這種方式下,每張表都包含具體類和繼承樹上所有父類的字段。因為多個表中有重復字段,從整個繼承樹上來說,字段是冗余的。
Meta繼承
創建抽象基類的時候,Django 會將你在基類中所聲明的有效的 Meta 內嵌類做為一個屬性。如果子類沒有聲明它自己的 Meta 內嵌類,它就會繼承父類的 Meta 。子類的 Meta 也可以直接繼承父類的 Meta 內嵌類,對其進行擴展。例如:
1
2
3
4
5
6
7
8
9
10
11
|
class
CommonInfo(models.Model):
name
=
models.CharField(max_length
=
100
)
age
=
models.PositiveIntegerField()
class
Meta:
abstract
=
True
ordering
=
[
'name'
]
class
Student(CommonInfo):
home_group
=
models.CharField(max_length
=
5
)
class
Meta(CommonInfo.Meta):
db_table
=
'student_info'
|
sqlall結果:
1
2
3
4
5
6
|
CREATE TABLE
"student_info"
(
"id"
integer NOT NULL PRIMARY KEY,
"name"
varchar(
100
) NOT NULL,
"age"
integer unsigned NOT NULL,
"home_group"
varchar(
5
) NOT NULL
)
|
按照我們指定的名稱student_info生成了table。
繼承時,Django 會對基類的 Meta 內嵌類做一個調整:在安裝 Meta 屬性之前,Django 會設置 abstract=False。 這意味着抽象基類的子類不會自動變成抽象類。當然,你可以讓一個抽象類繼承另一個抽象基類,不過每次都要顯式地設置 abstract=True 。
對於抽象基類而言,有些屬性放在 Meta 內嵌類里面是沒有意義的。例如,包含 db_table 將意味着所有的子類(是指那些沒有指定自己的 Meta 內嵌類的子類)都使用同一張數據表,一般來說,這並不是我們想要的。
小心使用 related_name (Be careful with related_name)
如果你在 ForeignKey 或 ManyToManyField 字段上使用 related_name 屬性,你必須總是為該字段指定一個唯一的反向名稱。但在抽象基類上這樣做就會引發一個很嚴重的問題。因為 Django 會將基類字段添加到每個子類當中,而每個子類的字段屬性值都完全相同 (這里面就包括 related_name)。注:這樣使用 ForeignKey 或 ManyToManyField 反向指定時就無法確定是指向哪個子類了。
當你在(且僅在)抽象基類中使用 related_name 時,如果想繞過這個問題,就要在屬性值中包含 '%(app_label)s' 和 '%(class)s'字符串。
1.'%(class)s'會被子類的名字取代。
2.'%(app_label)s'會被子類所在的app的名字所取代。
舉例,在app common中,common/models.py:
1
2
3
4
5
6
7
8
9
10
11
|
class
Base(models.Model):
m2m
=
models.ManyToManyField(OtherModel, related_name
=
"%(app_label)s_%(class)s_related"
)
class
Meta:
abstract
=
True
class
ChildA(Base):
pass
class
ChildB(Base):
pass
|
在另外一個app中,rare/models.py:
1
2
|
class
ChildB(Base):
pass
|
那么common.ChildA.m2m字段的反向名稱為common_childa_related, common.ChildB.m2m字段的反向名稱為common_childb_related, rare app中rare.ChildB.m2m字段的反向名稱為rare_childb_related.
如果你沒有在抽象基類中為某個關聯字段定義 related_name 屬性,那么默認的反向名稱就是子類名稱加上 '_set',它能否正常工作取決於你是否在子類中定義了同名字段。例如,在上面的代碼中,如果去掉 related_name 屬性,在 ChildA 中,m2m 字段的反向名稱就是 childa_set;而 ChildB 的 m2m 字段的反向名稱就是 childb_set 。
多表繼承(Multi-table inheritance)
這是 Django 支持的第二種繼承方式。使用這種繼承方式時,同一層級下的每個子 model 都是一個真正意義上完整的 model 。每個子 model 都有專屬的數據表,都可以查詢和創建數據表。繼承關系在子 model 和它的每個父類之間都添加一個鏈接 (通過一個自動創建的 OneToOneField 來實現)。 例如:
1
2
3
4
5
6
7
|
class
Place(models.Model):
name
=
models.CharField(max_length
=
50
)
address
=
models.CharField(max_length
=
80
)
class
Restaurant(Place):
serves_hot_dogs
=
models.BooleanField()
serves_pizza
=
models.BooleanField()
|
sqlall:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
BEGIN;
CREATE TABLE
"myapp_place"
(
"id"
integer NOT NULL PRIMARY KEY,
"name"
varchar(
50
) NOT NULL,
"address"
varchar(
80
) NOT NULL
)
;
CREATE TABLE
"myapp_restaurant"
(
"place_ptr_id"
integer NOT NULL PRIMARY KEY REFERENCES
"myapp_place"
(
"id"
),
"serves_hot_dogs"
bool
NOT NULL,
"serves_pizza"
bool
NOT NULL
)
;
COMMIT;
|
父類和子類都生成了單獨的數據表,Restaurant中存儲了Place的id,也就是通過OneToOneField鏈接在一起。繼承關系通過表的JOIN操作來表示。在JPA中稱作JOINED。這種方式下,每個表只包含類中定義的字段,不存在字段冗余,但是要同時操作子類和所有父類所對應的表。
Place 里面的所有字段在 Restaurant 中也是有效的,只不過數據保存在另外一張數據表當中。所以下面兩個語句都是可以運行的:
1
2
|
>>> Place.objects.
filter
(name
=
"Bob's Cafe"
)
>>> Restaurant.objects.
filter
(name
=
"Bob's Cafe"
)
|
如果你有一個 Place,那么它同時也是一個 Restaurant, 那么你可以使用子 model 的小寫形式從 Place 對象中獲得與其對應的 Restaurant 對象:
1
2
3
4
|
>>> p
=
Place.objects.
filter
(name
=
"Bob's Cafe"
)
# If Bob's Cafe is a Restaurant object, this will give the child class:
>>> p.restaurant
<Restaurant: ...>
|
但是,如果上例中的 p 並不是 Restaurant (比如它僅僅只是 Place 對象,或者它是其他類的父類),那么在引用 p.restaurant 就會拋開Restaurant.DoesNotExist 異常:
1
2
3
4
|
>>>
from
myapp.models
import
Place,Restaurant
>>> p
=
Place.objects.create(name
=
'Place'
,address
=
'Place'
)
>>> p.restaurant
DoesNotExist: Place has no restaurant.
|
也就是說,創建Place實例的同時不會創建Restaurant,但是創建Restaurant實例的同時會創建Place實例:
1
2
3
4
|
>>>Restaurant.objects.create(name
=
'M'
,address
=
'M'
,serves_hot_dogs
=
True
,serves_pizza
=
True
)
<Restaurant: Restaurant
object
>
>>> Place.objects.get(name
=
'M'
)
<Place: Place
object
>
|
多表繼承中的Meta (Meta and multi-table inheritance)
在多表繼承中,子類繼承父類的 Meta 內嵌類是沒什么意見的。所有的 Meta 選項已經對父類起了作用,再次使用只會起反作用。(這與使用抽象基類的情況正好相反,因為抽象基類並沒有屬於它自己的內容)
所以子 model 並不能訪問它父類的 Meta 內嵌類。但是在某些受限的情況下,子類可以從父類繼承某些 Meta :如果子類沒有指定 django.db.models.Options.ordering 屬性或 django.db.models.Options.get_latest_by 屬性,它就會從父類中繼承這些屬性。
如果父類有了排序設置,而你並不想讓子類有任何排序設置,你就可以顯式地禁用排序:
1
2
3
4
5
|
class
ChildModel(ParentModel):
# ...
class
Meta:
# Remove parent's ordering effect
ordering
=
[]
|
繼承與反向關聯(Inheritance and reverse relations)
因為多表繼承使用了一個隱含的 OneToOneField 來鏈接子類與父類,所以象上例那樣,你可以用父類來指代子類。但是這個 OnetoOneField 字段默認的 related_name 值與 django.db.models.fields.ForeignKey 和 django.db.models.fields.ManyToManyField 默認的反向名稱相同。如果你與其他 model 的子類做多對一或是多對多關系,你就必須在每個多對一和多對多字段上強制指定 related_name 。如果你沒這么做,Django 就會在你運行 驗證(validate) 或 同步數據庫(syncdb) 時拋出異常。
例如,仍以上面 Place 類為例,我們創建一個帶有 ManyToManyField 字段的子類:
1
2
3
|
class
Supplier(Place):
# Must specify related_name on all relations.
customers
=
models.ManyToManyField(Restaurant, related_name
=
'provider'
)
|
指定鏈接父類的字段(Specifying the parent link field)
之前我們提到,Django 會自動創建一個 OneToOneField 字段將子類鏈接至非抽象的父 model 。如果你想指定鏈接父類的屬性名稱,你可以創建你自己的 OneToOneField 字段並設置 parent_link=True ,從而使用該字段鏈接父類。
代理model (Proxy models)
使用 多表繼承(multi-table inheritance) 時,model 的每個子類都會創建一張新數據表,通常情況下,這正是我們想要的操作。這是因為子類需要一個空間來存儲不包含在基類中的字段數據。但有時,你可能只想更改 model 在 Python 層的行為實現。比如:更改默認的 manager ,或是添加一個新方法。
而這,正是代理 model 繼承方式要做的:為原始 model 創建一個代理(proxy)。你可以創建,刪除,更新代理 model 的實例,而且所有的數據都可以象使用原始 model 一樣被保存。不同之處在於:你可以在代理 model 中改變默認的排序設置和默認的 manager ,更不會對原始 model 產生影響。
聲明代理 model 和聲明普通 model 沒有什么不同。設置Meta 內置類中 proxy 的值為 True,就完成了對代理 model 的聲明。
舉個例子,假設你想給 Django 自帶的標准 User model (它被用在你的模板中)添加一個方法:
1
2
3
4
5
6
7
8
9
10
11
|
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
|
sqlall:
1
2
3
4
5
6
|
CREATE TABLE
"myapp_person"
(
"id"
integer NOT NULL PRIMARY KEY,
"first_name"
varchar(
30
) NOT NULL,
"last_name"
varchar(
30
) NOT NULL
)
;
|
MyPerson 類和它的父類 Person操作同一個數據表。特別的是,Person 的任何實例也可以通過 MyPerson 訪問,反之亦然:
1
2
3
|
>>> p
=
Person.objects.create(first_name
=
"foobar"
)
>>> MyPerson.objects.get(first_name
=
"foobar"
)
<MyPerson: foobar>
|
你也可以使用代理 model 給 model 定義不同的默認排序設置。Django 自帶的 User model 沒有定義排序設置(這是故意為之,是因為排序開銷極大,我們不想在獲取用戶時浪費額外資源)。你可以利用代理對 username 屬性進行排序,這很簡單:
1
2
3
4
|
class
OrderedPerson(Person):
class
Meta:
ordering
=
[
"last_name"
]
proxy
=
True
|
普通的 User 查詢,其結果是無序的;而 OrderedUser 查詢的結果是按 username 排序。
查詢集只返回請求時所使用的 model (Querysets still return the model that was requested)
無論你何時查詢Person 對象,Django 都不會返回 MyPerson 對象。針對 Person 對象的查詢集只返回 Person 對象。代理對象的精要就在於依賴原始 User 的代碼僅對它自己有效,而你自己的代碼就使用你擴展的內容。不管你怎么改動,都不會在查詢 Person 時得到 MyPerson。
基類的限制(Base class restrictions)
代理 model 必須繼承自一個非抽象基類。你不能繼承自多個非抽象基類,這是因為一個代理 model 不能連接不同的數據表。代理 model 也可以繼承任意多個抽象基類,但前提是它們沒有定義任何 model 字段。
代理 model 從非抽象基類中繼承那些未在代理 model 定義的 Meta 選項。
代理 model 的 manager (Proxy model managers)
如果你沒有在代理 model 中定義任何 manager ,代理 model 就會從父類中繼承 manager 。如果你在代理 model 中定義了一個 manager ,它就會變成默認的 manager ,不過定義在父類中的 manager 仍是有效的。
繼續上面的例子,你可以改變默認 manager,例如:
1
2
3
4
5
6
7
8
9
|
class
NewManager(models.Manager):
# ...
pass
class
MyPerson(Person):
objects
=
NewManager()
class
Meta:
proxy
=
True
|
如果你想給代理添加一個新的 manager ,卻不想替換已有的默認 manager ,那么你可以參考 自定義 manager (custom manager) 中提到的方法:創建一個包含新 manager 的基類,然后放在主基類后面繼承:
1
2
3
4
5
6
7
8
9
|
class
ExtraManagers(models.Model):
secondary
=
NewManager()
class
Meta:
abstract
=
True
class
MyPerson(Person, ExtraManagers):
class
Meta:
proxy
=
True
|
你可能不需要經常這樣做,但這樣做是可行的。
代理 model 與非托管 model 之間的差異(Differences between proxy inheritance and unmanaged models)
代理 model 繼承看上去和使用 Meta 內嵌類中的 managed 屬性的非托管 model 非常相似。但兩者並不相同,你應當考慮選用哪種方案。
一個不同之處是你可以在 Meta.managed=False 的 model 中定義字段(事實上,是必須指定,除非你真的想得到一個空 model )。在創建非托管 model 時要謹慎設置 Meta.db_table ,這是因為創建的非托管 model 映射某個已存在的 model ,並且有自己的方法。因此,如果你要保證這兩個 model 同步並對程序進行改動,那么就會變得繁冗而脆弱。
另一個不同之處是兩者對 manager 的處理方式不同。這對於代理 model 非常重要。代理 model 要與它所代理的 model 行為相似,所以代理 model 要繼承父 model 的 managers ,包括它的默認 manager 。但在普通的多表繼承中,子類不能繼承父類的 manager ,這是因為在處理非基類字段時,父類的 manager 未必適用。
我們實現了這兩種特性(Meta.proxy和Meta.unmanaged)之后,曾嘗試把兩者結合到一起。結果證明,宏觀的繼承關系和微觀的 manager 揉在一起,不僅導致 API 復雜難用,而且還難以理解。由於任何場合下都可能需要這兩個選項,所以目前二者仍是各自獨立使用的。
所以,一般規則是:
1.如果你要鏡像一個已有的 model 或數據表,且不想涉及所有的原始數據表的列,那就令 Meta.managed=False。通常情況下,對數據庫視圖創建 model 或是數據表不需要由 Django 控制時,就使用這個選項。
2.如果你想對 model 做 Python 層級的改動,又想保留字段不變,那就令 Meta.proxy=True。因此在數據保存時,代理 model 相當於完全復制了原始 model 的存儲結構。
多重繼承(Multiple inheritance)
和 Python 一樣,Django 的 model 也可以做多重繼承。這里要記住 Python 的名稱解析規則。如果某個特定名稱 (例如,Meta) 出現在第一個基類當中,那么子類就會使用第一個基類的該特定名稱。例如,如果多重父類都包含 Meta 內嵌類,只有第一個基類的 Meta 才會被使用,其他的都被會忽略。
一般來說,你沒必要使用多重繼承。
不允許"隱藏"字段(Field name "hiding" is not permitted)
普通的 Python 類繼承允許子類覆蓋父類的任何屬性。但在 Django 中,重寫 Field 實例是不允許的(至少現在還不行)。如果基類中有一個 author 字段,你就不能在子類中創建任何名為 author 的字段。
重寫父類的字段會導致很多麻煩,比如:初始化實例(指定在 Model.__init__ 中被實例化的字段) 和序列化。而普通的 Python 類繼承機制並不能處理好這些特性。所以 Django 的繼承機制被設計成與 Python 有所不同,這樣做並不是隨意而為的。
這些限制僅僅針對做為屬性使用的 Field 實例,並不是針對 Python 屬性,Python 屬性仍是可以被重寫的。在 Python 看來,上面的限制僅僅針對字段實例的名稱:如果你手動指定了數據庫的列名稱,那么在多重繼承中,你就可以在子類和某個父類當中使用同一個列名稱。(因為它們使用的是兩個不同數據表的字段)。
如果你在任何一個父類中重寫了某個 model 字段,Django 都會拋出 FieldError 異常。