文章目录
- Django 模型继承问题
- 继承出现的情况
- `Meta` 和多表继承
- `Meta` 和多表继承
- 继承与反向关系
- 指定父类连接字段
- 代理模型
- `QuerySet` 仍会返回请求的模型
- 基类约束
- 代理模型管理器
- 代理继承和未托管的模型间的区别
- 多重继承
- 不能用字段名 "hiding"
- 在一个包中管理模型
Django 模型继承问题
Django 模型 ORM 继承最典型一个就是内置 RABC 权限六表中 auth_user 表的扩展。
首先我们先看看源码与实现方式。
auth_user 表源码
。
class User(AbstractUser):
"""
Users within the Django authentication system are represented by this
model.
Username and password are required. Other fields are optional.
"""
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
AbstractUser 类源码
class AbstractUser(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
validators=[username_validator],
error_messages={
'unique': _("A user with that username already exists."),
},
)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=150, blank=True)
email = models.EmailField(_('email address'), blank=True)
is_staff = models.BooleanField(
_('staff status'),
default=False,
help_text=_('Designates whether the user can log into this admin site.'),
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
abstract = Trues
我们可以发现, User 类基本上就是 继承了 AbstractUser 类, 其他什么都没写。
我们在 扩展 User 表时 也是去继承 AbstractUser 类,而不是 User 类。扩展方法如下:
class UserInfo(AbstractUser):
phone = models.BigIntegerField()
...
需要注意的是:
1. 在扩展之前没有执行过数据库迁移命令
auth_user没有被创建,如果当前库已经创建了那么就需要重新换一个库
2. 继承的类里面不要覆盖AbstractUser里面的字段名
表里面的字段都不要动,只扩展额外字段即可
3. 需要在配置文件中告诉django你要用UserInfo代理auth_user(******)
AUTH_USER_MODEL = 'app01.UserInfo' # '应用名.表名' 声明认证用户表
那么问题来了。为啥要用User类去建表?为啥扩展 User 表时不继承 User 类 而是去继承 AbstractUser 类?
原因有多条,其中最广而易知的是 AbstractUser 是一个 抽象表,Meta 属性中设置了 abstract = Trues
属性。该类在执行建表命令 makemigrations 和 migrate 时不会去实现。
在本文中,会描述第二个原因。
继承出现的情况
举个栗子:
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 表,执行建表语句后结果(我插入数据后截下来的图)
实际上 继承非抽象表 类 ,默认会加上一个 OnetoOneField 外键关系
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
place_ptr = models.OneToOneField(Place, on_delete=models.CASCADE, parent_link=False)
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
插入数据
pl = models.Place.objects.create(name="Bod2", address="shanghai")
print(pl)
Re = models.Restaurant.objects.create(name="Bod2", address="shanghai")
print(Re)
执行完后,你会发现 place 表中插入了两条数据,restaurant 表中只有一条数据。并且 place_ptr_id 值指向 place 表中的第二条数据。
Place
的所有字段均在 Restaurant
中可用,虽然数据分别存在不同的表中。所有,以下操作均可:
Place.objects.filter(name="Bob's Cafe")
Restaurant.objects.filter(name="Bob's Cafe")
若有一个 Place
同时也是 Restaurant
,你可以通过小写的模型名将 Place
对象转为 Restaurant
对象。
r = Restaurant.objects.get(id=3)
r.place_ptr
p = Place.objects.get(id=3)
p.restaurant
<Restaurant: ...>
然而,若上述例子中的 p
不是 一个 Restaurant
(它仅是个 Place
对象或是其它类的父类),指向 p.restaurant
会抛出一个 Restaurant.DoesNotExist
异常。
但是我们可以通过设置 parent_link = True 来解决这个报错,允许 其通过父类 访问子类
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Restaurant(Place):
place_ptr = models.OneToOneField(Place, on_delete=models.CASCADE, parent_link=True)
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
Meta
和多表继承
多表继承情况下,子类不会继承父类的 Meta。所以的 Meta 类选项已被应用至父类,在子类中再次应用会导致行为冲突(与抽象基类中应用场景对比,这种情况下,基类并不存在)。
故,子类模型无法访问父类的 Meta 类。不过,有限的几种情况下:若子类未指定 ordering
属性或 get_latest_by
属性,子类会从父类继承这些。
如果父类有排序,而你并不期望子类有排序,你可以显示的禁止它:
class ChildModel(ParentModel):
# ...
class Meta:
# Remove parent's ordering effect
ordering = []
Meta
和多表继承
多表继承情况下,子类不会继承父类的 Meta。所以的 Meta 类选项已被应用至父类,在子类中再次应用会导致行为冲突(与抽象基类中应用场景对比,这种情况下,基类并不存在)。
故,子类模型无法访问父类的 Meta 类。不过,有限的几种情况下:若子类未指定 ordering
属性或 get_latest_by
属性,子类会从父类继承这些。
如果父类有排序,而你并不期望子类有排序,你可以显示的禁止它:
class ChildModel(ParentModel):
# ...
class Meta:
# Remove parent's ordering effect
ordering = []
继承与反向关系
由于多表继承使用隐式的 OneToOneField
连接子类和父类,所以直接从父类访问子类是可能的,就像上述例子展示的那样。然而,使用的名字是ForeignKey
和 ManyToManyField
关系的默认值。如果你在继承父类模型的子类中添加了这些关联,你 必须 指定 related_name
属性。假如你忘了,Django 会抛出一个合法性错误。
比如,让我们用上面的 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'.
将 related_name
像下面这样加至 customers
字段能解决此错误: models.ManyToManyField(Place, related_name='provider')
指定父类连接字段
如上所述,Django 会自动创建一个 OneToOneField
,将子类连接回非抽象的父类。如果你想修改连接回父类的属性名,你可以自己创建 OneToOneField
,并设置 parent_link=True
,表明该属性用于连接回父类。
代理模型
使用多表继承时,每个子类模型都会创建一张新表。这一般是期望的行为,因为子类需要一个地方存储基类中不存在的额外数据字段。不过,有时候你只想修改模型的 Python 级行为——可能是修改默认管理器,或添加一个方法。
这是代理模型继承的目的:为原模型创建一个 代理。你可以创建,删除和更新代理模型的实例,所以的数据都会存储的像你使用原模型(未代理的)一样。不同点是你可以修改代理默认的模型排序和默认管理器,而不需要修改原模型。
代理模型就像普通模型一样申明。你需要告诉 Django 这是一个代理模型,通过将 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>
你也可以用代理模型定义模型的另一种不同的默认排序方法。你也许不期望总对 “Persion” 进行排序,但是在使用代理时,总是依据 “last_name” 属性进行排序。这很简单:
class OrderedPerson(Person):
class Meta:
ordering = ["last_name"]
proxy = True
现在,普通的 Person
查询结果不会被排序,但 OrderdPerson
查询接轨会按 last_name
排序。
代理模型继承“Meta”属性 和普通模型一样。
QuerySet
仍会返回请求的模型
当你用 Person
对象查询时,Django 永远不会返回 MyPerson
对象。Person
对象的查询结果集总是返回对应类型。代理对象存在的全部意义是帮你复用原 Person
提供的代码和自定义的功能代码(并未依赖其它代码)。不存在什么方法能在你创建完代理后,帮你替换所有 Person
(或其它)模型。
基类约束
一个代理模型必须继承自一个非抽象模型类。你不能继承多个非抽象模型类,因为代理模型无法在不同数据表之间提供任何行间连接。一个代理模型可以继承任意数量的抽象模型类,假如他们 没有 定义任何的模型字段。一个代理模型也可以继承任意数量的代理模型,只需他们共享同一个非抽象父类。
代理模型管理器
若你未在代理模型中指定模型管理器,它会从父类模型中继承。如果你在代理模型中指定了管理器,它会成为默认管理器,但父类中定义的管理器仍是可用的。
随着上面的例子一路走下来,你可以在查询 Person
模型时这样修改默认管理器:
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
通常情况下,你可能不需要这么做。然而,你需要的时候,这也是可以的。
代理继承和未托管的模型间的区别
代理模型继承可能看起来和创建未托管的模型很类似,通过在模型的 Meta
类中定义 managed
属性。
通过小心地配置 Meta.db_table
,你将创建一个未托管的模型,该模型将对现有模型进行阴影处理,并添加一些 Python 方法。然而,这会是个经常重复的且容易出错的过程,因为你要在做任何修改时保持两个副本的同步。
另一方面,代理模型意在表现的和所代理的模型一样。它们总是与父模型保持一致,因为它们直接从福利继承字段和管理器。
通用性规则:
- 当你克隆一个已存在模型或数据表时,并且不想要所以的原数据表列,配置
Meta.managed=False
。这个选项在模型化未受 Django 控制的数据库视图和表格时很有用。 - 如果你只想修改模型的 Python 行为,并保留原有字段,配置
Meta.proxy=True
。这个配置使得代理模型在保存数据时,确保数据结构和原模型的完全一样。
多重继承
和 Python 中的继承一样,Django 模型也能继承自多个父类模型。请记住,Python 的命名规则这里也有效。第一个出现的基类(比如 Meta )就是会被使用的那个;举个例子,如果存在多个父类包含 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
。这会要求为每个父类模型和公共祖先使用显式的 OneToOneField
,避免与子类自动生成或继承的字段发生冲突:
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
不能用字段名 “hiding”
在普通的 Python 类继承中,允许子类重写父类的任何属性。在 Django 中,针对模型字段在,这一般是不允许的。如果有个非抽象模型基类,拥有一个名为 author
字段,你可以任意继承自基类的类中创建另一个模型字段,或定义一个叫 author
的属性。
此规范不针对从抽象模型基类继承获得的字段。这些字段可被其它字段或值重写,也可以通过配置 field_name = None
删除。
警告
模型管理器是由抽象基类继承来的。重写由 Manager
指定的字段可能会导致精细的 bug。
注解
某些字段在模型内定义了额外的属性,比如,一个 ForeignKey
定义了一个额外属性,名称为字段名接 _id
,并在外部模型中的添加 related_name
和 related_query_name
。
这些额外属性不能被重写,除非定义该属性的字段被修改或删除,这样就不会定义额外属性了。
在父模型中重写字段会在很多方面造成困难,比如创建新实例(特指那些字段在 Model.__init__
中初始化的那些)和序列化。这些特性,普通的 Python 类继承不需要用完全一样的方式处理,故此, Django 的模型继承和 Python 的类继承之间的区别不是随意的。
这些限制只针对那些是 Field
实例的属性。普通的 Python 属性可被随便重写。它还对 Python 能识别的属性生效:如果你同时在子类和多表继承的祖先类中指定了数据表的列名(它们是两张不同的数据表中的列)。
若你在祖先模型中重写了任何模型字段,Django 会抛出一个 FieldError
。
在一个包中管理模型
manage.py startapp
命令创建了一个应用结构,包含一个 models.py
文件。若你有很多 models.py
文件,用独立的文件管理它们会很实用。
为了达到此目的,创建一个 models
包。删除 models.py
,创建一个 myapp/models
目录,包含一个 __init__.py
文件和存储模型的文件。你必须在 __init__.py
文件中导入这些模块。
比如,若你在 models
目录下有 organic.py
和 synthetic.py
:
myapp/models/init.py
from .organic import Person
from .synthetic import Robot
显式导入每个模块,而不是使用 from .models import *
有助于不打乱命名空间,使代码更具可读性,让代码分析工具更有用。