很多時候,我們都不是從‘一窮二白’開始編寫模型的,有時候可以從第三方庫中繼承,有時候可以從以前的代碼中繼承,甚至現寫一個模型用於被其它模型繼承。這樣做的好處,我就不贅述了,每個學習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