模型的繼承 -- Django從入門到精通系列教程

来源:https://www.cnblogs.com/feixuelove1009/archive/2018/02/06/8420751.html
-Advertisement-
Play Games

該系列教程系個人原創,並完整發佈在個人官網 "劉江的博客和教程" 所有轉載本文者,需在頂部顯著位置註明原作者及www.liujiangblog.com官網地址。 Python及Django學習QQ群:453131687 很多時候,我們都不是從‘一窮二白’開始編寫模型的,有時候可以從第三方庫中繼承,有 ...


該系列教程系個人原創,並完整發佈在個人官網劉江的博客和教程

所有轉載本文者,需在頂部顯著位置註明原作者及www.liujiangblog.com官網地址。

Python及Django學習QQ群:453131687


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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言:最近要使用百度地圖實現樓盤可視化的功能,因此最基礎的功能就是將地圖網格化以後實現不同地域的樓盤劃分; 1,自行去百度地圖的開放平臺申請秘鑰哈,這裡我就把自己的秘鑰貼出來了;ak=A3CklGvnFOjkAzKzay2dySgfdig0GKz4 2,新建一個簡單頁面,下麵我把自己的頁面貼出來 3 ...
  • 本文最初發表於 "博客園" ,併在 "GitHub" 上持續更新 前端的系列文章 。歡迎在GitHub上關註我,一起入門和進階前端。 以下是正文。 前言 jQuery提供的一組網頁中常見的動畫效果,這些動畫是標準的、有規律的效果;同時還提供給我們了自定義動畫的功能。 顯示動畫 方式一: 解釋:無參數 ...
  • 1、window.screen.height window.screen.height:設備顯示屏的高度 (1)解析度為1080px的顯示屏 (2)手機屏 2、window.screen.availHeight 屏幕的可用高度 (1)解析度為1080px的顯示屏 (2)手機屏 3、document. ...
  • 容器的屬性 項目的屬性 ...
  • target:指定框架集中的哪個框架來裝在另一個資源,該屬性可以是_self、_blank、_top、_parent四個值,分別代表使用自身、新視窗、頂層框架、父框架來裝載新資源。 alt只是在圖片無法載入的還是才會顯示出提示文字,如果想讓圖片無論怎樣都顯示,用title 如果希望獲得最佳表單性能, ...
  • 對於剛接觸ubuntu的同學來說,一切都是新的,一切都是那麼熟悉而又不熟悉的.不管是作為一個前端工程師還是一個後端工程師,我相信大家知道nodejs,但是如果希望自己能夠在ubuntu上面使用nodejs,是需要給點功夫去做的. 當然對於一個ubuntuer來說 這個命令就再熟悉不過了,也是經常用的 ...
  • 登錄認證幾乎是任何一個系統的標配,web 系統、APP、PC 客戶端等,好多都需要註冊、登錄、授權認證。 場景說明 以一個電商系統,假設淘寶為例,如果我們想要下單,首先需要註冊一個賬號。擁有了賬號之後,我們需要輸入用戶名(比如手機號或郵箱)、密碼完成登錄過程。之後如果你在一段時間內再次進入系統,是不 ...
  • https://www.cnblogs.com/jiese/p/3164940.html 將抽象部份與它的實現部份分離,使它們都可以獨立地變化。 橋接模式號稱設計模式中最難理解的模式之一,關鍵就是這個抽象和實現的分離非常讓人奇怪,大部分人剛看到這個定義的時候都會認為實現就是繼承自抽象,那怎麼可能將他 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...