本节主要介绍Django模型的继承,包括模型的抽象基类、Meta继承、related_name和related_query_name属性、多表继承、Meta和多表继承、继承与反向关系、代理模型、代理模型继承和未托管模型,以及多重继承等内容。
Django模型的继承与普通类的继承基本一致,在Python语言中的工作方式也几乎完全相同,同时也要遵循Django官方文档中关于模型的3点描述(参看3.1.2节)。Django模型继承的基类需要继承自django.db.models.Model。
设计人员在使用模型继承时,只需要决定父类模型是否需要拥有数据表,或者父类模型是否仅作为承载子类中可见的公共信息的载体。
Django模型的继承有以下3种可用的集成风格:
· 建议将父类设计为抽象基类来使用,仅用于作为子类的公共信息的载体,免去在每个子类中将这些代码都重复写一遍。
· 假如要继承一个模型,并且想要每个模型都有对应的数据表,则建议使用多表继承方式。
· 假如只想修改模型的Python级行为,而不是以任何形式修改模型字段,则建议使用代理模型方式。
在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级别中提取公共信息的方法,同时仍会在子类模型中创建数据表。
在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类继承。
在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”。
在Django模型继承中,支持的第二种模型继承方式是层次结构中的每个模型都是一个单独的模型。每个模型都指向分离的数据表,并且可以被独立查询和创建。在继承关系中,子类和父类之间通过一个自动创建的OneToOneField进行连接。示例代码如下:
【代码3-20】
【代码分析】
在第03~05行代码中,定义了一个用于表示地点的抽象基类Place。其中,第04行和第05行代码定义了两个属性name和address,分别用于表示名字和地址。
在第07~10行代码中,定义了一个继承自抽象基类Place的、用于表示酒店的子类Hotel。其中,第08~10行代码定义了3个属性roomA、roomB和roomC,分别用于表示3种酒店房间类型。
另外根据继承规则,抽象基类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】
01 place_ptr = models.OneToOneField( 02 Place, on_delete=models.CASCADE, 03 parent_link=True, 04 )
【代码分析】
设计时可以在Hotel中通过声明自己的OneToOneField,并在其中设置“parent_link=True”属性来重写该字段。
在Django模型多表继承中,子类不会继承父类中的Meta类。所有的Meta类属性已被应用至父类,在子类中再次应用则会导致行为冲突。因此,子类模型无法访问父类中的Meta类。
不过也有例外情况,若子类未指定ordering属性或get_latest_by属性,则子类会从父类继承这些属性;而如果父类有排序属性,而设计子类时并不期望有排序属性,则可以显式进行禁止。示例代码如下:
【代码3-22】
【代码分析】
在第01~07行代码中,定义了一个子类ChildModel,它继承自父类ParentModel,具体内容说明如下:
· 在第03~05行代码中,定义了子类ChildModel的Meta类。其中,第05行代码定义了一个为空的ordering属性,实现了显式地禁止ordering排序属性的操作。
在Django模型继承中,由于多表继承使用隐式的OneToOneField连接子类和父类,因此直接从父类访问子类是可能的。同时,使用的名字是ForeignKey和ManyToManyField关系的默认值。
但是,如果在继承父类模型的子类中添加了这些关联,则必须指定related_name属性。如果不小心遗漏了,Django框架就会抛出一个合法性错误。
例如,使用【代码3-20】中的Place基类创建另一个子类Restaurant,且包含一个ManyToManyField,示例代码如下:
【代码3-23】
【代码分析】
第01~04行代码中,定义了一个子类Restaurant,它继承自父类Place。其中,在第02行代码中定义了子类与父类的ManyToManyField关系。
注意,由于子类Restaurant中没有定义related_name属性,因此会出现异常。
如果想让【代码3-23】的错误异常不出现,就需要将related_name属性添加进ManyToManyField关系中,示例代码如下:
【代码3-24】
【代码分析】
第02行代码在定义子类与父类的ManyToManyField关系中,添加了“related_name='provider'”属性。
在Django模型中使用多表继承时,每个子类模型都会创建一张新表,这是因为子类需要一个地方存储基类中不存在的数据字段。但是,有时候如果只想修改模型的Python级行为(例如修改默认管理器或添加一个方法),这时就需要使用代理模型了。
使用代理模型继承的目标就是为原模型创建一个“代理”。在设计时可以创建、删除或更新代理模型的实例,全部数据都会存储成与使用原模型(未代理)一样的形式。这里稍微有些不同的是,在设计时可以修改代理默认的模型排序和管理器,而不需要修改原模型。
使用代理模型时就像使用普通模型一样进行声明,只需要告诉Django框架这是一个代理模型,通过将Meta类的proxy属性设置为True即可。
例如,如果打算为一个Person模型添加一个方法,可以参照下面的代码示例。
【代码3-25】
【代码分析】
在第03~05行代码中,定义了一个Person类,它是一个用于描述人的模型。其中,在第04、05行代码中定义了两个属性,即first_name和last_name。
在第07~15行代码中,定义了一个Child类,它继承自父类Person,是一个用于描述孩子的模型。具体内容说明如下:
· 在第08、09行代码中,通过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>
使用代理模型还可以定义模型的另一种默认排序方法。比如在【代码3-25】中,也许不期望总是对Person进行排序,但在使用代理时总是会依据last_name属性进行排序,解决方法可参看下面的代码示例。
【代码3-26】
【代码分析】
通过上面的定义,普通Person模型的查询结果就不会被排序了。而通过第03行代码的定义,OrderedPerson模型的查询结果会按照last_name属性排序。
再次回看一下【代码3-25】,当使用Person模型对象进行查询时,Django框架是不会返回Child模型对象的,对于Person模型对象的查询结果集,总是返回相对应的类型(QuerySet仍会返回请求的模型)。
代理对象存在的意义是帮设计人员复用原Person模型所提供的代码,以及自定义的功能代码(并未依赖其他代码)。对于代理模型,是不存在任何方法来保证在设计时创建完代理后,能够替换所有Person(或其他)模型。
在使用代理模型时,对于其继承的基类是有约束条件的。一个代理模型必须继承一个非抽象模型类,而不能继承多个非抽象模型类的。原因在于,代理模型无法在不同数据表之间提供任何行间连接。一个代理模型既可以继承任意数量的抽象模型类(假设它没有定义任何模型字段),也可以继承任意数量的代理模型(只需共享同一个非抽象父类)。
另外,如果未在代理模型中指定模型管理器,则默认会从父类模型中继承。而如果在代理模型中指定了管理器,则该管理器就会成为默认的管理器,同时父类中所定义的管理器也仍可使用。
基于【代码3-25】和【代码3-26】的示例,我们可以在查询Person模型时修改默认管理器,示例代码如下:
【代码3-27】
【代码分析】
在第03~05行代码中,在不替换已存在的默认管理器情况下,为代理模型添加了新管理器NewManager。
另外,官方文档中对“自定义管理器”介绍了一种技巧,即先创建一个包含新管理器的基类,然后在继承列表中的主类后追加这个新管理器的基类。不过,通常情况下可能是不需要这么做的。
Django框架的代理模型继承看上去与创建未托管模型非常相似,未托管模型是通过在模型的Meta类中定义managed属性来实现的。
对于创建未托管模型的方法,主要是通过配置Meta.db_table项来实现的。未托管模型将对现有模型进行阴影处理,并添加一些Python方法。但是请注意,这个配置过程需要经常重复操作并且容易出错,主要原因是要在做任何修改时保持两个副本的同步,因此务必要谨慎小心。
相对于未托管模型,代理模型意在表现为与所代理的模型一样——总是与父模型保持一致。因为,代理模型将直接从父模型中继承字段和管理器。
关于代理模型继承和未托管模型的通用性规则,主要描述如下:
· 当克隆一个已存在模型或数据表并且不打算要全部的原数据表列时,请配置“Meta.managed=False”选项。这个选项在模型化未受Django框架控制的数据库视图和表格时很有用。
· 如果只想修改模型的Python级行为,同时要保留原有字段,请配置“Meta.proxy=True”选项。这个配置将使得代理模型在保存数据时,数据结构与原模型的保持一致。
在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模型。