购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.6 Django模型继承

本节将介绍Django模型的继承,包括模型的元数据Meta继承、模型的抽象基类、模型的多表继承、模型之代理模式、模型之多重继承以及用包来组织模型等内容。

3.6.1 什么是模型继承

Django模型继承与普通类的继承基本一致,在Python语言中的工作方式也几乎完全相同,同时也遵循Django官方文档中关于模型的三点描述(参见3.1.2小节)。Django模型继承的基类需要继承自django.db.models.Model。

开发人员在使用模型继承时,只需要决定父类模型是否需要拥有其数据表,或者父类模型是仅作为承载子类中可见的公共信息的载体。

关于Django模型继承有以下三种可用的集成风格,具体描述如下:

● 建议将父类设计为抽象基类来使用,仅用于作为子类的公共信息的载体,免去在每个子类中将这些代码重复写一遍。

● 假如要继承一个模型,并且想要每个模型都有对应的数据表,建议使用多表继承方式。

● 假如只想修改模型的Python级行为,而不是以任何形式修改模型字段,建议使用代理模型方式。

3.6.2 抽象基类

在Django模型中,抽象基类在将公共信息放入很多模型时会非常有用。

如果要实现一个抽象基类,需要先编写好一个基类,然后在该基类中添加Meta类,并填入属性abstract=True。因为这个基类被设计为抽象基类,模型就不会创建任何数据表了。然后,当这个抽象基类用作其他模型类的基类时,其自有的字段会自动添加到子类中。

关于抽象基类的使用方法,请看下面的代码示例:

【代码3-15】

【代码分析】

● 第03~08行代码中,定义了一个描述通用信息的抽象基类(CommonInfo)。

● 第04~05行代码中,定义了一组两个关于姓名(name)和年龄(age)的字段属性。

● 第07~08行代码中,在Meta类中添加了属性abstract=True,表明该类(CommonInfo)为抽象基类。

● 第10~11行代码中,定义了一个关于用户信息的子类(UserInfo)。第10行代码中,定义了子类(UserInfo)继承自基类(CommonInfo)。第11行代码中,定义了一个关于家庭组的字段属性(home_group)。

● 子类(UserInfo)因继承自基类(CommonInfo),所以顺带继承了基类(CommonInfo)中的姓名(name)属性和年龄(age)属性,这样子类(UserInfo)就拥有了3个字段属性(name、age和home_group)。

另外着重补充一下,因为基类(CommonInfo)是一个抽象基类,所以其不能作为普通的Django模型来使用。也就是说,基类(CommonInfo)不会生成数据表,也没有管理器,同时也不能被实例化和保存。

在Django模型中,从抽象基类继承来的字段可被其他字段或值重写,或者可使用“None”标识符进行删除。

对开发人员来讲,从抽象基类继承就是一种比较理想的方式了。抽象基类继承方式提供了一种在Python级别中提取公共信息的方法,同时仍会在子类模型中创建数据表。

3.6.3 Meta继承

在Django模型继承中,当一个抽象基类被设计完成后,会将该基类中所定义的Meta内部类以属性的形式提供给子类。还有,如果子类未定义自己的Meta类,那么它就会默认继承抽象基类的Meta类。

关于Meta类的继承,这里大致总结如下:

● 抽象基类中有的元数据属性,子模型没有的话,直接继承。

● 抽象基类中有的元数据属性,子模型也有的话,直接覆盖。

● 子模型可以额外添加元数据属性。

● 抽象基类中的abstract=True属性不会被子类所继承。

● 有一些元数据属性(如:db_table)对抽象基类是无效的。

首先,如果子类要设置自己的Meta属性,则必须要扩展抽象基类的Meta类。具体请看下面的代码示例:

【代码3-16】

【代码分析】

● 第03~07行代码中,定义了一个描述通用信息的抽象基类(CommonInfo)。

● 第05~07行代码中,在Meta类中添加了属性abstract=True,表明该类(CommonInfo)为抽象基类。

● 第09~14行代码中,定义了一个关于学生信息的子类(StudentInfo)。

● 第11行代码中,定义了自己的Meta类子类,并继承自基类的Meta类(CommonInfo.Meta)。

● 第12行代码中,定义了一个字段属性(db_table)。注意,该属性就是子类(StudentInfo)所扩展的、属于自己的Meta属性。

如前文所述,元数据属性(db_table)对抽象基类是无效的。

首先,对于抽象基类本身而言,是不会创建数据表的。所有子类也不会按照这个元数据属性来设置表名。

其次,如果想让一个抽象基类的子类也同样成为一个抽象基类,则必须显式地在该子类的Meta类中同样声明一个abstract=True属性。具体请看下面的代码示例:

【代码3-17】

【代码分析】

● 第03~07行代码中,定义了一个描述通用信息的抽象基类(CommonInfo)。

● 第05~07行代码中,在Meta类中添加了属性abstract=True,表明该类(CommonInfo)为抽象基类。

● 第09~13行代码中,定义了一个继承自抽象基类(CommonInfo)的用户信息子类(UserInfo)。

● 第11行代码中,定义了自己的Meta类子类,并继承自基类的Meta类(CommonInfo.Meta)。

● 第12行代码中,在Meta类中添加了属性abstract=True,表明该子类(UserInfo)仍为抽象基类。

● 第15~20行代码中,定义了一个继承自抽象基类(UserInfo)的学生信息子类(StudentInfo)。

● 第17行代码中,定义了自己的Meta类子类,并继承自基类的Meta类(UserInfo.Meta)。

● 第18行代码中,定义了一个字段属性(db_table)。注意,该属性就是子类(StudentInfo)所扩展的属于自己的Meta属性。

再有,基于Python语法继承的工作机制,如果子类继承了多个抽象基类,则默认情况下仅继承第一个列出基类的Meta选项。如果要从多个抽象基类中继承Meta选项,则必须显式地声明Meta继承。具体请看下面的代码示例:

【代码3-18】

【代码分析】

● 第03~09行代码中,定义了第一个描述通用信息的抽象基类(CommonInfo)。

● 第07~09行代码中,在Meta类中添加了属性abstract=True,表明该类(CommonInfo)为抽象基类。

● 第11~14行代码中,定义了第二个抽象基类(Unmanaged)。其中,第12~14行代码中,在Meta类中添加了属性abstract=True,表明该类(Unmanaged)为抽象基类。

● 第16~20行代码中,定义了一个同时继承自抽象基类(CommonInfo和Unmanaged)的学生信息子类(StudentInfo)。

● 第19行代码中,定义了自己的Meta类子类,并继承自基类的Meta类(CommonInfo.Meta和Unmanaged.Meta),该定义方式就是显式地声明Meta类继承。

3.6.4 related_name和related_query_name属性

在Django模型继承中,如果在“外键”或“多对多字段”中使用了related_name属性或related_query_name属性,则必须为该字段提供一个独一无二的反向名字和查询名字。但是,这样在抽象基类中一般会引发问题,因为基类中的字段都被子类继承,且保持了同样的值,当然也包括related_name属性和related_query_name属性。

为了解决上述问题,当在抽象基类中(也只能是在抽象基类中)使用related_name属性和related_query_name属性时,部分值需要包含“%(app_label)s”和“%(class)s”,具体说明如下:

● %(class)s:用该字段的子类的小写类名替换。

● %(app_label)s:用小写的、包含子类的应用名替换。每个安装的应用名必须是唯一的,应用内的每个模型类名也必须是唯一的,故替换后的名字也是唯一的。

关于related_name属性和related_query_name属性的使用,请看下面的代码示例。

【代码3-19】

【代码分析】

● 第01~20行代码中,定义了第一个Python应用(common app)。

● 第06~14行代码中,定义了一个抽象基类(Base)。

● 第07~11行代码中,定义了一个“多对多”属性(m2m),并使用了related_name属性和related_query_name属性。

● 第13~14行代码中,在Meta类中添加了属性abstract=True,表明该类(Base)为抽象基类。

● 第16~17行和第19~20行代码中,定义了两个继承自抽象基类(Base)的子类(ChildA和ChildB)。common.ChildA.m2m字段的反转名是common_childa_related,反转查询名是common_childas。common.ChildB.m2m字段的反转名是common_childb_related,反转查询名是common_childbs。

● 第22~28行代码中,定义了第二个Python应用(another app)。

● 第27~28行代码中,定义了一个继承自抽象基类(Base)的子类(ChildB)。其中,another.ChildB.m2m字段的反转名是another_childb_related,反转查询名是another_childbs。

如何使用“%(class)s”和“%(app_label)s”构建关联名字和关联查询名,取决于开发人员。不过,如果在设计时忘了使用“%(class)s”和“%(app_label)s”,Django会在执行系统检查或运行迁移时抛出错误。如果设计时未指定抽象基类中的related_name属性,则默认的反转名会是子类名后接“_set”。

3.6.5 多表继承

在Django模型继承中,所支持的第二种模型继承方式是:层次结构中的每个模型都是一个单独的模型。每个模型都指向分离的数据表,且可被独立查询和创建。在继承关系中,子类和父类之间通过一个自动创建的OneToOneField连接。请看下面的代码示例:

【代码3-20】

【代码分析】

● 第03~05行代码中,定义了一个用于表示地点的抽象基类(Place)。其中,第04行和第05行代码定义了两个属性(name和address),分别用于表示名字(name)和地址(address)。

● 第07~10行代码中,定义了一个继承自抽象基类(Place)的、用于表示酒店的子类(Hotel)。其中,第08~10行代码定义了三个属性(roomA、roomB和roomC),分别用于表示酒店房间的三种类型。

另外根据继承规则,抽象基类(Place)的所有属性在子类(Hotel)中也均可以使用。因此,基于【代码3-20】的模型设计,可以进行如下的操作:

     >>> Place.objects.filter(name="King's Place")
     >>> Hotel.objects.filter(name="King's Place ")

假若有一个Place对象同时也是Hotel对象,可以通过小写的模型名将Place对象转为Hotel对象。

     >>> p = Place.objects.get(id=10)
     # If p is a Hotel object, this will give the child class:
     >>> p.hotel
     <Hotel:...>

在上述例子中,如果p不是一个Hotel对象,而仅仅是一个Place对象(或者是其他类的父类),指向p.hotel则会抛出一个Hotel.DoesNotExist类型的异常。

在Hotel模型中自动创建的、连接至Place模型的OneToOneField看起来类似下面的代码:

【代码3-21】

【代码分析】

● 设计时可以在Hotel中重写该字段,通过声明自己的OneToOneField并在其中设置属性parent_link=True。

3.6.6 Meta和多表继承

在Django模型多表继承中,子类不会继承父类中的Meta类。所有的Meta类属性已被应用至父类,如果在子类中再次应用,则会导致行为冲突。因此,子类模型无法访问父类中的Meta类。

不过也有例外情况,若子类未指定ordering属性或get_latest_by属性,则子类会从父类继承这些属性。而如果父类有排序属性,在设计子类时并不期望有排序属性,则可以显式进行禁止。请看下面的代码示例:

【代码3-22】

【代码分析】

● 第01~07行代码中,定义了一个子类(ChildModel)模型,继承自父类(ParentModel)模型。

● 第03~05行代码中,定义了子类(ChildModel)的Meta类。其中,第05行代码定义了一个空的ordering属性,就实现了显式地禁止ordering排序属性操作。

3.6.7 继承与反向关系

在Django模型继承中,由于多表继承使用隐式的OneToOneField连接子类和父类,所以直接从父类访问子类是可能的。同时,使用的名字是“ForeignKey”和“ManyToManyField”关系的默认值。

但是,如果在继承父类模型的子类中添加了这些关联,则必须指定related_name属性。而假如不小心遗漏了,Django框架就会抛出一个合法性错误。

例如,使用上面【代码3-20】中的Place基类创建另一个子类(Restaurant),且包含一个“ManyToManyField”,请看下面的代码示例:

【代码3-23】

     01  class Restaurant(Place):
     02    customers = models.ManyToManyField(Place)
     03    #...
     04    pass

【代码分析】

● 第01~04行代码中,定义了一个子类(Restaurant)模型,继承自父类(Place)模型。其中,在第02行代码中定义了子类与父类的ManyToManyField关系。

注意

如果子类(Restaurant)中没有定义“elated_name属性,则会导致出现异常。

如果想要避免出现【代码3-23】的错误异常,就需要将related_name属性添加进ManyToManyField关系中,请看下面的代码示例:

【代码3-24】

     01  class Restaurant(Place):
     02    customers = models.ManyToManyField(Place, related_name='provider')
     03    #...
     04    pass

【代码分析】

● 第02行代码在定义了子类与父类的ManyToManyField关系中,添加了属性related_name='provider'。

3.6.8 代理模型

在Django模型中使用多表继承时,每个子类模型都会创建一张新表,这是因为子类需要一个地方存储基类中不存在的额外数据字段。但是,有时候如果只想修改模型的Python级行为(比如:修改默认管理器或添加一个方法),这时就需要使用代理模型了。

使用代理模型继承的目标,就是为原模型创建一个“代理”。在设计时,可以创建、删除或更新代理模型的实例,全部数据都会存储成像使用原模型(未代理)一样的形式。这里稍微有些不同的是,在设计时可以修改代理默认的模型排序和默认管理器,而不需要修改原模型。

使用代理模型时,可以像普通模型一样进行声明,只需要告诉Django框架这是一个代理模型,通过将Meta类的proxy属性设置为True即可。

例如,如果打算为一个Person模型添加一个方法,可以参照下面的代码示例:

【代码3-25】

【代码分析】

● 第03~05行代码中,定义了一个类(Person),用于描述人的模型。其中,在第04~05行代码中定义了两个属性(first_name和last_name)。

● 第03~05行代码中,定义了一个类(Child),继承自父类(Person),用于描述孩子的模型。

● 第04~05行代码中,通过Meta类定义了proxy=True属性,表明该类是一个代理类。

● 第11~13行代码中,为Person模型添加了一个方法(do_something)。

根据上面的代码,子类(Child)与父类(Person)操作同一张数据表。另外,Person模型的实例能通过Child模型访问,反之亦然。

     >>> p = Person.objects.create(first_name="king")
     >>> Child.objects.get(first_name="king")
     <Child: king>

使用代理模型还可以定义模型的另一种不同的默认排序方法。比如想要在使用代理时总是根据last_name属性进行排序,解决方法请看下面的代码示例:

【代码3-26】

     01  class OrderedPerson(Person):
     02      class Meta:
     03          ordering = ["last_name"]
     04          proxy = True
     05      #...
     06      pass

【代码分析】

● 通过上面的定义,普通Person模型的查询结果就不会被排序了。

● 通过第03行代码的定义,OrderdPerson模型的查询结果会按照last_name属性排序。

再次查看一下【代码3-24】,当使用Person模型对象查询时,Django框架不会返回Child模型对象。对于Person模型对象的查询结果集,总是返回相对应的类型(QuerySet仍会返回请求的模型)。

代理对象存在的全部意义是帮助开发人员复用原Person模型所提供的代码,以及自定义的功能代码(并未依赖其他代码)。如果尝试使用自己创建的代码,在任何地方去替换Person(或任何其他)模型上定义的代理对象,这将会是一种无效的途径。

在使用代理模型时,对于其继承的基类是有约束条件的。一个代理模型必须继承自一个非抽象模型类,而不能继承自多个非抽象模型类。原因在于,代理模型无法在不同数据表之间提供任何行间连接。一个代理模型可以继承任意数量的抽象模型类(假设其没有定义任何模型字段),一个代理模型也可以继承任意数量的代理模型(只需共享同一个非抽象父类)。

另外,如果未在代理模型中指定模型管理器,其默认会从父类模型中继承。而如果在代理模型中指定了管理器,其就会成为默认的管理器,同时父类中所定义的管理器也仍是可用的。

基于【代码3-25】和【代码3-26】的示例,我们可以在查询Person模型时这样来修改默认的管理器。请看下面的代码示例:

【代码3-27】

【代码分析】

● 第03~05行代码中,在不替换已存在的默认管理器情况下,为代理模型添加了新管理器(NewManager)。

在官方文档中,“自定义管理器”介绍了一种技巧,即创建一个包含新管理器的基类,然后在继承列表中的主类后追加这个新管理器的基类。不过,通常情况下不需要这么做。

3.6.9 代理模型继承和未托管模型

Django框架的代理模型继承看上去与创建未托管模型非常相似,未托管模型通过在模型的Meta类中定义managed”属性来实现。

创建未托管模型的方法主要通过配置Meta.db_table项来实现。未托管模型将对现有模型进行阴影处理,并添加一些Python方法。

注意

这个配置过程非常烦琐且易错,原因在于进行任何修改都需要两个副本保持同步。

相对于未托管模型,代理模型意在表现为与所代理的模型一样—总是与父模型保持一致。因为,代理模型将直接从父模型类继承字段和管理器。

关于代理模型继承和未托管模型的通用性规则,主要描述如下:

● 当克隆一个已存在模型或数据表,且不打算要全部的原数据表列时,请配置Meta.managed=False选项。该选项用于不在Django框架控制下的数据库视图和数据库表。

● 如果只想修改模型的Python级行为,同时要保留原有字段,请配置Meta.proxy=True选项。这个配置将使得代理模型在保存数据时,数据结构与原模型的保持一致。

3.6.10 多重继承

Django模型也支持使用多重继承,这一点与Python语法中的继承是一致的。Django模型多重继承就是同时继承多个父类模型,父类中第一个出现的基类(如:Meta类)是默认被使用的。如果存在多个父类包含Meta类的情况,则只有第一个会被使用,其他的都会被忽略。

一般来讲,在设计时需要同时继承多个父类的情况并不多见。比较常见的应用场景是“混合”类,所谓“混合”就是为每个继承类添加额外的字段或方法,尽量保持继承层级尽可能的简单和直接,这样做的目的就是,保证将来不会出现无法确认某段信息是从何而来的困扰。

注意

在继承自多个包含id主键的字段时会抛出错误。如果想要避免出现此问题,可以通过在基类中显式地使用AutoField方法,从而正确地使用多重继承。

关于多重继承,请看下面的代码示例:

【代码3-28】

【代码分析】

● 第01~03行代码中,定义了第一个类(Article),用于描述文章的模型。其中,在第02行代码中定义了一个id属性(article_id),该属性通过AutoField方法定义了主键(primary_key=True)。

● 第05~07行代码中,定义了第二个类(Book),用于描述书籍的模型。其中,在第06行代码中定义了一个id属性(book_id),该属性通过AutoField方法定义了主键(primary_key=True)。

● 第09~10行代码中,定义了一个子类(BookArticle),用于描述书籍和文章的模型,同时继承自Article模型和Book模型。

除了上面显示地使用AutoField方法之外,还可以通过在公共祖先中存储AutoField的方式,来实现同时包含多个id属性的操作。该方式要求对每个父类模型和公共祖先显式地使用OneToOneField方法,避免与子类自动生成或继承的字段发生冲突。请看下面的代码示例:

【代码3-29】

【代码分析】

● 第01~02行代码中,定义了一个基类(Piece)。

● 第04~09行代码中,定义了第一个继承自基类(Piece)的类(Article),用于描述文章的模型。其中,在第05~08行代码中定义了一个属性(article_piece),该属性通过OneToOneField方法获取。

● 第11~16行代码中,定义了第二个继承自基类(Piece)的类(Book),用于描述文章的模型。其中,在第12~15行代码中定义了一个属性(book_piece),该属性通过OneToOneField方法获取。

● 第18~19行代码中,定义了一个子类(BookArticle),用于描述书籍和文章的模型,同时继承自Article模型和Book模型。 nS5RM6J8CgBB3adT93j36d7g7tQlevLGU6cWmVbCuTztWx2Qinys0xvz+5vf3F67

点击中间区域
呼出菜单
上一章
目录
下一章
×