Django -- 模型层

模型和字段

一个模型(model)就是一个单独的、确定的数据的信息源,包含了数据的字段和操作方法。通常,每个模型映射为一张数据库中的表。

基本的原则如下:

  • 每个模型在Django中的存在形式为一个Python类
  • 每个类都是django.db.models.Model的子类
  • 模型(类)的每个字段(属性)代表数据表的某一列
  • Django自动为你生成访问数据库的API

简单示例

下面的模型定义了一个 Person 类,它具有first_name和last_name字段:

python 复制代码
from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

每一个字段都是一个类属性,每个类属性表示数据表中的一个列。

上面的代码,相当于下面的原生SQL语句:

sql 复制代码
CREATE TABLE myapp_person (
    "id" serial NOT NULL PRIMARY KEY,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(30) NOT NULL
);

注意:

  • 表名myapp_person由Django自动生成,默认格式为"项目名称+下划线+小写类名",你可以重写这个规则。
  • Django会自动创建自增主键id,当然,你也可以自己指定主键。
  • 上面的SQL语句基于PostgreSQL语法。

通常,我们会将模型编写在其所属app下的models.py文件中,没有特别需求时,请坚持这个原则,不要自己给自己添加麻烦。

创建了模型之后,在使用它之前,你需要先在settings文件中的INSTALLED_APPS 处,注册models.py文件所在的myapp。看清楚了,是注册app,不是模型,也不是models.py。如果你以前写过模型,可能已经做过这一步工作,可跳过。

python 复制代码
INSTALLED_APPS = [
    #...
    'myapp',
    #...
]

当你每次对模型进行增、删、修改时,请务必执行命令python manage.py migrate,让操作实际应用到数据库上。这里可以选择在执行migrate之前,先执行python manage.py makemigrations让修改动作保存到记录文件中,方便github等工具的使用。

模型的属性

每个模型都可以有很多属性,其中有Django内置的,也可以有你自定义的。

模型当中最重要的属性是 Manager管理器。它是 Django 模型和数据库查询操作之间的API接口,用于从数据库当中获取数据实例。如果没有指定自定义的 Manager ,那么它默认名称是 objects,这是Django自动为我们提供和生成的。Manager 只能通过模型类来访问,不能通过模型实例来访问,也就是说,只能Person.objects,不可以jack.objects。

模型还有一个不为人知的隐藏属性_state。

_state属性指向一个ModelState类实例,它持续跟踪着模型实例的生命周期。

_state自己又有2个属性:adding和db

  • adding:一个标识符,如果当前的模型实例还没有保存到数据库内,则为True,否则为False
  • db:一个字符串指向某个数据库,当前模型实例是从该数据库中读取出来的。

故而

  • 对于一个新创建的模型实例:adding=True并且db=None
  • 对于从某个数据库中读取出来的模型实例:adding=False并且db='数据库名'

常用字段类型

Django内置了许多字段类型,它们都位于django.db.models中,例如models.CharField,它们的父类都是Field类。这些类型基本满足需求,如果还不够,你也可以自定义字段。

下面列出了所有Django内置的字段类型,但不包括关系字段类型(字段名采用驼峰命名法,初学者请一定要注意):

AutoField

一个自动增加的整数类型字段。通常你不需要自己编写它,Django会自动帮你添加字段:id = models.AutoField(primary_key=True),这是一个自增字段,从1开始计数。如果你非要自己设置主键,那么请务必将字段设置为primary_key=True。Django在一个模型中只允许有一个自增字段,并且该字段必须为主键!

BigAutoField

64位整数类型自增字段,数字范围更大,从1到9223372036854775807

BigIntegerField

64位整数字段(看清楚,非自增),类似IntegerField ,-9223372036854775808 到9223372036854775807。在Django的模板表单里体现为一个NumberInput标签。

BinaryField

二进制数据类型。较少使用。

BooleanField

布尔值类型。默认值是None。在HTML表单中体现为CheckboxInput标签。如果设置了参数null=True,则表现为NullBooleanSelect选择框。可以提供default参数值,设置默认值。

CharField

最常用的类型,字符串类型。必须接收一个max_length参数,表示字符串长度不能超过该值。默认的表单标签是text input。

DateField

class DateField(auto_now=False, auto_now_add=False, **options) , 日期类型。一个Python中的datetime.date的实例。在HTML中表现为DateInput标签。在admin后台中,Django会帮你自动添加一个JS日历表和一个"Today"快捷方式,以及附加的日期合法性验证。两个重要参数:(参数互斥,不能共存) auto_now:每当对象被保存时将字段设为当前日期,常用于保存最后修改时间。auto_now_add:每当对象被创建时,设为当前日期,常用于保存创建日期(注意,它是不可修改的)。设置上面两个参数就相当于给field添加了editable=False和blank=True属性。如果想具有修改属性,请用default参数。例子:pub_time = models.DateField(auto_now_add=True),自动添加发布时间。

DateTimeField

日期时间类型。Python的datetime.datetime的实例。与DateField相比就是多了小时、分和秒的显示,其它功能、参数、用法、默认值等等都一样。

DecimalField

固定精度的十进制小数。相当于Python的Decimal实例,必须提供两个指定的参数!参数max_digits:最大的位数,必须大于或等于小数点位数 。decimal_places:小数点位数,精度。 当localize=False时,它在HTML表现为NumberInput标签,否则是textInput类型。例子:储存最大不超过999,带有2位小数位精度的数,定义如下:models.DecimalField(..., max_digits=5, decimal_places=2)。

DurationField

持续时间类型。存储一定期间的时间长度。类似Python中的timedelta。在不同的数据库实现中有不同的表示方法。常用于进行时间之间的加减运算。但是小心了,这里有坑,PostgreSQL等数据库之间有兼容性问题!

EmailField

邮箱类型,默认max_length最大长度254位。使用这个字段的好处是,可以使用Django内置的EmailValidator进行邮箱格式合法性验证。

FileField

class FileField(upload_to=None, max_length=100, **options)上传文件类型,后面单独介绍。

FilePathField

文件路径类型,后面单独介绍

FloatField

浮点数类型,对应Python的float。参考整数类型字段。

IntegerField

整数类型,最常用的字段之一。取值范围-2147483648到2147483647。在HTML中表现为NumberInput或者TextInput标签。

GenericIPAddressField

class GenericIPAddressField(protocol='both', unpack_ipv4=False, **options),IPV4或者IPV6地址,字符串形式,例如192.0.2.30或者2a02:42fe::4。在HTML中表现为TextInput标签。参数protocol默认值为'both',可选'IPv4'或者'IPv6',表示你的IP地址类型。

JSONField

JSON类型字段。Django3.1新增。签名为class JSONField(encoder=None,decoder=None,**options)。其中的encoder和decoder为可选的编码器和解码器,用于自定义编码和解码方式。如果为该字段提供default值,请务必保证该值是个不可变的对象,比如字符串对象。

PositiveBigIntegerField

正的大整数,0到9223372036854775807

PositiveIntegerField

正整数,从0到2147483647

PositiveSmallIntegerField

较小的正整数,从0到32767

SlugField

slug是一个新闻行业的术语。一个slug就是一个某种东西的简短标签,包含字母、数字、下划线或者连接线,通常用于URLs中。可以设置max_length参数,默认为50。

SmallAutoField

Django3.0新增。类似AutoField,但是只允许1到32767。

SmallIntegerField

小整数,包含-32768到32767。

TextField

用于储存大量的文本内容,在HTML中表现为Textarea标签,最常用的字段类型之一!如果你为它设置一个max_length参数,那么在前端页面中会受到输入字符数量限制,然而在模型和数据库层面却不受影响。只有CharField才能同时作用于两者。

TimeField

时间字段,Python中datetime.time的实例。接收同DateField一样的参数,只作用于小时、分和秒。

URLField

一个用于保存URL地址的字符串类型,默认最大长度200。

UUIDField

用于保存通用唯一识别码(Universally Unique Identifier)的字段。使用Python的UUID类。在PostgreSQL数据库中保存为uuid类型,其它数据库中为char(32)。这个字段是自增主键的最佳替代品,后面有例子展示。

FileField

python 复制代码
class FileField(upload_to=None, max_length=100, **options)

上传文件字段(不能设置为主键)。默认情况下,该字段在HTML中表现为一个ClearableFileInput标签。在数据库内,我们实际保存的是一个字符串类型,默认最大长度100,可以通过max_length参数自定义。真实的文件是保存在服务器的文件系统内的。

重要参数upload_to用于设置上传地址的目录和文件名。如下例所示:

python 复制代码
class MyModel(models.Model):
    # 文件被传至`MEDIA_ROOT/uploads`目录,MEDIA_ROOT由你在settings文件中设置
    upload = models.FileField(upload_to='uploads/')
    # 或者
    # 被传到`MEDIA_ROOT/uploads/2015/01/30`目录,增加了一个时间划分
    upload = models.FileField(upload_to='uploads/%Y/%m/%d/')

Django很人性化地帮我们实现了根据日期生成目录或文件的方式!

upload_to参数也可以接收一个回调函数,该函数返回具体的路径字符串,如下例:

python 复制代码
def user_directory_path(instance, filename):
    #文件上传到MEDIA_ROOT/user_<id>/<filename>目录中
    return 'user_{0}/{1}'.format(instance.user.id, filename)

class MyModel(models.Model):
    upload = models.FileField(upload_to=user_directory_path)

例子中,user_directory_path这种回调函数,必须接收两个参数,然后返回一个Unix风格的路径字符串。参数instace代表一个定义了FileField的模型的实例,说白了就是当前数据记录。filename是原本的文件名。

从Django3.0开始,支持使用pathlib.Path 处理路径。

当你访问一个模型对象中的文件字段时,Django会自动给我们提供一个 FieldFile实例作为文件的代理,通过这个代理,我们可以进行一些文件操作,主要如下:

  • FieldFile.name : 获取文件名
  • FieldFile.size: 获取文件大小
  • FieldFile.url :用于访问该文件的url
  • FieldFile.open(mode='rb'): 以类似Python文件操作的方式,打开文件
  • FieldFile.close(): 关闭文件
  • FieldFile.save(name, content, save=True): 保存文件
  • FieldFile.delete(save=True): 删除文件

这些代理的API和Python原生的文件读写API非常类似,其实本质上就是进行了一层封装,让我们可以在Django内直接对模型中文件字段进行读写,而不需要绕弯子。

ImageField

python 复制代码
class ImageField(upload_to=None, height_field=None, width_field=None, max_length=100, **options)

用于保存图像文件的字段。该字段继承了FileField,其用法和特性与FileField基本一样,只不过多了两个属性height和width。默认情况下,该字段在HTML中表现为一个ClearableFileInput标签。在数据库内,我们实际保存的是一个字符串类型,默认最大长度100,可以通过max_length参数自定义。真实的图片是保存在服务器的文件系统内的。

height_field参数:保存有图片高度信息的模型字段名。

width_field参数:保存有图片宽度信息的模型字段名。

使用Django的ImageField需要提前安装pillow模块,pip install pillow即可。

使用FileField或者ImageField字段的步骤

  1. 在settings文件中,配置MEDIA_ROOT,作为你上传文件在服务器中的基本路径(为了性能考虑,这些文件不会被储存在数据库中)。再配置个MEDIA_URL,作为公用URL,指向上传文件的基本路径。请确保Web服务器的用户账号对该目录具有写的权限。
  2. 添加FileField或者ImageField字段到你的模型中,定义好upload_to参数,文件最终会放在MEDIA_ROOT目录的"upload_to"子目录中。
  3. 所有真正被保存在数据库中的,只是指向你上传文件路径的字符串而已。可以通过url属性,在Django的模板中方便的访问这些文件。例如,假设你有一个ImageField字段,名叫mug_shot,那么在Django模板的HTML文件中,可以使用{{ object.mug_shot.url }}来获取该文件。其中的object用你具体的对象名称代替。
  4. 可以通过name和size属性,获取文件的名称和大小信息。

安全建议:

无论你如何保存上传的文件,一定要注意他们的内容和格式,避免安全漏洞!务必对所有的上传文件进行安全检查,确保它们不出问题!如果你不加任何检查就盲目的让任何人上传文件到你的服务器文档根目录内,比如上传了一个CGI或者PHP脚本,很可能就会被访问的用户执行,这具有致命的危害。

FilePathField

python 复制代码
class FilePathField(path='', match=None, recursive=False, allow_files=True, allow_folders=False, max_length=100, **options)

一种用来保存文件路径信息的字段。在数据表内以字符串的形式存在,默认最大长度100,可以通过max_length参数设置。

它包含有下面的一些参数:

  • path:必须指定的参数。表示一个系统绝对路径。path通常是个字符串,也可以是个可调用对象,比如函数。
  • match:可选参数,一个正则表达式,用于过滤文件名。只匹配基本文件名,不匹配路径。例如foo.*.txt$,只匹配文件名foo23.txt,不匹配bar.txt与foo23.png。
  • recursive:可选参数,只能是True或者False。默认为False。决定是否包含子目录,也就是是否递归的意思。
  • allow_files:可选参数,只能是True或者False。默认为True。决定是否应该将文件名包括在内。它和allow_folders其中,必须有一个为True。
  • allow_folders: 可选参数,只能是True或者False。默认为False。决定是否应该将目录名包括在内。
python 复制代码
FilePathField(path="/home/images", match="foo.*", recursive=True)

它只匹配/home/images/foo.png,但不匹配/home/images/foo/bar.png,因为默认情况,只匹配文件名,而不管路径是怎么样的。

UUIDField

数据库无法自己生成uuid,因此需要如下使用default参数:

python 复制代码
import uuid     # Python的内置模块
from django.db import models

class MyUUIDModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # 其它字段

关系类型字段

Django还定义了一组关系类型字段,用来表示模型与模型之间的关系.

多对一(ForeignKey)

多对一的关系,通常被称为外键。外键字段类的定义如下:

python 复制代码
class ForeignKey(to, on_delete, **options)

外键需要两个位置参数,一个是关联的模型,另一个是on_delete。在Django2.0版本后,on_delete属于必填参数。

外键要定义在'多'的一方!

python 复制代码
from django.db import models

class Manufacturer(models.Model):
    pass

class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)

多对一字段的变量名一般设置为关联的模型的小写单数,而多对多则一般设置为小写复数。

如果你要关联的模型位于当前模型之后,则需要通过字符串的方式进行引用,看下面的例子:

python 复制代码
from django.db import models

class Car(models.Model):
    manufacturer = models.ForeignKey(
        'Manufacturer',    # 注意这里
        on_delete=models.CASCADE,
    )

class Manufacturer(models.Model):
    pass

上面的例子中,每辆车都会有一个生产工厂,一个工厂可以生产N辆车,于是用一个外键字段manufacturer表示,并放在Car模型中。注意,此manufacturer非彼Manufacturer模型类,它是一个字段的名称。在Django的模型定义中,经常出现类似的英文单词大小写不同,一定要注意区分!

如果要关联的对象在另外一个app中,可以显式的指出。下例假设Manufacturer模型存在于production这个app中,则Car模型的定义如下:

python 复制代码
class Car(models.Model):
    manufacturer = models.ForeignKey(
        'production.Manufacturer',      # 关键在这里!!
        on_delete=models.CASCADE,
    )

如果要创建一个递归的外键,也就是自己关联自己的的外键,使用下面的方法:

models.ForeignKey('self', on_delete=models.CASCADE)

核心在于'self'这个引用。什么时候需要自己引用自己的外键呢?典型的例子就是评论系统!一条评论可以被很多人继续评论,如下所示:

python 复制代码
class Comment(models.Model):
    title = models.CharField(max_length=128)
    text = models.TextField()
    parent_comment = models.ForeignKey('self', on_delete=models.CASCADE)

注意上面的外键字段定义的是父评论,而不是子评论。为什么呢?因为外键要放在'多'的一方!

在实际的数据库后台,Django会为每一个外键添加_id后缀,并以此创建数据表里的一列。在上面的工厂与车的例子中,Car模型对应的数据表中,会有一列叫做manufacturer_id。但实际上,在Django代码中你不需要使用这个列名,除非你书写原生的SQL语句,一般我们都直接使用字段名manufacturer。

参数说明

on_delete

这个参数在Django2.0之后,不可以省略了,需要显式的指定!这也是除了路由编写方式外,Django2和Django1.x最大的不同点之一!

当一个外键关联的对象被删除时,Django将模仿on_delete参数定义的SQL约束执行相应操作。比如,你有一个可为空的外键,并且你想让它在关联的对象被删除时,自动设为null,可以如下定义:

python 复制代码
user = models.ForeignKey(
    User,
    on_delete=models.SET_NULL,
    blank=True,
    null=True,
)

该参数可选的值都内置在django.db.models中(全部为大写),包括

  • CASCADE:模拟SQL语言中的ON DELETE CASCADE约束,将定义有外键的模型对象同时删除!
  • PROTECT:阻止上面的删除操作,但是弹出ProtectedError异常
  • SET_NULL:将外键字段设为null,只有当字段设置了null=True时,方可使用该值。
  • SET_DEFAULT:将外键字段设为默认值。只有当字段设置了default参数时,方可使用。
  • DO_NOTHING:什么也不做。
  • SET():设置为一个传递给SET()的值或者一个回调函数的返回值。注意大小写。
python 复制代码
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models

def get_sentinel_user():
    return get_user_model().objects.get_or_create(username='deleted')[0]

class MyModel(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET(get_sentinel_user),
    )

RESTRICT: Django3.1新增。这个模式比较难以理解。它与PROTECT不同,在大多数情况下,同样不允许删除,但是在某些特殊情况下,却是可以删除的。看下面的例子,多揣摩一下:

python 复制代码
# 假设有这样的三个模型以及外键关系

class Artist(models.Model):
    name = models.CharField(max_length=10)

class Album(models.Model):
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE)  # 注意这里

class Song(models.Model):
    artist = models.ForeignKey(Artist, on_delete=models.CASCADE) # 注意这里
    album = models.ForeignKey(Album, on_delete=models.RESTRICT) # 注意这里

尝试在Django的shell里测试下面的API:

python 复制代码
>>> artist_one = Artist.objects.create(name='artist one')
>>> artist_two = Artist.objects.create(name='artist two')
>>> album_one = Album.objects.create(artist=artist_one)
>>> album_two = Album.objects.create(artist=artist_two)
>>> song_one = Song.objects.create(artist=artist_one, album=album_one)
>>> song_two = Song.objects.create(artist=artist_one, album=album_two)
>>> album_one.delete()
# Raises RestrictedError.  不可以删除
>>> artist_two.delete()
# Raises RestrictedError.   不可以删除
>>> artist_one.delete()
(4, {'Song': 2, 'Album': 1, 'Artist': 1})    # 居然可以删除

为什么artist_one可以被删除,但是artist_two不可以?Django设计的这个模式真的比较难以理解。

limit_choices_to

该参数用于限制外键所能关联的对象,只能用于Django的ModelForm(Django的表单模块)和admin后台,对其它场合无限制功能。其值可以是一个字典、Q对象或者一个返回字典或Q对象的函数调用,如下例所示:

python 复制代码
staff_member = models.ForeignKey(
    User,
    on_delete=models.CASCADE,
    limit_choices_to={'is_staff': True},
)

用于关联对象反向引用模型的名称。以前面车和工厂的例子解释,就是从工厂反向关联到车的关系名称。

通常情况下,这个参数我们可以不设置,Django会默认以模型的小写加上_set作为反向关联名,比如对于工厂就是car_set,如果你觉得car_set还不够直观,可以如下定义:

python 复制代码
class Car(models.Model):
    manufacturer = models.ForeignKey(
        'production.Manufacturer',      
        on_delete=models.CASCADE,
        related_name='car_producted_by_this_manufacturer',  # 看这里!!
    )

也许我定义了一个蹩脚的词,但表达的意思很清楚。以后从工厂对象反向关联到它所生产的汽车,就可以使用maufacturer.car_producted_by_this_manufacturer了。

如果你不想为外键设置一个反向关联名称,可以将这个参数设置为"+"或者以"+"结尾,如下所示:

python 复制代码
user = models.ForeignKey(
    User,
    on_delete=models.CASCADE,
    related_name='+',
)

反向关联查询名。用于从目标模型反向过滤模型对象的名称。(过滤和查询在后续章节会介绍)

这个参数的默认值是定义有外键字段的模型的小写名,如果设置了related_name参数,那么就是这个参数值,如果在此基础上还指定了related_query_name的值,则是related_query_name的值。三者依次有优先顺序。

要注意related_query_name和related_name的区别,前者用于在做查询操作时候作为参数使用,后者主要用于在属性调用时使用。

python 复制代码
class Tag(models.Model):
    article = models.ForeignKey(
        Article,
        on_delete=models.CASCADE,
        related_name="tags",
        related_query_name="tag",       # 注意这一行
    )
    name = models.CharField(max_length=255)

# 现在可以使用'tag'作为查询名了
Article.objects.filter(tag__name="important")

to_field

默认情况下,外键都是关联到被关联对象的主键上(一般为id)。如果指定这个参数,可以关联到指定的字段上,但是该字段必须具有unique=True属性,也就是具有唯一属性。

db_constraint

默认情况下,这个参数被设为True,表示遵循数据库约束,这也是大多数情况下你的选择。如果设为False,那么将无法保证数据的完整性和合法性。在下面的场景中,你可能需要将它设置为False:

  • 有历史遗留的不合法数据,没办法的选择
  • 你正在分割数据表

当它为False,并且你试图访问一个不存在的关系对象时,会抛出DoesNotExist 异常。

swappable

控制迁移框架的动作,如果当前外键指向一个可交换的模型。使用场景非常稀少,通常请将该参数保持默认的True。

多对多(ManyToManyField)

python 复制代码
class ManyToManyField(to, **options)

多对多关系在数据库中也是非常常见的关系类型。比如一本书可以有好几个作者,一个作者也可以写好几本书。多对多的字段可以定义在任何的一方,请尽量定义在符合人们思维习惯的一方,但不要同时都定义,只能选择一个模型设置该字段(比如我们通常将披萨上的配料字段放在披萨模型中,而不是在配料模型中放置披萨字段)。

建议为多对多字段名使用复数形式。

多对多关系需要一个位置参数:关联的对象模型,其它用法和外键多对一基本类似。

如果要创建一个关联自己的多对多字段,依然是通过'self'引用。

在数据库后台,Django实际上会额外创建一张用于体现多对多关系的中间表。默认情况下,该表的名称是"多对多字段名+包含该字段的模型名+一个独一无二的哈希码",例如'author_books_9cdf4',当然你也可以通过db_table选项,自定义表名。

参数说明

related_name : 参考外键的相同参数。
related_query_name : 参考外键的相同参数。
limit_choices_to : 参考外键的相同参数。但是对于使用through参数自定义中间表的多对多字段无效。
symmetrical: 默认情况下,Django中的多对多关系是对称的。看下面的例子:

python 复制代码
from django.db import models

class Person(models.Model):
    friends = models.ManyToManyField("self")

Django认为,如果我是你的朋友,那么你也是我的朋友,这是一种对称关系,Django不会为Person模型添加person_set属性用于反向关联。如果你不想使用这种对称关系,可以将symmetrical设置为False,这将强制Django为反向关联添加描述符。

through

如果你想自定义多对多关系的那张额外的关联表,可以使用这个参数!参数的值为一个中间模型。

最常见的使用场景是你需要为多对多关系添加额外的数据,比如添加两个人建立QQ好友关系的时间。

通常情况下,这张表在数据库内的结构是这个样子的:

中间表的id列...模型对象的id列...被关联对象的id列 各行数据

如果自定义中间表并添加时间字段,则在数据库内的表结构如下:

中间表的id列...模型对象的id列...被关联对象的id列...时间对象列

看下面的例子

python 复制代码
from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=50)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(
        Person,
        through='Membership',       ## 自定义中间表
        through_fields=('group', 'person'),
    )

class Membership(models.Model):  # 这就是具体的中间表模型
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    inviter = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
        related_name="membership_invites",
    )
    invite_reason = models.CharField(max_length=64)

上面的代码中,通过class Membership(models.Model)定义了一个新的模型,用来保存Person和Group模型的多对多关系,并且同时增加了'邀请人'和'邀请原因'的字段。

through_fields

接着上面的例子。Membership模型中包含两个关联Person的外键,Django无法确定到底使用哪个作为和Group关联的对象。所以,在这个例子中,必须显式的指定through_fields参数,用于定义关系。

through_fields参数接收一个二元元组('field1', 'field2'),field1是指向定义有多对多关系的模型的外键字段的名称,这里是Membership中的'group'字段(注意大小写),另外一个则是指向目标模型的外键字段的名称,这里是Membership中的'person',而不是'inviter'。

再通俗的说,就是through_fields参数指定从中间表模型Membership中选择哪两个字段,作为关系连接字段。

db_table

设置中间表的名称。不指定的话,则使用默认值。

db_constraint

参考外键的相同参数。

swappable

参考外键的相同参数。

ManyToManyField多对多字段不支持Django内置的validators验证功能。

null参数对ManyToManyField多对多字段无效!设置null=True毫无意义

一对一(OneToOneField)

一对一关系类型的定义如下:

python 复制代码
class OneToOneField(to, on_delete, parent_link=False, **options)

从概念上讲,一对一关系非常类似具有unique=True属性的外键关系,但是反向关联对象只有一个。这种关系类型多数用于当一个模型需要从别的模型扩展而来的情况。比如,Django自带auth模块的User用户表,如果你想在自己的项目里创建用户模型,又想方便的使用Django的auth中的一些功能,那么一个方案就是在你的用户模型里,使用一对一关系,添加一个与auth模块User模型的关联字段。

该关系的第一位置参数为关联的模型,其用法和前面的多对一外键一样。

如果你没有给一对一关系设置related_name参数,Django将使用当前模型的小写名作为默认值。

看下面的例子:

python 复制代码
from django.conf import settings
from django.db import models

# 两个字段都使用一对一关联到了Django内置的auth模块中的User模型
class MySpecialUser(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )
    supervisor = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='supervisor_of',
    )

这样下来,你的User模型将拥有下面的属性:

python 复制代码
>>> user = User.objects.get(pk=1)
>>> hasattr(user, 'myspecialuser')
True
>>> hasattr(user, 'supervisor_of')
True

OneToOneField一对一关系拥有和多对一外键关系一样的额外可选参数,只是多了一个不常用的parent_link参数。

跨模块的模型:

有时候,我们关联的模型并不在当前模型的文件内,没关系,就像我们导入第三方库一样的从别的模块内导入进来就好,如下例所示:

python 复制代码
from django.db import models
from geography.models import ZipCode

class Restaurant(models.Model):
    # ...
    zip_code = models.ForeignKey(
        ZipCode,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

多对多中间表详解

我们都知道对于ManyToMany字段,Django采用的是第三张中间表的方式。通过这第三张表,来关联ManyToMany的双方。下面我们根据一个具体的例子,详细解说中间表的使用。

默认中间表

首先, 模型是这样

python 复制代码
class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person)

    def __str__(self):
        return self.name

在Group模型中,通过members字段,以ManyToMany方式与Person模型建立了关系。

让我们到数据库内看一下实际的内容,Django为我们创建了三张数据表,其中的app1是应用名。

自定义中间表

一般情况,普通的多对多已经够用,无需自己创建第三张关系表。但是某些情况可能更复杂一点,比如如果你想保存某个人加入某个分组的时间呢?想保存进组的原因呢?

Django提供了一个through参数,用于指定中间模型,你可以将类似进组时间,邀请原因等其他字段放在这个中间模型内。例子如下:

python 复制代码
from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=128)
    def __str__(self): 
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')
    def __str__(self): 
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()        # 进组时间
    invite_reason = models.CharField(max_length=64)  # 邀请原因

在中间表中,我们至少要编写两个外键字段,分别指向关联的两个模型。在本例中就是'Person'和'group'。 这里,我们额外增加了'date_joined'字段,用于保存人员进组的时间,'invite_reason'字段用于保存邀请进组的原因。

使用中间表

针对上面的中间表,下面是一些使用例子(以欧洲著名的甲壳虫乐队成员为例)

python 复制代码
>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
...     date_joined=date(1962, 8, 16),
...     invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(person=paul, group=beatles,
...     date_joined=date(1960, 8, 1),
...     invite_reason="Wanted to form a band.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>

可以使用 add(), create(), 或 set() 创建关联对象,只需指定 through_defaults 参数:

python 复制代码
>>> beatles.members.add(john, through_defaults={'date_joined': date(1960, 8, 1)})
>>> beatles.members.create(name="George Harrison", through_defaults={'date_joined': date(1960, 8, 1)})
>>> beatles.members.set([john, paul, ringo, george], through_defaults={'date_joined': date(1960, 8, 1)})

也可以直接创建中间模型实例。并且如果自定义中间模型没有强制设定 (model1, model2) 对的唯一性,调用 remove() 方法会删除所有中间模型的实例:

python 复制代码
>>> Membership.objects.create(person=ringo, group=beatles,
...     date_joined=date(1968, 9, 4),
...     invite_reason="You've been gone for a month and we miss you.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
>>> # remove方法同时删除了两个 Ringo Starr
>>> beatles.members.remove(ringo)
>>> beatles.members.all()
<QuerySet [<Person: Paul McCartney>]>

clear()方法能清空所有的多对多关系。

python 复制代码
多对多中间表详解
阅读: 93896     评论:34
我们都知道对于ManyToMany字段,Django采用的是第三张中间表的方式。通过这第三张表,来关联ManyToMany的双方。下面我们根据一个具体的例子,详细解说中间表的使用。

一、默认中间表
首先,模型是这样的:

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person)

    def __str__(self):
        return self.name
在Group模型中,通过members字段,以ManyToMany方式与Person模型建立了关系。

让我们到数据库内看一下实际的内容,Django为我们创建了三张数据表,其中的app1是应用名。



然后我在数据库中添加了下面的Person对象:



再添加下面的Group对象:



让我们来看看,中间表是个什么样子的:



首先有一列id,这是Django默认添加的,没什么好说的。然后是Group和Person的id列,这是默认情况下,Django关联两张表的方式。如果你要设置关联的列,可以使用to_field参数。

可见在中间表中,并不是将两张表的数据都保存在一起,而是通过id的关联进行映射。

二、自定义中间表
一般情况,普通的多对多已经够用,无需自己创建第三张关系表。但是某些情况可能更复杂一点,比如如果你想保存某个人加入某个分组的时间呢?想保存进组的原因呢?

Django提供了一个through参数,用于指定中间模型,你可以将类似进组时间,邀请原因等其他字段放在这个中间模型内。例子如下:

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=128)
    def __str__(self): 
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')
    def __str__(self): 
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()        # 进组时间
    invite_reason = models.CharField(max_length=64)  # 邀请原因
在中间表中,我们至少要编写两个外键字段,分别指向关联的两个模型。在本例中就是'Person'和'group'。 这里,我们额外增加了'date_joined'字段,用于保存人员进组的时间,'invite_reason'字段用于保存邀请进组的原因。

下面我们依然在数据库中实际查看一下(应用名为app2):



注意中间表的名字已经变成"app2_membership"了。





Person和Group没有变化。



但是中间表就截然不同了!它完美的保存了我们需要的内容。

三、使用中间表
针对上面的中间表,下面是一些使用例子(以欧洲著名的甲壳虫乐队成员为例):

>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
...     date_joined=date(1962, 8, 16),
...     invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(person=paul, group=beatles,
...     date_joined=date(1960, 8, 1),
...     invite_reason="Wanted to form a band.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
可以使用 add(), create(), 或 set() 创建关联对象,只需指定 through_defaults 参数:

>>> beatles.members.add(john, through_defaults={'date_joined': date(1960, 8, 1)})
>>> beatles.members.create(name="George Harrison", through_defaults={'date_joined': date(1960, 8, 1)})
>>> beatles.members.set([john, paul, ringo, george], through_defaults={'date_joined': date(1960, 8, 1)})
也可以直接创建中间模型实例。并且如果自定义中间模型没有强制设定 (model1, model2) 对的唯一性,调用 remove() 方法会删除所有中间模型的实例:

>>> Membership.objects.create(person=ringo, group=beatles,
...     date_joined=date(1968, 9, 4),
...     invite_reason="You've been gone for a month and we miss you.")
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
>>> # remove方法同时删除了两个 Ringo Starr
>>> beatles.members.remove(ringo)
>>> beatles.members.all()
<QuerySet [<Person: Paul McCartney>]>
clear()方法能清空所有的多对多关系。

>>> # 甲壳虫乐队解散了
>>> beatles.members.clear()
>>> # 删除了中间模型的对象
>>> Membership.objects.all()
<QuerySet []>

一旦你通过创建中间模型实例的方法建立了多对多的关联,你立刻就可以像普通的多对多那样进行查询操作:

python 复制代码
# 查找组内有Paul这个人的所有的组(以Paul开头的名字)
>>> Group.objects.filter(members__name__startswith='Paul')
<QuerySet [<Group: The Beatles>]>

可以使用中间模型的属性进行查询:

python 复制代码
# 查找甲壳虫乐队中加入日期在1961年1月1日之后的成员
>>> Person.objects.filter(
... group__name='The Beatles',
... membership__date_joined__gt=date(1961,1,1))
<QuerySet [<Person: Ringo Starr]>

可以像普通模型一样使用中间模型:

python 复制代码
>>> ringos_membership = Membership.objects.get(group=beatles, person=ringo)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'
python 复制代码
>>> ringos_membership = ringo.membership_set.get(group=beatles)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'

这一部分内容,需要结合后面的模型query,如果暂时看不懂,没有关系。


对于中间表,有一点要注意,默认情况下,中间模型只能包含一个指向源模型的外键关系,上面例子中,也就是在Membership中只能有Person和Group外键关系各一个,不能多。否则,你必须显式的通过ManyToManyField.through_fields参数指定关联的对象。参考下面的例子:

python 复制代码
from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=50)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(
						    Person,
						    through='Membership',
						    through_fields=('group', 'person'),
 			 )

class Membership(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
					   inviter = models.ForeignKey(
					    Person,
					    on_delete=models.CASCADE,
					    related_name="membership_invites",
 					 )
    invite_reason = models.CharField(max_length=64)

Admin后台相关

使用了中间表后,在admin中展示多对多关系会有如下问题:

python 复制代码
[(admin.E013) The value of 'fieldsets[2][1]["fields"]' cannot include the ManyToManyField 'menu_field', because that field manually specifies a relationship model

这个问题,有Django的专门讨论链接以及stackoverflow的解决方法

解决问题的核心是使用admin.InlineModelAdmin类和inlines属性,将多对多中间表以内嵌框的形式展现出来。

下面是一个解决例子:

python 复制代码
from .models import Person,Group,Membership

@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    list_display = ['name']

class MemberInlineAdmin(admin.TabularInline):
    model = Group.members.through

@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {
            'fields': ('name',)
        }),
    )
    inlines = (MemberInlineAdmin,)

@admin.register(Membership)
class MemberShipAdmin(admin.ModelAdmin):
    list_display = ['group','person','invite_reason']

模型的元数据Meta

模型的元数据,指的是"除了字段外的所有内容",例如排序方式、数据库表名、人类可读的单数或者复数名等等。所有的这些都是非必须的,甚至元数据本身对模型也是非必须的。但是,我要说但是,有些元数据选项能给予你极大的帮助,在实际使用中具有重要的作用,是实际应用的'必须'。

想在模型中增加元数据,方法很简单,在模型类中添加一个子类,名字是固定的Meta,然后在这个Meta类下面增加各种元数据选项或者说设置项。参考下面的例子:

python 复制代码
from django.db import models

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:         # 注意,是模型的子类,要缩进!
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

上面的例子中,我们为模型Ox增加了两个元据 orderingverbose_name_plural,分别表示排序和复数名,下面我们会详细介绍有哪些可用的元数据选项。

强调:每个模型都可以有自己的元数据类,每个元数据类也只对自己所在模型起作用。

abstract

如果abstract=True,那么模型会被认为是一个抽象模型。抽象模型本身不实际生成数据库表,而是作为其它模型的父类,被继承使用。具体内容可以参考Django模型的继承。

app_label

如果定义了模型的app没有在INSTALLED_APPS中注册,则必须通过此元选项声明它属于哪个app,例如:

python 复制代码
app_label = 'myapp'

base_manager_name

模型的_base_manager管理器的名字,默认是'objects'。模型管理器是Django为模型提供的API所在。

db_table

指定在数据库中,当前模型生成的数据表的表名。

db_table = 'my_freinds'

如果你没有指定这个选项,那么Django会自动使用app名和模型名,通过下划线连接生成数据表名,比如app_book。

不要使用SQL语言或者Python的保留字,注意冲突。

友情建议:使用MySQL和MariaDB数据库时,db_table用小写英文。

db_tablespace

自定义数据库表空间的名字。默认值是项目的DEFAULT_TABLESPACE配置项指定的值。

default_manager_name

模型的_default_manager管理器的名字。

默认情况下,从一个模型反向关联设置有关系字段的源模型,我们使用<model_name>_set,也就是源模型的名字+下划线+set。

这个元数据选项可以让你自定义反向关系名,同时也影响反向查询关系名!看下面的例子:

python 复制代码
from django.db import models

class Foo(models.Model):
    pass

class Bar(models.Model):
    foo = models.ForeignKey(Foo, on_delete=models.CASCADE)

    class Meta:
        default_related_name = 'bars'   # 关键在这里

具体的使用差别如下:

python 复制代码
>>> bar = Bar.objects.get(pk=1)
>>> # 不能再使用"bar"作为反向查询的关键字了。
>>> Foo.objects.get(bar=bar)
>>> # 而要使用你自己定义的"bars"了。
>>> Foo.objects.get(bars=bar)

get_latest_by

Django管理器给我们提供有latest()和earliest()方法,分别表示获取最近一个和最前一个数据对象。但是,如何来判断最近一个和最前面一个呢?也就是根据什么来排序呢?

get_latest_by元数据选项帮你解决这个问题,它可以指定一个类似 DateField、DateTimeField或者IntegerField这种可以排序的字段,作为latest()和earliest()方法的排序依据,从而得出最近一个或最前面一个对象。例如:

python 复制代码
 # 根据order_date升序排列
get_latest_by = "order_date"  

 # 根据priority降序排列,如果发生同序,则接着使用order_date升序排列
get_latest_by = ['-priority', 'order_date'] 

managed

该元数据默认值为True,表示Django将按照既定的规则,管理数据库表的生命周期。

如果设置为False,将不会针对当前模型创建和删除数据库表,也就是说Django暂时不管这个模型了。

在某些场景下,这可能有用,但更多时候,你可以忘记该选项。

order_with_respect_to

这个选项不好理解。其用途是根据指定的字段进行排序,通常用于关系字段。看下面的例子:

python 复制代码
from django.db import models

class Question(models.Model):
    text = models.TextField()
    # ...

class Answer(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    # ...

    class Meta:
        order_with_respect_to = 'question'

上面在Answer模型中设置了order_with_respect_to = 'question',这样的话,Django会自动提供两个API,get_RELATED_order()和set_RELATED_order(),其中的RELATED用小写的模型名代替。假设现在有一个Question对象,它关联着多个Answer对象,下面的操作返回包含关联的Anser对象的主键的列表[1,2,3]:

python 复制代码
>>> question = Question.objects.get(id=1)
>>> question.get_answer_order()
[1, 2, 3]

我们可以通过set_RELATED_order()方法,指定上面这个列表的顺序:

python 复制代码
>>> question.set_answer_order([3, 1, 2])

同样的,关联的对象也获得了两个方法get_next_in_order()和get_previous_in_order(),用于通过特定的顺序访问对象,如下所示:

python 复制代码
>>> answer = Answer.objects.get(id=2)
>>> answer.get_next_in_order()
<Answer: 3>
>>> answer.get_previous_in_order()
<Answer: 1>

ordering

最常用的元数据之一了!

用于指定该模型生成的所有对象的排序方式,接收一个字段名组成的元组或列表。默认按升序排列,如果在字段名前加上字符"-"则表示按降序排列,如果使用字符问号"?"表示随机排列。请看下面的例子:

这个顺序是你通过查询语句,获得Queryset后的列表内元素的顺序,切不可和前面的get_latest_by等混淆。

python 复制代码
ordering = ['pub_date']             # 表示按'pub_date'字段进行升序排列
ordering = ['-pub_date']            # 表示按'pub_date'字段进行降序排列
ordering = ['-pub_date', 'author']  # 表示先按'pub_date'字段进行降序排列,再按`author`字段进行升序排列。

permissions

该元数据用于当创建对象时增加额外的权限。它接收一个所有元素都是二元元组的列表或元组,每个元素都是(权限代码, 直观的权限名称)的格式。比如下面的例子:

这个Meta选项非常重要,和auth框架的权限系统紧密相关。

python 复制代码
permissions = (("can_deliver_pizzas", "可以送披萨"),)

default_permissions

Django默认会在建立数据表的时候就自动给所有的模型设置('add', 'change', 'delete')的权限,也就是增删改。你可以自定义这个选项,比如设置为一个空列表,表示你不需要默认的权限,但是这一操作必须在执行migrate命令之前。也是配合auth框架使用。

proxy

如果设置了proxy = True,表示使用代理模式的模型继承方式。具体内容与abstract选项一样,参考模型继承.

required_db_features

声明模型依赖的数据库功能。比如['gis_enabled'],表示模型的建立依赖GIS功能。

required_db_vendor

声明模型支持的数据库。Django默认支持sqlite, postgresql, mysql, oracle。

select_on_save

决定是否使用1.6版本之前的django.db.models.Model.save()算法保存对象。默认值为False。这个选项我们通常不用关心。

indexes

接收一个应用在当前模型上的索引列表,如下例所示:

python 复制代码
from django.db import models

class Customer(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)

    class Meta:
        indexes = [
            models.Index(fields=['last_name', 'first_name']),
            models.Index(fields=['first_name'], name='first_name_idx'),
        ]

unique_together

这个元数据是非常重要的一个!它等同于数据库的联合约束!

举个例子,假设有一张用户表,保存有用户的姓名、出生日期、性别和籍贯等等信息。要求是所有的用户唯一不重复,可现在有好几个叫"张伟"的,如何区别它们呢?(不要和我说主键唯一,这里讨论的不是这个问题)

我们可以设置不能有两个用户在同一个地方同一时刻出生并且都叫"张伟",使用这种联合约束,保证数据库能不能重复添加用户(也不要和我谈小概率问题)。在Django的模型中,如何实现这种约束呢?

使用unique_together,也就是联合唯一!

比如:

python 复制代码
unique_together = [['name', 'birth_day', 'address'],......]

这样,哪怕有两个在同一天出生的张伟,但他们的籍贯不同,也就是两个不同的用户。一旦三者都相同,则会被Django拒绝创建。这个元数据选项经常被用在admin后台,并且强制应用于数据库层面。

unique_together接收一个二维的列表,每个元素都是一维列表,表示一组联合唯一约束,可以同时设置多组约束。为了方便,对于只有一组约束的情况下,可以简单地使用一维元素,例如:

python 复制代码
# 联合唯一无法作用于普通的多对多字段。
unique_together = ['name', 'birth_day', 'address']

index_together

联合索引,用法和特性类似unique_together。

constraints

为模型添加约束条件。通常是列表的形式,每个列表元素就是一个约束。

python 复制代码
from django.db import models

class Customer(models.Model):
    age = models.IntegerField()

    class Meta:
        constraints = [
            models.CheckConstraint(check=models.Q(age__gte=18), name='age_gte_18'),
        ]

上例中,会检查age年龄的大小,不得低于18。

verbose_name

最常用的元数据之一!用于设置模型对象的直观、人类可读的名称,用于在各种打印、页面展示等场景。可以用中文。例如:

python 复制代码
verbose_name = "story"
verbose_name = "披萨"

如果你不指定它,那么Django会使用小写的模型名作为默认值。

verbose_name_plural

英语有单数和复数形式。这个就是模型对象的复数名,比如"apples"。因为我们中文通常不区分单复数,所以保持和verbose_name一致也可以。

python 复制代码
verbose_name_plural = "stories"
verbose_name_plural = "披萨"
verbose_name_plural = verbose_name

如果不指定该选项,那么默认的复数名字是verbose_name加上's'.

label

前面介绍的元数据都是可修改和设置的,但还有两个只读的元数据,label就是其中之一。

label等同于app_label.object_name。例如polls.Question,polls是应用名,Question是模型名。

label_lower

同上,不过是小写的模型名。

模型继承

很多时候,我们都不是从'一穷二白'开始编写模型的,有时候可以从第三方库中继承,有时候可以从以前的代码中继承,甚至现写一个模型用于被其它模型继承。这样做的好处,我就不赘述了,每个学习Django的人都非常清楚。

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

Django中所有的模型都必须继承django.db.models.Model模型,不管是直接继承也好,还是间接继承也罢。

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

Django有三种继承的方式

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

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

抽象基类

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

python 复制代码
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模型不能当做一个正常的模型使用。

那如果我想修改CommonInfo父类中的name字段的定义呢?在Student类中创建一个name字段,覆盖父类的即可。这其实就是很简单的Python语法。

那如果我不需要CommonInfo父类中的name字段呢?在Student类中创建一个name变量,值设为None即可。

抽象基类的Meta数据

如果子类没有声明自己的Meta类,那么它将自动继承抽象基类的Meta类。

如果子类要设置自己的Meta属性,则需要扩展基类的Meta

python 复制代码
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'

这里有几点要特别说明:

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

class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True
        ordering = ['name']

class Unmanaged(models.Model):
    class Meta:
        abstract = True
        managed = False

class Student(CommonInfo, Unmanaged):
    home_group = models.CharField(max_length=5)

    class Meta(CommonInfo.Meta, Unmanaged.Meta):
        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模块:

python 复制代码
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:

python 复制代码
from common.models import Base

class ChildB(Base):
    pass

对于上面的继承关系:

  • common.ChildA.m2m字段的reverse name(反向关系名)应该是 common_childa_related;reverse 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参数,这些问题就不存在了。

多表继承

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

python 复制代码
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的所有字段,并且各有各的数据库表和字段,比如

python 复制代码
>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")

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

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

但是,如果这个Place是个纯粹的Place对象,并不是一个Restaurant对象,那么上面的调用方式会弹出Restaurant.DoesNotExist异常。

让我们看一组更具体的展示,注意里面的注释内容。

python 复制代码
>>> 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字段,形同下面所示:

python 复制代码
place_ptr = models.OneToOneField(
    Place, on_delete=models.CASCADE,
    parent_link=True,
)

可以通过创建一个OneToOneField字段并设置 parent_link=True,自定义这个一对一字段。

从上面的API操作展示可以看出,这种继承方式还是有点混乱的,不如抽象基类来得直接明了。

Meta和多表继承

在多表继承的情况下,由于父类和子类都在数据库内有物理存在的表,父类的Meta类会对子类造成不确定的影响,因此,Django在这种情况下关闭了子类继承父类的Meta功能。这一点和抽象基类的继承方式有所不同。

但是,还有两个Meta元数据属性特殊一点,那就是ordering和get_latest_by,这两个参数是会被继承的。因此,如果在多表继承中,你不想让你的子类继承父类的上面两种参数,就必须在子类中显示的指出或重写。如下:

python 复制代码
class ChildModel(ParentModel):
    # ...

    class Meta:
        # 移除父类对子类的排序影响
        ordering = []

多表继承和反向关联

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

仍以上面Place类为例,我们创建一个带有ManyToManyField字段的子类:

python 复制代码
class Supplier(Place):
    customers = models.ManyToManyField(Place)

这会产生下面的错误:

python 复制代码
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参数.

python 复制代码
customers = models.ManyToManyField(Place, related_name='provider')。

代理模型

使用多表继承时,父类的每个子类都会创建一张新数据表,通常情况下,这是我们想要的操作,因为子类需要一个空间来存储不包含在父类中的数据。但有时,你可能只想更改模型在Python层面的行为,比如更改默认的manager管理器,或者添加一个新方法。

代理模型就是为此而生的。你可以创建、删除、更新代理模型的实例,并且所有的数据都可以像使用原始模型(非代理类模型)一样被保存。不同之处在于你可以在代理模型中改变默认的排序方式和默认的manager管理器等等,而不会对原始模型产生影响。

代理模型其实就是给原模型换了件衣服(API),实际操作的还是原来的模型和数据。

声明一个代理模型只需要将Meta中proxy的值设为True。

例如你想给Person模型添加一个方法。你可以这样做:

python 复制代码
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类进行访问,反之亦然。

python 复制代码
>>> p = Person.objects.create(first_name="foobar")
>>> MyPerson.objects.get(first_name="foobar")
<MyPerson: foobar>

下面的例子通过代理进行排序,但父类却不排序:

python 复制代码
class OrderedPerson(Person):
    class Meta:
        # 现在,普通的Person查询是无序的,而OrderedPerson查询会按照`last_name`排序。
        ordering = ["last_name"]
        proxy = True

一些约束:

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

代理模型的管理器

如不指定,则继承父类的管理器。如果你自己定义了管理器,那它就会成为默认管理器,但是父类的管理器依然有效。如下例子:

python 复制代码
from django.db import models

class NewManager(models.Manager):
    # ...
    pass

class MyPerson(Person):
    objects = NewManager()

    class Meta:
        proxy = True

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

python 复制代码
# Create an abstract class for the new manager.
from django.db import models

class NewManager(models.Manager):
    # ...
    pass

class ExtraManagers(models.Model):
    secondary = NewManager()

    class Meta:
        abstract = True

class MyPerson(Person, ExtraManagers):
    class Meta:
        proxy = True

多重继承

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

Django的模型体系支持多重继承,就像Python一样。如果多个父类都含有Meta类,则只有第一个父类的会被使用,剩下的会忽略掉。

一般情况,能不要多重继承就不要,尽量让继承关系简单和直接,避免不必要的混乱和复杂。

请注意,继承同时含有相同id主键字段的类将抛出异常。为了解决这个问题,你可以在基类模型中显式的使用AutoField字段。如下例所示:

python 复制代码
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字段保持与祖先的关系,如下所示:

python 复制代码
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,如果继承的是一个非抽象基类,那么子类与父类之间不可以有相同的字段名!

比如下面是不行的!

python 复制代码
class A(models.Model):
    name = models.CharField(max_length=30)

class B(A):
    name = models.CharField(max_length=30)

如果你执行python manage.py makemigrations会弹出下面的错误:

python 复制代码
django.core.exceptions.FieldError: Local field 'name' in class 'B' clashes with field of the same name from base class 'A'.

但是!如果父类是个抽象基类就没有问题,如下:

python 复制代码
class A(models.Model):
    name = models.CharField(max_length=30)

    class Meta:
        abstract = True

class B(A):
    name = models.CharField(max_length=30)

用包来组织模型

在我们使用python manage.py startapp xxx命令创建新的应用时,Django会自动帮我们建立一个应用的基本文件组织结构,其中就包括一个models.py文件。通常,我们把当前应用的模型都编写在这个文件里,但是如果你的模型很多,那么将单独的models.py文件分割成一些独立的文件是个更好的做法。

首先,我们需要在应用中新建一个叫做models的包,再在包下创建一个__init__.py文件,这样才能确立包的身份。然后将models.py文件中的模型分割到一些.py文件中,比如organic.py和synthetic.py,然后删除models.py文件。最后在__init__.py文件中导入所有的模型。如下例所示:

python 复制代码
#  myapp/models/__init__.py

from .organic import Person
from .synthetic import Robot

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

验证器

在Django的模型字段参数中,有一个参数叫做validators,这个参数是用来指定当前字段需要使用的验证器,也就是对字段数据的合法性进行验证,比如大小、类型等。

Django的验证器可以分为模型相关的验证器和表单相关的验证器,它们基本类似,但在使用上有区别。

自定义验证器

一个验证器其实就是一个可调用的对象(函数或类),接收一个初始输入值作为参数,对这个值进行一系列逻辑判断,如果不满足某些规则或者条件,则表示验证不通过,抛出一个ValidationError异常。如果满足条件则通过验证,不返回任何内容(也就是默认的return None),可以继续下一步。

验证器具有重要作用,可以被重用在别的字段上,是工具类型的逻辑封装。

下面是一个验证器的例子,它只允许偶数通过验证:

python 复制代码
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def validate_even(value):
    if value % 2 != 0:
        raise ValidationError(
            _('%(value)s is not an even number'),
            params={'value': value},
        )

通过下面的方式,将偶数验证器应用在字段上:

python 复制代码
from django.db import models

class MyModel(models.Model):
    even_field = models.IntegerField(validators=[validate_even])

因为验证器运行之前,(输入的)数据会被转换为 Python 对象,因此我们可以将同样的验证器用在 Django form 表单中(事实上Django为表单提供了另外一些验证器):

python 复制代码
from django import forms

class MyForm(forms.Form):
    even_field = forms.IntegerField(validators=[validate_even])

你还可以通过Python的魔法方法__cal__()编写更复杂的可配置的验证器,比如Django内置的RegexValidator验证器就是这么干的。

验证器也可以是一个类,但这时候就比较复杂了,需要确保它可以被迁移框架序列化,确保编写了deconstruction()和__eq__()方法。这种做法很难找到参考文献和博文,要靠自己摸索或者研究DJango源码。

python 复制代码
from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def save(self, *args, **kwargs):
        if self.name != "blog":
            return # 只有实例的 bolg 的 name 为 blog 才可以保存
        else:
            super().save(*args, **kwargs)  # 调用真正的save方法

*args, **kwargs的参数设计,确保我们自定义的save方法是个万金油,不论Django源码中的save方法的参数怎么变,我们自己的save方法不会因为参数定义的不正确而出现bug。

相关推荐
blues_C1 天前
十三、【核心功能篇】测试计划管理:组织和编排测试用例
vue.js·django·测试用例·drf·测试平台
恸流失2 天前
DJango项目
后端·python·django
编程大全2 天前
41道Django高频题整理(附答案背诵版)
数据库·django·sqlite
网安小张2 天前
解锁FastAPI与MongoDB聚合管道的性能奥秘
数据库·python·django
KENYCHEN奉孝2 天前
Pandas和Django的示例Demo
python·django·pandas
老胖闲聊2 天前
Python Django完整教程与代码示例
数据库·python·django
noravinsc2 天前
django paramiko 跳转登录
后端·python·django
践行见远2 天前
django之请求处理过程分析
数据库·django·sqlite
声声codeGrandMaster2 天前
Django之表格上传
后端·python·django
菌菌的快乐生活2 天前
网站静态文件加速-Django项目静态文件存储到腾讯云COS存储提升网络请求速度
django·cos存储