模型的继承 -- Django从入门到精通系列教程

该系列教程系我的原创,并完整发布在我的官网刘江的博客和教程

全部转载本文者,需在顶部显著位置注明原做者及www.liujiangblog.com官网地址。


不少时候,咱们都不是从‘一贫如洗’开始编写模型的,有时候能够从第三方库中继承,有时候能够从之前的代码中继承,甚至现写一个模型用于被其它模型继承。这样作的好处,我就不赘述了,每一个学习Django的人都很是清楚。python

类同于Python的类继承,Django也有完善的继承机制。shell

Django中全部的模型都必须继承django.db.models.Model模型,不论是直接继承也好,仍是间接继承也罢。数据库

你惟一须要决定的是,父模型是不是一个独立自主的,一样在数据库中建立数据表的模型,仍是一个只用来保存子模型共有内容,并不实际建立数据表的抽象模型。django

Django有三种继承的方式:app

  • 抽象基类:被用来继承的模型被称为Abstract base classes,将子类共同的数据抽离出来,供子类继承重用,它不会建立实际的数据表;
  • 多表继承:Multi-table inheritance,每个模型都有本身的数据库表;
  • 代理模型:若是你只想修改模型的Python层面的行为,并不想改动模型的字段,可使用代理模型。

注意!同Python的继承同样,Django也是能够同时继承两个以上父类的!ide

1、 抽象基类:

只须要在模型的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:rest

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'

这里有几点要特别说明:code

  • 抽象基类中有的元数据,子模型没有的话,直接继承;
  • 抽象基类中有的元数据,子模型也有的话,直接覆盖;
  • 子模型能够额外添加元数据;
  • 抽象基类中的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参数,这些问题就不存在了。


2、 多表继承

这种继承方式下,父类和子类都是独立自主、功能完整、可正常使用的模型,都有本身的数据库表,内部隐含了一个一对一的关系。例如:

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')。

3、 代理模型

使用多表继承时,父类的每一个子类都会建立一张新数据表,一般状况下,这是咱们想要的操做,由于子类须要一个空间来存储不包含在父类中的数据。但有时,你可能只想更改模型在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

4、 多重继承

注意,多重继承和多表继承是两码事,两个概念。

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)