標籤:blog 使用 os io strong 資料 for ar
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 將拋出異常)。
class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() class Meta: abstract = Trueclass Student(CommonInfo): home_group = models.CharField(max_length=5)
sqlall結果:
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 內嵌類,對其進行擴充。例如:
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結果:
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:
class Base(models.Model): m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related") class Meta: abstract = Trueclass ChildA(Base): passclass ChildB(Base): pass
在另外一個app中,rare/models.py:
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 來實現)。 例如:
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:
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 中也是有效,只不過資料儲存在另外一張資料表當中。所以下面兩個語句都是可以啟動並執行:
>>> Place.objects.filter(name="Bob‘s Cafe")>>> Restaurant.objects.filter(name="Bob‘s Cafe")
如果你有一個 Place,那麼它同時也是一個 Restaurant, 那麼你可以使用子 model 的小寫形式從 Place 對象中獲得與其對應的 Restaurant 對象:
>>> 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 異常:
>>> from myapp.models import Place,Restaurant>>> p=Place.objects.create(name=‘Place‘,address=‘Place‘)>>> p.restaurantDoesNotExist: Place has no restaurant.
也就是說,建立Place執行個體的同時不會建立Restaurant,但是建立Restaurant執行個體的同時會建立Place執行個體:
>>>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 屬性,它就會從父類中繼承這些屬性。
如果父類有了排序設定,而你並不想讓子類有任何排序設定,你就可以顯式地禁用排序:
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 欄位的子類:
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 (它被用在你的模板中)添加一個方法:
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:
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 訪問,反之亦然:
>>> p = Person.objects.create(first_name="foobar")>>> MyPerson.objects.get(first_name="foobar")<MyPerson: foobar>
你也可以使用代理 model 給 model 定義不同的預設排序設定。Django 內建的 User model 沒有定義排序設定(這是故意為之,是因為排序開銷極大,我們不想在擷取使用者時浪費額外資源)。你可以利用代理對 username 屬性進行排序,這很簡單:
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,例如:
class NewManager(models.Manager): # ... passclass MyPerson(Person): objects = NewManager() class Meta: proxy = True
如果你想給代理添加一個新的 manager ,卻不想替換已有的預設 manager ,那麼你可以參考 自訂 manager (custom manager) 中提到的方法:建立一個包含新 manager 的基類,然後放在主基類後面繼承:
class ExtraManagers(models.Model): secondary = NewManager() class Meta: abstract = Trueclass 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 未必適用。在 manager documentation 有詳細介紹。
我們實現了這兩種特性(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 異常。