很多时候,我们都不是从‘一穷二白’开始编写模型的,有时候可以从第三方库中继承,有时候可以从以前的代码中继承,甚至现写一个模型用于被其它模型继承。这样做的好处,我就不赘述了,每个学习Django的人都非常清楚。
类同于Python的类继承,Django也有完善的继承机制。
Django中所有的模型都必须继承django.db.models.Model模型,不管是直接继承也好,还是间接继承也罢。
你唯一需要决定的是,父模型是否是一个独立自主的,同样在数据库中创建数据表的模型,还是一个只用来保存子模型共有内容,并不实际创建数据表的抽象模型。
Django有三种继承的方式:

  • 抽象基类:被用来继承的模型被称为Abstract base classes,将子类共同的数据抽离出来,供子类继承重用,它不会创建实际的数据表;
  • 多表继承:Multi-table inheritance,每一个模型都有自己的数据库表,父子之间独立存在;
  • 代理模型:如果你只想修改模型的Python层面的行为,并不想改动模型的字段,可以使用代理模型。

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

一、 抽象基类:

只需要在模型的Meta类里添加abstract=True元数据项,就可以将一个模型转换为抽象基类。Django不会为这种类创建实际的数据库表,它们也没有管理器,不能被实例化也无法直接保存,它们就是被当作父类供起来,让子类继承的。抽象基类完全就是用来保存子模型们共有的内容部分,达到重用的目的。当它们被继承时,它们的字段会全部复制到子模型中。看下面的例子:

  1. from django.db import models
  2. class CommonInfo(models.Model):
  3. name = models.CharField(max_length=100)
  4. age = models.PositiveIntegerField()
  5. class Meta:
  6. abstract = True
  7. class Student(CommonInfo):
  8. home_group = models.CharField(max_length=5)

Student模型将拥有name,age,home_group三个字段,并且CommonInfo模型不能当做一个正常的模型使用。
那如果我想修改CommonInfo父类中的name字段的定义呢?在Student类中创建一个name字段,覆盖父类的即可。这其实就是很简单的Python语法。
那如果我不需要CommonInfo父类中的name字段呢?在Student类中创建一个name变量,值设为None即可。

抽象基类的Meta数据:

如果子类没有声明自己的Meta类,那么它将自动继承抽象基类的Meta类。
如果子类要设置自己的Meta属性,则需要扩展基类的Meta:

  1. from django.db import models
  2. class CommonInfo(models.Model):
  3. # ...
  4. class Meta:
  5. abstract = True
  6. ordering = ['name']
  7. class Student(CommonInfo):
  8. # ...
  9. class Meta(CommonInfo.Meta): # 注意这里有个继承关系
  10. db_table = 'student_info'

这里有几点要特别说明:

  • 抽象基类中有的元数据,子模型没有的话,直接继承;
  • 抽象基类中有的元数据,子模型也有的话,直接覆盖;
  • 子模型可以额外添加元数据;
  • 抽象基类中的abstract=True这个元数据不会被继承。也就是说如果想让一个抽象基类的子模型,同样成为一个抽象基类,那你必须显式的在该子模型的Meta中同样声明一个abstract = True
  • 有一些元数据对抽象基类无效,比如db_table,首先是抽象基类本身不会创建数据表,其次它的所有子类也不会按照这个元数据来设置表名。
  • 由于Python继承的工作机制,如果子类继承了多个抽象基类,则默认情况下仅继承第一个列出的基类的 Meta 选项。如果要从多个抽象基类中继承 Meta 选项,必须显式地声明 Meta 继承。例如:

    1. from django.db import models
    2. class CommonInfo(models.Model):
    3. name = models.CharField(max_length=100)
    4. age = models.PositiveIntegerField()
    5. class Meta:
    6. abstract = True
    7. ordering = ['name']
    8. class Unmanaged(models.Model):
    9. class Meta:
    10. abstract = True
    11. managed = False
    12. class Student(CommonInfo, Unmanaged):
    13. home_group = models.CharField(max_length=5)
    14. class Meta(CommonInfo.Meta, Unmanaged.Meta):
    15. pass

    警惕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模块:

  1. from django.db import models
  2. class Base(models.Model):
  3. m2m = models.ManyToManyField(
  4. OtherModel,
  5. related_name="%(app_label)s_%(class)s_related",
  6. related_query_name="%(app_label)s_%(class)ss",
  7. )
  8. class Meta:
  9. abstract = True
  10. class ChildA(Base):
  11. pass
  12. class ChildB(Base):
  13. pass
  14. 对于另外一个应用中的rare/models.py:
  15. from common.models import Base
  16. class ChildB(Base):
  17. 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参数,这些问题就不存在了。

二、 多表继承

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

  1. from django.db import models
  2. class Place(models.Model):
  3. name = models.CharField(max_length=50)
  4. address = models.CharField(max_length=80)
  5. class Restaurant(Place):
  6. serves_hot_dogs = models.BooleanField(default=False)
  7. serves_pizza = models.BooleanField(default=False)

Restaurant将包含Place的所有字段,并且各有各的数据库表和字段,比如:

  1. >>> Place.objects.filter(name="Bob's Cafe")
  2. >>> Restaurant.objects.filter(name="Bob's Cafe")

如果一个Place对象同时也是一个Restaurant对象,你可以使用小写的子类名,在父类中访问它,例如:

  1. >>> p = Place.objects.get(id=12)
  2. # 如果p也是一个Restaurant对象,那么下面的调用可以获得该Restaurant对象。
  3. >>> p.restaurant
  4. <Restaurant: ...>

但是,如果这个Place是个纯粹的Place对象,并不是一个Restaurant对象,那么上面的调用方式会弹出Restaurant.DoesNotExist异常。
让我们看一组更具体的展示,注意里面的注释内容。

  1. >>> from app1.models import Place, Restaurant # 导入两个模型到shell里
  2. >>> p1 = Place.objects.create(name='coff',address='address1')
  3. >>> p1 # p1是个纯Place对象
  4. <Place: Place object>
  5. >>> p1.restaurant # p1没有餐馆属性
  6. Traceback (most recent call last):
  7. File "<console>", line 1, in <module>
  8. File "C:\Python36\lib\site-packages\django\db\models\fields\related_descriptors.py", line 407, in __get__
  9. self.related.get_accessor_name()
  10. django.db.models.fields.related_descriptors.RelatedObjectDoesNotExist: Place has no restaurant.
  11. >>> r1 = Restaurant.objects.create(serves_hot_dogs=True,serves_pizza=False)
  12. >>> r1 # r1在创建的时候,只赋予了2个字段的值
  13. <Restaurant: Restaurant object>
  14. >>> r1.place # 不能这么调用
  15. Traceback (most recent call last):
  16. File "<console>", line 1, in <module>
  17. AttributeError: 'Restaurant' object has no attribute 'place'
  18. >>> r2 = Restaurant.objects.create(serves_hot_dogs=True,serves_pizza=False, name='pizza', address='address2')
  19. >>> r2 # r2在创建时,提供了包括Place的字段在内的4个字段
  20. <Restaurant: Restaurant object>
  21. >>> r2.place # 可以看出这么调用都是非法的,异想天开的
  22. Traceback (most recent call last):
  23. File "<console>", line 1, in <module>
  24. AttributeError: 'Restaurant' object has no attribute 'place'
  25. >>> p2 = Place.objects.get(name='pizza') # 通过name,我们获取到了一个Place对象
  26. >>> p2.restaurant # 这个P2其实就是前面的r2
  27. <Restaurant: Restaurant object>
  28. >>> p2.restaurant.address
  29. 'address2'
  30. >>> p2.restaurant.serves_hot_dogs
  31. True
  32. >>> lis = Place.objects.all()
  33. >>> lis
  34. <QuerySet [<Place: Place object>, <Place: Place object>, <Place: Place object>]>
  35. >>> lis.values()
  36. <QuerySet [{'id': 1, 'name': 'coff', 'address': 'address1'}, {'id': 2, 'name': '', 'address': ''}, {'id': 3, 'name': 'pizza', 'address': 'address2'}]>
  37. >>> lis[2]
  38. <Place: Place object>
  39. >>> lis[2].serves_hot_dogs
  40. Traceback (most recent call last):
  41. File "<console>", line 1, in <module>
  42. AttributeError: 'Place' object has no attribute 'serves_hot_dogs'
  43. >>> lis2 = Restaurant.objects.all()
  44. >>> lis2
  45. <QuerySet [<Restaurant: Restaurant object>, <Restaurant: Restaurant object>]>
  46. >>> lis2.values()
  47. <QuerySet [{'id': 2, 'name': '', 'address': '', 'place_ptr_id': 2, 'serves_hot_dogs': True, 'serves_pizza': False}, {'id': 3, 'name': 'pizza', 'address
  48. ': 'address2', 'place_ptr_id': 3, 'serves_hot_dogs': True, 'serves_pizza': False}]>
  49. 其内部隐含的OneToOne字段,形同下面所示:
  50. place_ptr = models.OneToOneField(
  51. Place, on_delete=models.CASCADE,
  52. parent_link=True,
  53. )

可以通过创建一个OneToOneField字段并设置 parent_link=True,自定义这个一对一字段。
从上面的API操作展示可以看出,这种继承方式还是有点混乱的,不如抽象基类来得直接明了。


Meta和多表继承

在多表继承的情况下,由于父类和子类都在数据库内有物理存在的表,父类的Meta类会对子类造成不确定的影响,因此,Django在这种情况下关闭了子类继承父类的Meta功能。这一点和抽象基类的继承方式有所不同。
但是,还有两个Meta元数据属性特殊一点,那就是orderingget_latest_by,这两个参数是会被继承的。因此,如果在多表继承中,你不想让你的子类继承父类的上面两种参数,就必须在子类中显示的指出或重写。如下:

  1. class ChildModel(ParentModel):
  2. # ...
  3. class Meta:
  4. # 移除父类对子类的排序影响
  5. ordering = []

多表继承和反向关联

因为多表继承使用了一个隐含的OneToOneField来链接子类与父类,所以象上例那样,你可以从父类访问子类。但是这个OnetoOneField字段默认的related_name值与ForeignKey和 ManyToManyField默认的反向名称相同。如果你与父类或另一个子类做多对一或是多对多关系,你就必须在每个多对一和多对多字段上强制指定related_name。如果你没这么做,Django就会在你运行或验证(validation)时抛出异常。
仍以上面Place类为例,我们创建一个带有ManyToManyField字段的子类:

  1. class Supplier(Place):
  2. customers = models.ManyToManyField(Place)
  3. 这会产生下面的错误:
  4. Reverse query name for 'Supplier.customers' clashes with reverse query
  5. name for 'Supplier.place_ptr'.
  6. HINT: Add or change a related_name argument to the definition for
  7. 'Supplier.customers' or 'Supplier.place_ptr'.
  8. 解决方法是:向customers字段中添加related_name参数.
  9. customers = models.ManyToManyField(Place, related_name='provider')。

三、 代理模型

使用多表继承时,父类的每个子类都会创建一张新数据表,通常情况下,这是我们想要的操作,因为子类需要一个空间来存储不包含在父类中的数据。但有时,你可能只想更改模型在Python层面的行为,比如更改默认的manager管理器,或者添加一个新方法。
代理模型就是为此而生的。你可以创建、删除、更新代理模型的实例,并且所有的数据都可以像使用原始模型(非代理类模型)一样被保存。不同之处在于你可以在代理模型中改变默认的排序方式和默认的manager管理器等等,而不会对原始模型产生影响。
代理模型其实就是给原模型换了件衣服(API),实际操作的还是原来的模型和数据。
声明一个代理模型只需要将Meta中proxy的值设为True。
例如你想给Person模型添加一个方法。你可以这样做:

  1. from django.db import models
  2. class Person(models.Model):
  3. first_name = models.CharField(max_length=30)
  4. last_name = models.CharField(max_length=30)
  5. class MyPerson(Person):
  6. class Meta:
  7. proxy = True
  8. def do_something(self):
  9. # ...
  10. pass

MyPerson类将操作和Person类同一张数据库表。并且任何新的Person实例都可以通过MyPerson类进行访问,反之亦然。

  1. >>> p = Person.objects.create(first_name="foobar")
  2. >>> MyPerson.objects.get(first_name="foobar")
  3. <MyPerson: foobar>
  4. 下面的例子通过代理进行排序,但父类却不排序:
  5. class OrderedPerson(Person):
  6. class Meta:
  7. # 现在,普通的Person查询是无序的,而OrderedPerson查询会按照`last_name`排序。
  8. ordering = ["last_name"]
  9. proxy = True

一些约束:

  • 代理模型必须继承自一个非抽象的基类,并且不能同时继承多个非抽象基类;
  • 代理模型可以同时继承任意多个抽象基类,前提是这些抽象基类没有定义任何模型字段。
  • 代理模型可以同时继承多个别的代理模型,前提是这些代理模型继承同一个非抽象基类。

如果你理解透彻了代理模型的本质,那么上面的三条约束是顺理成章的。
代理模型的管理器
如不指定,则继承父类的管理器。如果你自己定义了管理器,那它就会成为默认管理器,但是父类的管理器依然有效。如下例子:

  1. from django.db import models
  2. class NewManager(models.Manager):
  3. # ...
  4. pass
  5. class MyPerson(Person):
  6. objects = NewManager()
  7. class Meta:
  8. proxy = True

如果你想要向代理中添加新的管理器,而不是替换现有的默认管理器,你可以创建一个含有新的管理器的基类,并在继承时把他放在主基类的后面:

  1. # Create an abstract class for the new manager.
  2. from django.db import models
  3. class NewManager(models.Manager):
  4. # ...
  5. pass
  6. class ExtraManagers(models.Model):
  7. secondary = NewManager()
  8. class Meta:
  9. abstract = True
  10. class MyPerson(Person, ExtraManagers):
  11. class Meta:
  12. proxy = True

四、 多重继承

注意,多重继承和多表继承是两码事,两个概念。
Django的模型体系支持多重继承,就像Python一样。如果多个父类都含有Meta类,则只有第一个父类的会被使用,剩下的会忽略掉。
一般情况,能不要多重继承就不要,尽量让继承关系简单和直接,避免不必要的混乱和复杂。
请注意,继承同时含有相同id主键字段的类将抛出异常。为了解决这个问题,你可以在基类模型中显式的使用AutoField字段。如下例所示:

  1. class Article(models.Model):
  2. article_id = models.AutoField(primary_key=True)
  3. ...
  4. class Book(models.Model):
  5. book_id = models.AutoField(primary_key=True)
  6. ...
  7. class BookReview(Book, Article):
  8. pass

或者使用一个共同的祖先来持有AutoField字段,并在直接的父类里通过一个OneToOne字段保持与祖先的关系,如下所示:

  1. class Piece(models.Model):
  2. pass
  3. class Article(Piece):
  4. article_piece = models.OneToOneField(Piece, on_delete=models.CASCADE, parent_link=True)
  5. ...
  6. class Book(Piece):
  7. book_piece = models.OneToOneField(Piece, on_delete=models.CASCADE, parent_link=True)
  8. ...
  9. class BookReview(Book, Article):
  10. pass

警告

在Python语言层面,子类可以拥有和父类相同的属性名,这样会造成覆盖现象。但是对于Django,如果继承的是一个非抽象基类,那么子类与父类之间不可以有相同的字段名!
比如下面是不行的!

  1. class A(models.Model):
  2. name = models.CharField(max_length=30)
  3. class B(A):
  4. 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. class A(models.Model):
  2. name = models.CharField(max_length=30)
  3. class Meta:
  4. abstract = True
  5. class B(A):
  6. name = models.CharField(max_length=30)

五、用包来组织模型

在我们使用python manage.py startapp xxx命令创建新的应用时,Django会自动帮我们建立一个应用的基本文件组织结构,其中就包括一个models.py文件。通常,我们把当前应用的模型都编写在这个文件里,但是如果你的模型很多,那么将单独的models.py文件分割成一些独立的文件是个更好的做法。
首先,我们需要在应用中新建一个叫做models的包,再在包下创建一个__init__.py文件,这样才能确立包的身份。然后将models.py文件中的模型分割到一些.py文件中,比如organic.pysynthetic.py,然后删除models.py文件。最后在__init__.py文件中导入所有的模型。如下例所示:

  1. # myapp/models/__init__.py
  2. from .organic import Person
  3. from .synthetic import Robot

要显式明确地导入每一个模型,而不要使用from .models import *的方式,这样不会混淆命名空间,让代码更可读,更容易被分析工具使用。