模型的繼承 -- Django從入門到精通系列教程
該系列教程繫個人原創,並完整發布在個人官網劉江的部落格和教程
所有轉載本文者,需在頂部顯著位置註明原作者及www.liujiangblog.com官網地址。
很多時候,我們都不是從‘一窮二白’開始編寫模型的,有時候可以從第三方庫中繼承,有時候可以從以前的程式碼中繼承,甚至現寫一個模型用於被其它模型繼承。這樣做的好處,我就不贅述了,每個學習Django的人都非常清楚。
類同於Python的類繼承,Django也有完善的繼承機制。
Django中所有的模型都必須繼承django.db.models.Model
模型,不管是直接繼承也好,還是間接繼承也罷。
你唯一需要決定的是,父模型是否是一個獨立自主的,同樣在資料庫中建立資料表的模型,還是一個只用來儲存子模型共有內容,並不實際建立資料表的抽象模型。
Django有三種繼承的方式:
- 抽象基類:被用來繼承的模型被稱為
Abstract base classes
,將子類共同的資料抽離出來,供子類繼承重用,它不會建立實際的資料表; - 多表繼承:
Multi-table inheritance
,每一個模型都有自己的資料庫表; - 代理模型:如果你只想修改模型的Python層面的行為,並不想改動模型的欄位,可以使用代理模型。
注意!同Python的繼承一樣,Django也是可以同時繼承兩個以上父類的!
一、 抽象基類:
只需要在模型的Meta類裡新增abstract=True
元資料項,就可以將一個模型轉換為抽象基類。Django不會為這種類建立實際的資料庫表,它們也沒有管理器,不能被例項化也無法直接儲存,它們就是用來被繼承的。抽象基類完全就是用來儲存子模型們共有的內容部分,達到重用的目的。當它們被繼承時,它們的欄位會全部複製到子模型中。看下面的例子:
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模型不能當做一個正常的模型使用。
抽象基類的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'
這裡有幾點要特別說明:
- 抽象基類中有的元資料,子模型沒有的話,直接繼承;
- 抽象基類中有的元資料,子模型也有的話,直接覆蓋;
- 子模型可以額外新增元資料;
- 抽象基類中的
abstract=True
這個元資料不會被繼承。也就是說如果想讓一個抽象基類的子模型,同樣成為一個抽象基類,那你必須顯式的在該子模型的Meta中同樣宣告一個abstract = True
; - 有一些元資料對抽象基類無效,比如
db_table
,首先是抽象基類本身不會建立資料表,其次它的所有子類也不會按照這個元資料來設定表名。
警惕related_name和related_query_name引數
如果在你的抽象基類中存在ForeignKey或者ManyToManyField欄位,並且使用了related_name
或者related_query_name
引數,那麼一定要小心了。因為按照預設規則,每一個子類都將擁有同樣的欄位,這顯然會導致錯誤。為了解決這個問題,當你在抽象基類中使用related_name
或者related_query_name
引數時,它們兩者的值中應該包含%(app_label)s
和%(class)s
部分:
%(class)s
用欄位所屬子類的小寫名替換%(app_label)s
用子類所屬app的小寫名替換
例如,對於common/models.py
模組:
from django.db import models
class Base(models.Model):
m2m = models.ManyToManyField(
OtherModel,
related_name="%(app_label)s_%(class)s_related",
related_query_name="%(app_label)s_%(class)ss",
)
class Meta:
abstract = True
class ChildA(Base):
pass
class ChildB(Base):
pass
對於另外一個應用中的rare/models.py
:
from common.models import Base
class ChildB(Base):
pass
對於上面的繼承關係:
common.ChildA.m2m
欄位的reverse name
(反向關係名)應該是common_childa_related
;reverse query name
(反向查詢名)應該是common_childas
。common.ChildB.m2m
欄位的反向關係名應該是common_childb_related
;反向查詢名應該是common_childbs
。rare.ChildB.m2m
欄位的反向關係名應該是rare_childb_related
;反向查詢名應該是rare_childbs
。
當然,如果你不設定related_name
或者related_query_name
引數,這些問題就不存在了。
二、 多表繼承
這種繼承方式下,父類和子類都是獨立自主、功能完整、可正常使用的模型,都有自己的資料庫表,內部隱含了一個一對一的關係。例如:
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)
Restaurant將包含Place的所有欄位,並且各有各的資料庫表和欄位,比如:
>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")
如果一個Place物件同時也是一個Restaurant物件,你可以使用小寫的子類名,在父類中訪問它,例如:
>>> p = Place.objects.get(id=12)
# 如果p也是一個Restaurant物件,那麼下面的呼叫可以獲得該Restaurant物件。
>>> p.restaurant
<Restaurant: ...>
但是,如果這個Place是個純粹的Place物件,並不是一個Restaurant物件,那麼上面的呼叫方式會彈出Restaurant.DoesNotExist
異常。
讓我們看一組更具體的展示,注意裡面的註釋內容。
>>> from app1.models import Place, Restaurant # 匯入兩個模型到shell裡
>>> p1 = Place.objects.create(name='coff',address='address1')
>>> p1 # p1是個純Place物件
<Place: Place object>
>>> p1.restaurant # p1沒有餐館屬性
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "C:\Python36\lib\site-packages\django\db\models\fields\related_descriptors.py", line 407, in __get__
self.related.get_accessor_name()
django.db.models.fields.related_descriptors.RelatedObjectDoesNotExist: Place has no restaurant.
>>> r1 = Restaurant.objects.create(serves_hot_dogs=True,serves_pizza=False)
>>> r1 # r1在建立的時候,只賦予了2個欄位的值
<Restaurant: Restaurant object>
>>> r1.place # 不能這麼呼叫
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Restaurant' object has no attribute 'place'
>>> r2 = Restaurant.objects.create(serves_hot_dogs=True,serves_pizza=False, name='pizza', address='address2')
>>> r2 # r2在建立時,提供了包括Place的欄位在內的4個欄位
<Restaurant: Restaurant object>
>>> r2.place # 可以看出這麼呼叫都是非法的,異想天開的
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Restaurant' object has no attribute 'place'
>>> p2 = Place.objects.get(name='pizza') # 通過name,我們獲取到了一個Place物件
>>> p2.restaurant # 這個P2其實就是前面的r2
<Restaurant: Restaurant object>
>>> p2.restaurant.address
'address2'
>>> p2.restaurant.serves_hot_dogs
True
>>> lis = Place.objects.all()
>>> lis
<QuerySet [<Place: Place object>, <Place: Place object>, <Place: Place object>]>
>>> lis.values()
<QuerySet [{'id': 1, 'name': 'coff', 'address': 'address1'}, {'id': 2, 'name': '', 'address': ''}, {'id': 3, 'name': 'pizza', 'address': 'address2'}]>
>>> lis[2]
<Place: Place object>
>>> lis[2].serves_hot_dogs
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: 'Place' object has no attribute 'serves_hot_dogs'
>>> lis2 = Restaurant.objects.all()
>>> lis2
<QuerySet [<Restaurant: Restaurant object>, <Restaurant: Restaurant object>]>
>>> lis2.values()
<QuerySet [{'id': 2, 'name': '', 'address': '', 'place_ptr_id': 2, 'serves_hot_dogs': True, 'serves_pizza': False}, {'id': 3, 'name': 'pizza', 'address
': 'address2', 'place_ptr_id': 3, 'serves_hot_dogs': True, 'serves_pizza': False}]>
其機制內部隱含的OneToOne欄位,形同下面所示:
place_ptr = models.OneToOneField(
Place, on_delete=models.CASCADE,
parent_link=True,
)
可以通過建立一個OneToOneField欄位並設定 parent_link=True
,自定義這個一對一欄位。
Meta和多表繼承
在多表繼承的情況下,由於父類和子類都在資料庫內有物理存在的表,父類的Meta類會對子類造成不確定的影響,因此,Django在這種情況下關閉了子類繼承父類的Meta功能。這一點和抽象基類的繼承方式有所不同。
但是,還有兩個Meta元資料特殊一點,那就是ordering
和get_latest_by
,這兩個引數是會被繼承的。因此,如果在多表繼承中,你不想讓你的子類繼承父類的上面兩種引數,就必須在子類中顯示的指出或重寫。如下:
class ChildModel(ParentModel):
# ...
class Meta:
# 移除父類對子類的排序影響
ordering = []
多表繼承和反向關聯
因為多表繼承使用了一個隱含的OneToOneField來連結子類與父類,所以象上例那樣,你可以從父類訪問子類。但是這個OnetoOneField欄位預設的related_name
值與ForeignKey和 ManyToManyField預設的反向名稱相同。如果你與父類或另一個子類做多對一或是多對多關係,你就必須在每個多對一和多對多欄位上強制指定related_name
。如果你沒這麼做,Django就會在你執行或驗證(validation)時丟擲異常。
仍以上面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'.
解決方法是:向customers欄位中新增related_name
引數.
customers = models.ManyToManyField(Place, related_name='provider')。
三、 代理模型
使用多表繼承時,父類的每個子類都會建立一張新資料表,通常情況下,這是我們想要的操作,因為子類需要一個空間來儲存不包含在父類中的資料。但有時,你可能只想更改模型在Python層面的行為,比如更改預設的manager管理器,或者新增一個新方法。
代理模型就是為此而生的。你可以建立、刪除、更新代理模型的例項,並且所有的資料都可以像使用原始模型(非代理類模型)一樣被儲存。不同之處在於你可以在代理模型中改變預設的排序方式和預設的manager管理器等等,而不會對原始模型產生影響。
宣告一個代理模型只需要將Meta中proxy的值設為True。
例如你想給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>
下面的例子通過代理進行排序,但父類卻不排序:
class OrderedPerson(Person):
class Meta:
# 現在,普通的Person查詢是無序的,而OrderedPerson查詢會按照`last_name`排序。
ordering = ["last_name"]
proxy = True
一些約束:
- 代理模型必須繼承自一個非抽象的基類,並且不能同時繼承多個非抽象基類;
- 代理模型可以同時繼承任意多個抽象基類,前提是這些抽象基類沒有定義任何模型欄位。
- 代理模型可以同時繼承多個別的代理模型,前提是這些代理模型繼承同一個非抽象基類。(早期Django版本不支援這一條)
代理模型的管理器
如不指定,則繼承父類的管理器。如果你自己定義了管理器,那它就會成為預設管理器,但是父類的管理器依然有效。如下例子:
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
四、 多重繼承
注意,多重繼承和多表繼承是兩碼事,兩個概念。
Django的模型體系支援多重繼承,就像Python一樣。如果多個父類都含有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欄位,並在直接的父類裡通過一個OneToOne欄位保持與祖先的關係,如下所示:
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,如果繼承的是一個非抽象基類,那麼子類與父類之間不可以有相同的欄位名!
比如下面是不行的!
class A(models.Model):
name = models.CharField(max_length=30)
class B(A):
name = models.CharField(max_length=30)
如果你執行python manage.py makemigrations
會彈出下面的錯誤:
django.core.exceptions.FieldError: Local field 'name' in class 'B' clashes with field of the same name from base class 'A'.
但是!如果父類是個抽象基類就沒有問題了(1.10版新增特性),如下:
class A(models.Model):
name = models.CharField(max_length=30)
class Meta:
abstract = True
class B(A):
name = models.CharField(max_length=30)