一、模型

自定义模型

from django.db import models

# 每个自定义模型都继承models.Model
class Person(models.Model):
    # 定义模型字段,每个字段被指定为类属性
    # 每个属性映射为数据库列
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
    # 用于查询模型对象时返回参数
    def __str__(self):
        return self.first_name
    # 使得full_name()方法转变为属性,可以通过self.full_name直接使用,就如同一个属性
    @property
    def full_name(self):
        "Returns the person's full name."
        return f"{self.first_name} {self.last_name}"

对应数据库语句

# myapp_person为自动派生,可使用meta改写
CREATE TABLE myapp_person (
    # 自动创建自增长主键,可在模型定义中使用primary_key改写
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(30) NOT NULL
);

使用模型

INSTALLED_APPS 添加新的应用的时候,务必运行 manage.py migrate,此外你也可以先使用以下命令进行迁移 manage.py makemigrations

字段

每个字段都是某个 Field 类的实例

  • 字段类型用以指定数据库数据类型(如:INTEGER, VARCHAR, TEXT)
  • 在渲染表单字段时默认使用的 HTML 视图 (如: <input type="text">, <select>)
  • 基本的有效性验证功能,用于 Django 后台和自动生成的表单

字段类型:

models.AutoField # IntegerField,根据可用的 ID 自动递增
models.BigAutoField # 适合 1 到 9223372036854775807 的数字
models.BigIntegerField # 适合从 -9223372036854775808 到 9223372036854775807 的数字 默认表单部件是一个 NumberInput
models.BinaryField # 于存储原始二进制数据的字段。可以指定为 bytes、bytearray 或 memoryview。默认BinaryField 将 ediditable 设置为 False
models.BooleanField #  true/false 字段
models.CharField # 字符串字段,适用于小到大的字符串
models.TextField # 大量的文本, 默认表单部件是一个 TextInput
models.DateField 
# auto_now=False/True, 保存对象时,自动将该字段设置为现在,调用 Model.save() 时,该字段才会自动更新
# auto_now_add=False/True 第一次创建对象时,自动将该字段设置为现在,修改字段可以设置以下内容来代替 auto_now_add=True
# 默认表单部件是一个 DateInput
# 对于 DateField: default=date.today ——来自 datetime.date.today()
# 对于 DateTimeField: default=timezone.now ——来自 django.utils.timezone.now()
# 将 auto_now 或 auto_now_add 设置为 True,将导致该字段设置为 editable=False 和 blank=True
models.DateTimeField # 默认表单部件是单独的 DateTimeInput。管理中使用两个单独的 TextInput 部件
models.DecimalField # 固定精度的十进制数 max_digits:数字中允许的最大位数,这个数字必须大于或等于 decimal_places
# decimal_places:与数字一起存储的小数位数
models.EmailField # CharField,使用 EmailValidator 来检查该值是否为有效的电子邮件地址
models.FileField # 文件上传字段,primary_key 参数不支持,如果使用,会引起错误
'''
需要在配置文件中配置 MEDIA_ROOT
MEDIA_ROOT 为 '/home/media', upload_to 为 'photos/%Y/%m/%d'
2007 年 1 月 15 日上传了一个文件,它将被保存在 /home/media/photos/2007/01/15 目录下
upload_to='':设置上传目录和文件名的方式,值会传递给 Storage.save()
设置方法:1. 字符串 2. 调用函数,如 upload_to=user_directory_path
def user_directory_path(instance, filename):
    # file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
    return "user_{0}/{1}".format(instance.user.id, filename)
避免上传的文件填满指定的目录:upload = models.FileField(upload_to="uploads/%Y/%m/%d/"),使用上传日期作目录
storage=None, 存储对象,或是一个返回存储对象的可调用对象,默认表单部件是一个 ClearableFileInput
检索上传文件的盘上文件名,或者文件的大小,可以分别使用 name 和 size 属性
max_length=100,数据库中最大长度
'''
models.FieldFile # 访问一个模型上的 FileField 时,得到一个 FieldFile 的实例作为访问底层文件的代理,其API相同
# 关键的区别:该类所封装的对象不一定是 Python 内置文件对象的封装 相反,它是 Storage.open() 方法结果的封装,
# 该方法可能是 File 对象,也可能是自定义存储对 File API 的实现
models.FloatField # 当 localize 为 False 时是 NumberInput 否则,该字段的默认表单部件是 TextInput
'''
FloatField 内部使用 Python 的 float 类型,而 DecimalField 则使用 Python 的 Decimal 类
'''
models.GenericIPAddressField # IPv4 或 IPv6 地址,字符串格式(如 192.0.2.30 或 2a02:42fe::4 )。该字段的默认表单部件是一个 TextInput
'''
protocol:接受的值是 'both' (默认)、'IPv4' 或 'IPv6'。匹配不分大小写
unpack_ipv4:解压 IPv4 映射地址,如 ::fffff:192.0.2.1。启用该选项,该地址将被解压为 192.0.2.1。默认为禁用。
只有当 protocol 设置为 'both' 时才会启用
'''
models.ImageField # 继承 FileField 的所有属性和方法,验证上传的对象是有效的图像
'''
需要Pillow库
height_field:模型字段的名称,每次保存模型实例时将自动填充图像的高度
width_field:同理
默认表单部件是一个 ClearableFileInput
'''
models.IntegerField # -2147483648 到 2147483647 
models.JSONField # encoder=None, decoder=None
models.TextField # 大的文本字段。该字段的默认表单部件是一个 Textarea
models.TimeField # 接受与 DateField 相同的自动填充选项。该字段默认的表单部件t是一个 TimeInput
models.URLField # URL 的 CharField,由 URLValidator 验证。默认表单部件是一个 URLInput

关系字段

models.ForeignKey
'''
多对一的关系
class Car(models.Model):
    manufacturer = models.ForeignKey(
    	# 需要在一个尚未定义的模型上创建关系,你可以使用模型的名称,而不是模型对象本身
        "Manufacturer",# 尚未定义的模型
        on_delete=models.CASCADE,
    )
'''
models.ManyToManyField
'''
多对多的关系
在实例化 ManyToManyField 的时候使用 through 参数指定多对多关系使用哪个中间模型
限制条件
中间模型要么有且 仅 有一个指向源模型(我们例子当中的 Group )的外键,
或者必须通过 ManyToManyField.through_fields 参数在多个外键当中手动选择一个外键
'''
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)
    
models.OneToOneField
'''
一对一的关系
'''
models.

字段选项

models.CharField(verbose_name="first name", # 字段备注名,关系字段中也存在
    			 max_length=200, 
                 null=false, # True时,字段为空时,Django 会将数据库中该字段设置为 NULL
                 blank=false, #  True,该字段允许为空
                 default="name", # 默认值
                 help_text="hints", # 在对应字段下方打印提示
                 primary_key=True, # 将该字段设置为该模型的主键
                 unique=True, # 字段值表中唯一
                 choices=[
    					("FR", "Freshman"),
    					("SO", "Sophomore"),
    					("JR", "Junior"),
    					("SR", "Senior"),
    					("GR", "Graduate"),
						] # 一系列二元组,用作此字段的选项
                 # 提供了二元组,默认表单小部件是一个选择框,而不是标准文本字段,并将限制给出的选项,只能从选项中选择
                 # 每个二元组的第一个值会储存在数据库中,而第二个值将只会用于在表单中显示
                )
# 使用枚举类以简洁的方式来定义 choices 
MedalType = models.TextChoices("MedalType", "GOLD SILVER BRONZE")
choices=MedalType.choices

# 使用model.save()更新模型操作

Meta 选项

给模型赋予元数据[写在模型类里面]

比如排序选项( ordering ),数据库表名( db_table ),或是阅读友好的单复数名( verbose_name 和 verbose_name_plural )

如:

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

    class Meta:
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

模型继承

  • 抽象基类

class Meta中增加abstract = True即可

抽象基类不会生成数据表,也没有管理器,也不能被实例化和保存,基类数据和方法可被子类覆盖重写

子类中abstract = True会自动变为False

父类地Meta也可被继承 class Meta(father_model.Meta)

# 若子类继承多个基类,子类中必须显式继承
class Meta(father_model1.Meta, father_model2.Meta):
        pass

# 在多对多关系中,需要提供独一无二的反向名字与查询名字 related_name 与 related_query_name
models.ManyToManyField(
        OtherModel,
        related_name="%(app_label)s_%(class)s_related",
        related_query_name="%(app_label)s_%(class)ss",
    )
  • 多表继承

子类可以访问父类的表数据

father.objects.filter(name="Bob's Cafe")
son.objects.filter(name="Bob's Cafe")

既是父对象也是子对象,使用模型名称小写进行查询

# Restaurant 中自动创建的连接至 Place 的 OneToOneField:
# 可以在 Restaurant 中重写该字段,parent_link=True,必须设置
place_ptr = models.OneToOneField(
    Place,
    on_delete=models.CASCADE,
    parent_link=True,
    primary_key=True,
)
  • Meta与多表继承

多表继承中子类不会继承父类的 Meta

Meta 类选项已被应用至父类,在子类中再次应用会导致行为冲突[基类中不存在此问题]

子类无法访问父类Meta类,但可以显式的禁用部分选项

class Meta:
        # 去除父类的排序
        ordering = []

多表继承使用隐式的 OneToOneField 连接子类和父类,父类可直接访问子类

  • 代理模型

适用于对已有模型增加新方法或属性时,即可创建一个代理模型来继承原模型并添加新方法或属性,而不用修改原模型

将一个模型类的Meta 类的 proxy 属性设置为 True表面其是一个代理模型

  • 多重继承

第一个出现的基类(比如 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

字段名不可隐藏,但可以显式删除 field_name = None

二、执行查询

创建对象

# 方法一
b = Blog(name="Beatles Blog", tagline="All the latest Beatles news.")
b.save()
'''
此方法仅显式调用save才会进行数据库操作,无返回值
参数列表:force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None
还存在asave方法用于异步
'''
# 方法二
p = Person.objects.create(first_name="Bruce", last_name="Springsteen")
'''
异步版本: acreate()
get_or_create():
用于查找具有给定 kwargs 的对象(如果你的模型对所有字段都有默认值,则可能为空),必要时创建一个对象
返回 (object, created) 的元组,其中 object 是检索或创建的对象,created 是指定是否创建新对象的布尔值
update_or_create():
更新对象,其余一致
'''

保存 ForeignKeyManyToManyField 字段

# ForeignKey
entry = Entry.objects.get(pk=1)
cheese_blog = Blog.objects.get(name="Cheddar Talk")
entry.blog = cheese_blog
entry.save()
# ManyToManyField 使用add方法
joe = Author.objects.create(name="Joe")
entry.authors.add(joe)
entry.authors.add(john, paul, george, ringo)

数据库检索对象

通过模型类的 Manager 构建一个 QuerySet 来实现,只能通过模型类访问,不能使用实例,目的是强制分离 “表级” 操作和 “行级” 操作

# 过滤器检索多个对象,允许链式操作即 .filter().exclude()[因为返回都为新的 QuerySet ,与之前无关] 可能会产生重复的结果
all() # 返回的 QuerySet 包含了数据表中所有的对象。大多数情况下,只需要完整对象集合的一个子集。
# 要创建一个这样的子集,要通过添加过滤条件精炼原始 QuerySet 两种 QuerySet 的方式:
filter(**kwargs) # 返回一个新的 QuerySet,包含的对象满足给定查询参数。
exclude(**kwargs) # 返回一个新的 QuerySet,包含的对象 不 满足给定查询参数
# 若要限制返回时每次显示数目,可使用切片实现,注意:不支持负索引以及对切片结果进行进一步的排序或过滤
# 对 QuerySet 进行切片会返回一个新的 QuerySet,它不会评估查询。
# 一个例外是如果使用了 Python 切片语法的 "step" 参数。例如,这会实际执行查询,以返回前10个对象中的每 第二个 对象的列表:
Entry.objects.all()[:10:2]

Entry.objects.filter(pub_date__year=2006) # 注意: QuerySet 的结果只有当使用时才会对数据库执行过滤操作
Entry.objects.all().filter(pub_date__year=2006) # 即这句只有对返回值进行相关操作时才进行数据库操作

# get() 检索单个对象
one_entry = Entry.objects.get(pk=1)
'''
pk id 以及模型对象所拥有的参数名称可用于检索
pk__in=[1, 4, 7] # id in 1, 4 and 7
pk__gt=14 # id > 14
要注意:
DoesNotExist 异常
MultipleObjectsReturned 异常
'''

字段查询

# 基本的查找关键字参数采用形式 field__lookuptype=value (使用双下划线)
Entry.objects.filter(pub_date__lte="2006-01-01")
# 转换为 SQL 语句大致如下:
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';
#  "exact" 匹配
# iexact 不区分大小写的匹配
Entry.objects.get(headline__exact="Cat bites dog")
# 转换为 SQL 语句大致如下:
SELECT ... WHERE headline = 'Cat bites dog';
# contains 大小写敏感的包含测试
# 大小写不敏感的版本, icontains
Entry.objects.get(headline__contains="Lennon")
SELECT ... WHERE headline LIKE '%Lennon%';
'''
startswith, endswith
以……开头和以……结尾的查找。当然也有大小写不敏感的版本,名为 istartswith 和 iendswith
'''

# 在 ForeignKey 中,可以指定以 _id 为后缀的字段名
# value 参数需要包含 foreign 模型的主键的原始值。例子:
Entry.objects.filter(blog_id=4)

跨关系查询

# 检索所有具有 name 为 'Beatles Blog' 的 Blog 的 Entry 对象
Entry.objects.filter(blog__name="Beatles Blog")
# 反向工作
Blog.objects.filter(entry__headline__contains="Lennon")
# 要选择所有包含 2008 年至少一个标题中有 "Lennon" 的条目的博客(满足两个条件的同一条目):
Blog.objects.filter(entry__headline__contains="Lennon", entry__pub_date__year=2008)

# 一次 exclude() 调用的条件并不需要指向同一项目
# 以下查询会排除那些关联条目标题包含 "Lennon" 且发布于 2008 年的博客:但是其并不会限制博客同时满足这两种条件
Blog.objects.exclude(
    entry__headline__contains="Lennon",
    entry__pub_date__year=2008,
)
# 若需要同时满足来排除操作[两次查询]
Blog.objects.exclude(
    entry__in=Entry.objects.filter(
        headline__contains="Lennon",
        pub_date__year=2008,
    ),
)

过滤器可以为模型指定字段[使用F 表达式]

from django.db.models import F
# F("number_of_pingbacks")为当前模型的所有实例的值引用
# 支持在 F() 对象中使用加法、减法、乘法、除法、取模和幂算术,既可以与常数一起使用,也可以与其他 F() 对象一起使用
# 支持位操作,包括 .bitand()、.bitor()、.bitxor()、.bitrightshift() 和 .bitleftshift() 在支持位操作的数据库中才可
F("somefield").bitand(16)
Entry.objects.filter(number_of_comments__gt=F("number_of_pingbacks"))

from django.db.models import Min
# aggregate聚合查询计算某个字段的聚合值(如总和、平均值、最大值、最小值等)
Entry.objects.aggregate(first_published_year=Min("pub_date__year"))

LIKE 语句中转义百分号和下划线

百分号和下划线自动转义。(在 LIKE 语句中,百分号匹配多个任意字符,而下划线匹配一个任意字符。)

Entry.objects.filter(headline__contains="%")
SELECT ... WHERE headline LIKE '%\%%';

缓存和 QuerySet

新创建的QuerySet为空,只有执行数据库查询后才会添加缓存,并将缓存内容返回至调用语句中

对于同一类模型需要重复查询数据库时应当保存查询结果的缓存,减少数据库负载

queryset = Entry.objects.all()
print([p.headline for p in queryset])
print([p.pub_date for p in queryset]) # 使用保存的缓存数据,避免重复查询

# 注意:
# 使用数组切片或索引的 限制查询结果集 不会填充缓存,因为只有调用结果时才会进行数据库操作
# 打印查询结果集不会填充缓存。因为调用 __repr__() 仅返回了完整结果集的一个切片
# 反复获取查询集对象中的某个索引会每次查询数据库:
queryset = Entry.objects.all()
print(queryset[5])  # Queries the database
print(queryset[5])  # Queries the database again

# 如果整个查询集已经被评估过,那么会检查缓存而不是查询数据库:

queryset = Entry.objects.all()
[entry for entry in queryset]  # Queries the database
print(queryset[5])  # Uses cache
print(queryset[5])  # Uses cache

查询迭代

for 遍历查询会在幕后导致阻塞的数据库查询,因为 Django 在迭代时加载结果,可以切换到 async for

async for entry in Authors.objects.filter(name__startswith="A"):

此外,也不能遍历查询集的操作

filter()exclude(),实际上不会运行查询 —— 它们设置了查询集,以便在迭代时运行 —— 所以可以在异步代码中自由使用

QuerySet 和管理器方法

  • 返回新查询集的方法:这些方法是非阻塞的,没有异步版本。在任何情况下自由使用它们,使用之前请阅读关于 defer()only() 的注释
  • 不返回查询集的方法:这些方法是阻塞的,有异步版本 - 在每个方法的文档中都有它的异步名称,标准模式是在名称前加上 a 前缀
# 一个有效的异步查询示例:
user = await User.objects.filter(username=my_input).afirst()

事务

异步查询和更新不支持事务

使用事务,建议将 ORM 代码编写在单独的同步函数中,然后使用 sync_to_async 来调用它

查询 JSONField

将 None 存储为字段的值将其存储为 SQL NULL。虽然不推荐这样做,

但可以通过使用 Value(None, JSONField()) 来存储 JSON 标量的 null,而不是 SQL NULL

无论存储哪种值,当从数据库检索时,JSON 标量 null 的 Python 表示法与 SQL 的 NULL 相同,即 None

保存 JSON 的 null 值不违反 Django 的 null=False

**4.2 版后已移除:**传递 Value("null") 以表示 JSON null 已弃用

键、索引和路径转换:

Dog.objects.create(name="Meg", data={"breed": "collie", "owner": None})
Dog.objects.filter(data__breed="collie")
# 也可以多个键链接查询
# 如果键是整数,则将其解释为数组中的索引变换
Dog.objects.filter(data__owner__name="Bob")
# 要查询缺少的键,请使用 isnull 查找,不能使用exclude() 和 filter() ,因为不能保证产生详尽的集合
Dog.objects.create(name="Shep", data={"breed": "collie"})
Dog.objects.filter(data__owner__isnull=True)

KT() 表达式: 表示 JSONField 的键、索引或路径变换的文本值

from django.db.models.fields.json import KT
Dog.objects.create(
     name="Shep",
     data={
         "owner": {"name": "Bob"},
         "breed": ["collie", "lhasa apso"],
     },
 )
# <Dog: Shep>
Dogs.objects.annotate(
     first_breed=KT("data__breed__1"), owner_name=KT("data__owner__name")
 ).filter(first_breed__startswith="lhasa", owner_name="Bob")
# <QuerySet [<Dog: Shep>]>

包含与键查找

Oracle 和 SQLite 不支持 contains

Dog.objects.create(name="Rufus", data={"breed": "labrador", "owner": "Bob"})
# <Dog: Rufus>
Dog.objects.create(name="Meg", data={"breed": "collie", "owner": "Bob"})
# <Dog: Meg>
Dog.objects.create(name="Fred", data={})
# <Dog: Fred>
Dog.objects.filter(data__contains={"owner": "Bob"})
# <QuerySet [<Dog: Rufus>, <Dog: Meg>]>
Dog.objects.filter(data__contains={"breed": "collie"})
# <QuerySet [<Dog: Meg>]>

contained_by

这是 contains 查询的反向 - 返回的对象将是那些对象,其上的键值对是传递值中的子集

Dog.objects.filter(data__contained_by={"breed": "collie", "owner": "Bob"})
# <QuerySet [<Dog: Meg>, <Dog: Fred>]>

has_key:返回具有特定键的对象

has_keys: 返回同时具有多个特定键的对象

has_any_keys:返回多个键中具有某个特定键的所有对象[或的关系]

Dog.objects.filter(data__has_key="owner")
# <QuerySet [<Dog: Meg>, <Dog: Rufus>]>

通过 Q 对象完成复杂查询

from django.db.models import Q

Q(question__startswith="Who") | Q(question__startswith="What")
# 等同于
WHERE question LIKE 'Who%' OR question LIKE 'What%'

Q(question__startswith="Who") | ~Q(pub_date__year=2005)
# 为查询函数提供了多个 Q 对象参数,这些参数会通过 "AND" 连接
Poll.objects.get(
    Q(question__startswith="Who"),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)
# 若提供了 Q 对象,那么它必须位于所有关键字参数之前
# 以下查询无效
Poll.objects.get(
    question__startswith="Who",
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
)
# 比较对象
# 对于模型(主键字段)使用 == 进行判断,以下两句等效
some_obj == other_obj
some_obj.name == other_obj.name
# 删除对象
e.delete()
# 批量删除
Entry.objects.filter(pub_date__year=2005).delete()
# 想要删除所有对象,你必须显示请求完整结果集合:
Entry.objects.all().delete()
# Entry.objects.delete() 无效
# 批量修改 update() 方法是直接转为 SQL 语句,并不是重新调用save[save不能进行批量保存,必须迭代每个模型实例保存]
# update时不能使用 F表达式
Entry.objects.filter(pub_date__year=2007).update(headline="Everything is the same")
# 复制模型实例
# 无内置方法,但可通过将 pk 设置为 None 并将 _state.adding 设置为 True
blog = Blog(name="My blog", tagline="Blogging is easy")
blog.save()  # blog.pk == 1

blog.pk = None
blog._state.adding = True
blog.save()  # blog.pk == 2

# 若Blog为继承,则由于继承的工作方式,必须将 pk 和 id 都设置为 None,并将 _state.adding 设置为 True
blog.pk = None
blog.id = None
blog._state.adding = True
blog.save()  # django_blog.pk == 4
'''
不会拷贝不是模型数据表中的关联关系。
例如, Entry 有一个对 Author 的 ManyToManyField 关联关系。
在复制条目后,必须为新条目设置多对多关联关系
'''
# 对于 OneToOneField 关联,必须拷贝关联对象,并将其指定给新对象的关联字段,避免违反一对一唯一性约束
detail = EntryDetail.objects.all()[0]
detail.pk = None
detail._state.adding = True
detail.entry = entry
detail.save()

三、聚合

from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()


class Publisher(models.Model):
    name = models.CharField(max_length=300)


class Book(models.Model):
    name = models.CharField(max_length=300)
    pages = models.IntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    rating = models.FloatField()
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    pubdate = models.DateField()


class Store(models.Model):
    name = models.CharField(max_length=300)
    books = models.ManyToManyField(Book)

模型执行常见的聚合查询:

Book.objects.count() # 模型对象总数
Book.objects.filter(publisher__name="BaloneyPress").count() # 特定对象总数
from django.db.models import Avg, Max, Min, Count # Max,Min输出格式为Decimal
Book.objects.aggregate(avg=Avg("price", default=0)) # 对模型某个字段进行聚合计算返回一个名值对的字典 {'avg': 34.35},
# 若不手动命名则默认为 {'price__avg': 34.35},
Book.objects.aggregate(price_diff=Max("price", output_field=FloatField()) - Avg("price")) # 对Max输出格式进行转换
pubs = Publisher.objects.annotate(num_books=Count("book"))
pubs
# <QuerySet [<Publisher: BaloneyPress>, <Publisher: SalamiPress>, ...]>
pubs[0].num_books
# 73
# 结合Q对象以及过滤器
above_5 = Count("book", filter=Q(book__rating__gt=5))
below_5 = Count("book", filter=Q(book__rating__lte=5))
# annotate() 子句可以生成每一个对象的汇总,输出为 QuerySet
pubs = Publisher.objects.annotate(below_5=below_5).annotate(above_5=above_5) # 也可进行排序 .order_by("-num_books")[:5]
pubs[0].above_5 # 23
pubs[0].below_5 # 12

# 注意:使用 annotate() 组合多个聚合将产生错误的结果
q = Book.objects.annotate(Count('authors'), Count('store')) # 将会产生错误,正常运行但是结果不对
q = Book.objects.annotate(Count('authors', distinct=True), Count('store', distinct=True)) # 使用 distinct 参数来避免

# 连接(Joins)和聚合 【聚合的值属于你正在查询模型的关联模型】
# 找出每家商店提供的书籍价格范围
Store.objects.annotate(min_price=Min("books__price"), max_price=Max("books__price"))
# 要提取任何可售书籍中最年轻作者的年龄
Store.objects.aggregate(youngest_age=Min("books__authors__age"))

反向关系

# 使用 'book' 来指定 Publisher -> Book 的反向外键跳跃):
from django.db.models import Avg, Count, Min, Sum
Publisher.objects.annotate(Count("book"))
# 不仅适用于外键,还适用于多对多关系
# 使用 'book' 来指定 Author -> Book 的反向多对多跳跃):
Author.objects.annotate(total_pages=Sum("book__pages"))

聚合和其他 QuerySet 子句

filter()exclude()

annotate() 子句一起使用时,过滤器的效果是限制计算注释的对象

aggregate() 子句一起使用时,过滤器的效果是限制计算聚合的对象

过滤注解

注解的别名可以和任何其他模型字段一样使用 filter()exclude()

# 具有多位作者的书籍列表
# 查询生成一个注解结果集,然后生成一个基于注解的过滤器
Book.objects.annotate(num_authors=Count("authors")).filter(num_authors__gt=1)

annotate()filter() 子句的顺序

在第一个查询里,注解优先于过滤器,因此过滤器没有影响注解。distinct=True 用来避免 a query bug

第二个查询每个发布者评分3以上的书籍数量。过滤器优先于注解,因此过滤器约束计算注解时考虑的对象

必须将过滤器置于注解之前进行约束

values()

使用 values() 子句来对结果集进行约束时,生成注解值的方法会稍有不同。不是在原始 QuerySet 中对每个对象添加注解并返回,而是根据定义在 values() 子句中的字段组合先对结果进行分组,再对每个单独的分组进行注解

# 查询每个作者所著书的平均评分:
Author.objects.annotate(average_rating=Avg('book__rating'))
'''
values
作者会按名字分组,所以你只能得到不重名的作者分组的注解值
意味着如果你有两个作者同名,那么他们原本各自的查询结果将被合并到同一个结果中
两个作者的所有评分都将被计算为一个平均分
'''
Author.objects.values("name").annotate(average_rating=Avg("book__rating"))

聚合注解

# 计算每本书的平均作者数量,您首先要用作者数量对书籍集进行注释,然后对该作者数量进行聚合
Book.objects.annotate(num_authors=Count("authors")).aggregate(Avg("num_authors")

在空查询集或组上进行聚合操作时,需要格外小心,因为这可能会导致未定义的行为或错误。

在执行聚合操作之前,通常应确保查询集或组中包含足够的数据以执行所需的聚合计算。

如果查询集或组为空,可以使用条件语句来避免聚合错误或不必要的操作

# 提供 `default` 参数来指定返回值。但是,由于` Count` 不支持` default `参数,它在空的查询集或分组上始终返回 0
Book.objects.filter(name__contains="web").aggregate(Sum("price"))
# 默认返回{"price__sum": None}
# 提供默认值返回
Book.objects.filter(name__contains="web").aggregate(Sum("price", default=0))
# {"price__sum": Decimal("0")}

四、搜索

标准文本查询

Author.objects.filter(name__contains="Terry")

数据库比较函数[仅支持PostgreSQL]

# 处理非英语姓名时,进一步的改进是使用 非重音比较:
# 这种比较是不对称的 —— 筛选 Helen 能拿到 Helena 或 Hélène,但反着来却不行
Author.objects.filter(name__unaccent__icontains="Helen")
# [<Author: Helen Mirren>, <Author: Helena Bonham Carter>, <Author: Hélène Joy>]

# 三元搜索综合考虑了三种字母的所有组合形式,并同时再查询和源字符串中比较了出现的次数。
# 对于长名字,源字符串中包含了更多的组合方式,所以其不再被认为是一种近似匹配
Author.objects.filter(name__unaccent__lower__trigram_similar="Hélène")
# [<Author: Helen Mirren>, <Author: Hélène Joy>]

文档搜索

PostgreSQL 内置专属的全文本搜索实现。内置在数据库中。

# 选择所有提到 "cheese" 的博客条目
Entry.objects.filter(body_text__search="cheese")
'''
annotate()方法用于向查询集添加额外的注解,这些注解是数据库级别的计算字段,但不会被保存回数据库。
使用SearchVector(来自Django的SearchVector或PostgreSQL的to_tsvector函数)来创建一个搜索向量,
该向量结合了blog__tagline和body_text两个字段的内容。
'''
Entry.objects.annotate(
	search=SearchVector("blog__tagline", "body_text"),
	 ).filter(search="cheese")

五、管理器

Manager 是一种接口,它赋予了 Django 模型操作数据库的能力。Django 应用中每个模型拥有至少一个 Manager

# 模型基类中重命名
class Person(models.Model):
    # Person.objects 会产生一个 AttributeError 异常,而 Person.people.all() 会返回包含所有 Person 对象的列表
    people = models.Manager()
# 继承与改写
class DahlBookManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(author="Roald Dahl")
    # 同一模型中使用多个管理器
    people = models.Manager()
    authors = AuthorManager()
    editors = EditorManager()