Django之模型关联

多表关联是模型层的重要功能之一, Django提供了一套基于关联字段独特的解决方案.那就是用:OneToOneFieldForeignKeyManyToMany

先来区分一下什么是一对一、一对多、多对多
一对一:子表从母表中选出一条数据一一对应,母表中选出来一条就少一条,子表不可以再选择母表中已被选择的那条数据
一对多:子表从母表中选出一条数据一一对应,但母表的这条数据还可以被其他子表数据选择
共同点是在admin中添加数据的话,都会出现一个select选框,但只能单选,因为不论一对一还是一对多,自己都是“一”
多对多:
比如有多个孩子,和多种颜色、
每个孩子可以喜欢多种颜色,一种颜色可以被多个孩子喜欢,对于双向均是可以有多个选择
应用场景:
一对一(OneToOneField):一般用于某张表的补充,比如用户基本信息是一张表,但并非每一个用户都需要有登录的权限,不需要记录用户名和密码,此时,合理的做法就是新建一张记录登录信息的表,与用户信息进行一对一的关联,可以方便的从子表查询母表信息或反向查询
外键(ForeignKey):有很多的应用场景,比如每个员工归属于一个部门,那么就可以让员工表的部门字段与部门表进行一对多关联,可以查询到一个员工归属于哪个部门,也可反向查出某一部门有哪些员工
多对多(ManyToMany):如很多公司,一台服务器可能会有多种用途,归属于多个产品线当中,那么服务器与产品线之间就可以做成对多对,多对多在A表添加manytomany字段或者从B表添加,效果一致.

创建模型

实例:我们来假定下面这些概念,字段和关系

作者模型:一个作者有姓名和年龄。

作者详细模型:把作者的详情放到详情表,包含生日,手机号,家庭住址等信息。作者详情模型和作者模型之间是一对一的关系(one-to-one)

出版商模型:出版商有名称,所在城市以及email。

书籍模型: 书籍有书名和出版日期,一本书可能会有多个作者,一个作者也可以写多本书,所以作者和书籍的关系就是多对多的关联关系(many-to-many);一本书只应该由一个出版商出版,所以出版商和书籍是一对多关联关系(one-to-many)。

模型建立如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from django.db import models


# Create your models here.
class Author(models.Model):
"""
作者表
"""
id= models.AutoField(primary_key=True)
name = models.CharField(max_length=50, verbose_name='姓名')
age = models.IntegerField(null=True, blank=True, verbose_name='年龄')
# 关联作者详情表一对一
intro = models.OneToOneField(to='Intro', on_delete=models.CASCADE, verbose_name='简介')
def __str__(self):
return self.name
class Meta:
verbose_name = '作者'
verbose_name_plural = '作者表'

class Intro(models.Model):
'''
作者详情
'''
id= models.AutoField(primary_key=True)
phone = models.BigIntegerField()
addr = models.CharField(max_length=100, null=True, blank=True)
avatar = models.ImageField(upload_to='static/ups/', null=True, blank=True)

def __str__(self):
return str(self.phone)
# 只是为了Django Admin显示友好,排序之类有要求的也可以写入下方元数据
class Meta:
verbose_name='作者详情'
verbose_name_plural="作者详情表"

class Article(models.Model):
'''
文章
'''
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=200, verbose_name='文章标题')
body = models.TextField(verbose_name='文章内容')
cratetd_time = models.DateTimeField('发布时间', auto_now_add=True)
# 关联作者,多对一,外键保存在多的表中
author = models.ForeignKey(to='Author', on_delete=models.CASCADE, related_name='articles', verbose_name='作者')
def __str__(self):
return self.title
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章表'

class Role(models.Model):
'''
角色表
'''
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=50, verbose_name='角色名')
authors = models.ManyToManyField(to='Author', related_name='roles', verbose_name='角色')
def __str__(self):
return self.name
class Meta:
verbose_name = '角色'
verbose_name_plural = '角色表'

生成的表结构:

img

注意事项:

  • 表的名称myapp_modelName,是根据 模型中的元数据自动生成的,也可以覆写为别的名称  
  • id 字段是自动添加的
  • 对于外键字段,Django 会在字段名上添加"_id" 来创建数据库中的列名
  • 这个例子中的CREATE TABLE SQL 语句使用PostgreSQL 语法格式,要注意的是Django 会根据settings 中指定的数据库类型来使用相应的SQL 语句。
  • 定义好模型之后,你需要告诉Django _使用_这些模型。你要做的就是修改配置文件中的INSTALL_APPSZ中设置,在其中添加models.py所在应用的名称。
  • 外键字段 ForeignKey 有一个 null=True 的设置(它允许外键接受空值 NULL),你可以赋给它空值 None

添加表记录

一对多

1
2
3
4
5
6
7
8
9
# 方式一
# 首先获取作者对象
author_obj = Author.objects.get(pk=1)
# 添加文章并关联作者
article_obj = Article.objects.create(title='孙悟空之新野猴戏', body='新野出土的大量汉画砖,除了杂技、游戏之外,猴子、狗和人在一起狩猎、嬉戏的精彩画面屡见不鲜。到了南北朝时期,猴戏已在新野盛行。明清时期,新野民间玩猴就已经较为流行。', author= author_obj)

# 方式二
# 直接指定关联的作者主键
article_obj2 = Article.objects.create(title='猪八戒之身世来历', body='根据《西游记》故事,猪八戒原是天庭中统领八万天河水兵的天蓬元帅,由于蟠桃会上喝酒醉后调戏月宫的霓裳仙子,打了两千锤后被贬下凡,又投错胎变成猪模样,入赘高老庄。', author_id=2)

img

多对多

作者表现有数据

img

1
2
3
4
5
6
7
# 先添加角色
role_obj = Role.objects.create(name='歌手')
# 查询出要关联的两个作者
andy = Author.objects.filter(name='刘德华').first()
jay = Author.objects.filter(name='周杰伦').first()
# 绑定多对多关系, 即向关系表role_authors中添加纪录
role_obj.authors.add(andy, jay)

生成后的数据

img

对象关联查询

一对一(Author 与 Intro)

正向查询

按字段:intro(Author模型中的字段)

1
2
3
4
# 查询刘德华的电话和地址
andy = Author.objects.filter(name='刘德华').first()
print(andy.name, andy.intro.phone, andy.intro.addr)
# 输出:刘德华 15155555555 香港贵妃榻
反向查询

按表名:(模型名小写 author)

1
2
3
4
# 查询手机号为15154545454的作者姓名
intro_obj = Intro.objects.filter(phone=15154545454).first()
print(intro_obj, intro_obj.author.name)
# 输出:15154545454 小白龙

一对多(Author 与 Article)

正向查询

按字段:author(Article模型中的字段)

1
2
3
4
# 查询文章主键为5的作者姓名
article_obj = Article.objects.filter(pk=5).first()
print("《%s》" % article_obj.title, article_obj.author.name)
# 输出:《猪八戒之人物形象》 猪八戒
反向查询
按表名

(模型名小写 article_set)

1
2
3
4
# 查询作者主键为3发布的文章
author_obj = Author.objects.filter(pk=3).first()
print(author_obj.article_set.all())
# 输出:<QuerySet [<Article: 沙悟净之人物评价>, <Article: 沙悟净之形象分析>]>
related_name

Arcticle模型author字段中我们设置了参数related_name,这个参数主要就是用来做反向查询的,我们这里设置了反向查询名称为articles,此时在使用上方的article_set,就会报错:`’Author’ object has no attribute ‘article_set’

1
2
# 关联作者,多对一,外键保存在多的表中
author = models.ForeignKey(to='Author', on_delete=models.CASCADE, related_name='articles', verbose_name='作者')

此时按我们自己设置的反向查询参数来查,得到一样的结果。

1
2
3
4
# 查询作者主键为3发布的文章
author_obj = Author.objects.filter(pk=3).first()
print(author_obj.articles.all())
# 输出:<QuerySet [<Article: 沙悟净之人物评价>, <Article: 沙悟净之形象分析>]>

多对多(Author 与 Role)

正向查询

按字段:Role模型中的字段authors

1
2
3
4
5
6
7
8
9
# 查询角色名为歌手的作者姓名及电话
role_obj = Role.objects.filter(name='歌手').first()
authors = role_obj.authors.all()
print(authors)
for author in authors:
print(author.name, author.intro.phone)
# 输出:<QuerySet [<Author: 刘德华>, <Author: 周杰伦>]>
# 刘德华 15155555555
# 周杰伦 15156565656
反向查询
按表名

(模型名小写 role_set)

1
2
3
4
5
6
7
# 查询主键为2的作者的角色名称
author_obj = Author.objects.filter(pk=2).first()
print(author_obj)
print(author_obj.role_set.all())
# 输出:
# 猪八戒
# <QuerySet [<Role: 神仙>, <Role: 将军>]>
related_name

role模型authors字段中我们设置了参数related_name,这个参数主要就是用来做反向查询的,我们这里设置了反向查询名称为roles,此时在使用上方的role_set,就会报错:`’Author’ object has no attribute ‘role_set’

1
2
3
4
5
6
7
# 查询主键为2的作者的角色名称
author_obj = Author.objects.filter(pk=2).first()
print(author_obj)
print(author_obj.roles.all())
# 输出:
# 猪八戒
# <QuerySet [<Role: 神仙>, <Role: 将军>]>

双下划线关联查询

Django 还提供了一种直观而高效的方式在查询(lookups)中表示关联关系,它能自动确认 SQL JOIN 联系。要做跨关系查询,就使用两个下划线来链接模型(model)间关联字段的名称,直到最终链接到你想要的model 为止。

注意:正向查询按字段,反向查询按表名小写用来告诉ORM引擎join哪张表,反向查询时,如果定义了related_name ,则用related_name替换表名。

一对一

正向查询

按字段,Author模型中的intro字段后跟双下划线关联的Intro模型中的phone字段。

1
2
3
4
# 查询刘德华的手机号
andy = Author.objects.filter(name='刘德华').values('name','intro__phone')
print(andy)
# 输出:<QuerySet [{'name': '刘德华', 'intro__phone': 15155555555}]>
反向查询

关联的表名小写后跟双下划线加字段

1
2
3
4
# 查询刘德华的手机号
intro_obj = Intro.objects.filter(author__name='刘德华').values('author__name', 'phone')
print(intro_obj)
# 输出:<QuerySet [{'author__name': '刘德华', 'phone': 15155555555}]>

一对多

正向查询

按字段:author

1
2
3
4
# 查询猪八戒发表过的文章
articles = Article.objects.filter(author__name='猪八戒').values_list('author__name', 'title')
print(articles)
# 输出:<QuerySet [('猪八戒', '猪八戒自我介绍'), ('猪八戒', '猪八戒之人物形象'), ('猪八戒', '猪八戒之性格特点'), ('猪八戒', '猪八戒之外貌形象'), ('猪八戒', '猪八戒之身世来历')]>
反向查询

按表名:article

1
2
3
4
# 查询猪八戒发表过的文章
query_result = Author.objects.filter(name='猪八戒').values('name', 'articles__title')
print(query_result)
# 输出:<QuerySet [{'name': '猪八戒', 'articles__title': '猪八戒自我介绍'}, {'name': '猪八戒', 'articles__title': '猪八戒之人物形象'}, {'name': '猪八戒', 'articles__title': '猪八戒之性格特点'}, {'name': '猪八戒', 'articles__title': '猪八戒之外貌形象'}, {'name': '猪八戒', 'articles__title': '猪八戒之身世来历'}]>

此处我们使用的是articles__title,并没有使用上面说明的表名article,原因在于我们文章中定义author关联字段时使用了related_name字段,将其设置为了articles,故这里使用articles

多对多

正向查询

按字段:authors

1
2
3
4
# 查询角色为神仙的作者
query_result = Role.objects.filter(name='神仙').values('name', 'authors__name')
print(query_result)
# 输出:<QuerySet [{'name': '神仙', 'authors__name': '孙悟空'}, {'name': '神仙', 'authors__name': '猪八戒'}, {'name': '神仙', 'authors__name': '沙悟净'}, {'name': '神仙', 'authors__name': '小白龙'}]>
反向查询

按表名:role

1
2
3
4
# 查询角色为神仙的作者
query_result = Author.objects.filter(roles__name='神仙').values('name', 'roles__name')
print(query_result)
# 输出:<QuerySet [{'name': '孙悟空', 'roles__name': '神仙'}, {'name': '猪八戒', 'roles__name': '神仙'}, {'name': '沙悟净', 'roles__name': '神仙'}, {'name': '小白龙', 'roles__name': '神仙'}]>

此处我们使用的是roles__name,并没有使用上面说明的表名role,原因在于我们Role模型中中定义authors关联字段时使用了related_name字段,将其设置为了roles,故这里使用roles

连续跨表查询

正向查询
1
2
3
4
# 查询角色为将军的所有作者名字及作者发表的文章
query_result = Article.objects.filter(author__roles__name='将军').values('author__roles__name', 'author__name', 'title')
print(query_result)
# 输出:<QuerySet [{'author__roles__name': '将军', 'author__name': '猪八戒', 'title': '猪八戒自我介绍'}, {'author__roles__name': '将军', 'author__name': '猪八戒', 'title': '猪八戒之人物形象'}, {'author__roles__name': '将军', 'author__name': '猪八戒', 'title': '猪八戒之性格特点'}, {'author__roles__name': '将军', 'author__name': '猪八戒', 'title': '猪八戒之外貌形象'}, {'author__roles__name': '将军', 'author__name': '猪八戒', 'title': '猪八戒之身世来历'}, {'author__roles__name': '将军', 'author__name': '沙悟净', 'title': '沙悟净之人物评价'}, {'author__roles__name': '将军', 'author__name': '沙悟净', 'title': '沙悟净之形象分析'}]>
反向查询
1
2
3
4
# 查询角色为将军的所有作者名字及作者发表的文章
query_result = Role.objects.filter(name='将军').values('name', 'authors__name', 'authors__articles__title')
print(query_result)
# 输出:<QuerySet [{'name': '将军', 'authors__name': '猪八戒', 'authors__articles__title': '猪八戒自我介绍'}, {'name': '将军', 'authors__name': '猪八戒', 'authors__articles__title': '猪八戒之人物形象'}, {'name': '将军', 'authors__name': '猪八戒', 'authors__articles__title': '猪八戒之性格特点'}, {'name': '将军', 'authors__name': '猪八戒', 'authors__articles__title': '猪八戒之外貌形象'}, {'name': '将军', 'authors__name': '猪八戒', 'authors__articles__title': '猪八戒之身世来历'}, {'name': '将军', 'authors__name': '沙悟净', 'authors__articles__title': '沙悟净之人物评价'}, {'name': '将军', 'authors__name': '沙悟净', 'authors__articles__title': '沙悟净之形象分析'}]>
中间表查询
1
2
3
4
# 查询角色为将军的所有作者名字及作者发表的文章
query_result = Author.objects.filter(roles__name='将军').values('roles__name', 'name', 'articles__title')
print(query_result)
# 输出:<QuerySet [{'roles__name': '将军', 'name': '猪八戒', 'articles__title': '猪八戒自我介绍'}, {'roles__name': '将军', 'name': '猪八戒', 'articles__title': '猪八戒之人物形象'}, {'roles__name': '将军', 'name': '猪八戒', 'articles__title': '猪八戒之性格特点'}, {'roles__name': '将军', 'name': '猪八戒', 'articles__title': '猪八戒之外貌形象'}, {'roles__name': '将军', 'name': '猪八戒', 'articles__title': '猪八戒之身世来历'}, {'roles__name': '将军', 'name': '沙悟净', 'articles__title': '沙悟净之人物评价'}, {'roles__name': '将军', 'name': '沙悟净', 'articles__title': '沙悟净之形象分析'}]>

模糊查询示例

模糊双下滑查询,依然可以正向、反向、中间表进行查询。

1
2
3
4
5
6
7
8
9
10
# 查询手机号码以15154开头的作者以及作者发表过的文章
query_result = Intro.objects.filter(phone__regex='15154').values('author__name', 'phone', 'author__articles__title')
print(query_result)
# 输出:<QuerySet [{'author__name': '小白龙', 'phone': 15154545454, 'author__articles__title': '小白龙之角色解读'}, {'author__name': '小白龙', 'phone': 15154545454, 'author__articles__title': '白龙马之原著记载'}]>

# 或者
# 查询手机号码以15154开头的作者以及作者发表过的文章
query_result = Intro.objects.filter(phone__startswith='15154').values('author__name', 'phone', 'author__articles__title')
print(query_result)
# 输出:<QuerySet [{'author__name': '小白龙', 'phone': 15154545454, 'author__articles__title': '小白龙之角色解读'}, {'author__name': '小白龙', 'phone': 15154545454, 'author__articles__title': '白龙马之原著记载'}]>

聚合查询

语法:aggregate(*args, **kwargs)

1
2
3
4
5
from django.db.models import Avg
# 查询所有文章的平均查看数
query_result = Article.objects.all().aggregate(Avg('views'))
print(query_result)
# 输出:{'views__avg': 2156.0}

aggregate()QuerySet 的一个终止子句,意思是说,它返回一个包含一些键值对的字典。键的名称是聚合值的标识符,值是计算出来的聚合值。键的名称是按照字段和聚合函数的名称自动生成出来的。如果你想要为聚合值指定一个名称,可以向聚合子句提供它。

1
2
3
4
5
from django.db.models import Avg
# 查询所有文章的平均查看数
query_result = Article.objects.all().aggregate(avg_views = Avg('views'))
print(query_result)
# 输出:{'avg_views': 2156.0}

如果你希望生成不止一个聚合,你可以向aggregate()子句中添加另一个参数。

1
2
3
4
5
6
7
from django.db.models import Avg
from django.db.models import Max
from django.db.models import Min
# 查询所有文章的平均查看数,最大查看数,最小查看数
query_result = Article.objects.all().aggregate(avg_views = Avg('views'), max_views=Max('views'), min_views=Min('views'))
print(query_result)
# 输出:{'avg_views': 2156.0, 'max_views': 9595, 'min_views': 34}

分组查询

单表查询每一个班级对应的学生数

1
2
3
4
5
from django.db.models import Count
query_result = Student.objects.values('classes').annotate(nums = Count('id'))
print(query_result)
# 输出:<QuerySet [{'classes': '软件二班', 'nums': 4}, {'classes': '图像一班', 'nums': 4}, {'classes': '师范一班', 'nums': 3}]>
# sql:SELECT `wechat_student`.`classes`, COUNT(`wechat_student`.`id`) AS `nums` FROM `wechat_student` GROUP BY `wechat_student`.`classes` ORDER BY NULL LIMIT 21;

多表联合分组查询每个作者及其对应发布的文章数

1
2
3
4
5
from django.db.models import Count
query_result = Author.objects.values('id').annotate(sums = Count('articles')).values('name', 'sums')
print(query_result)
# 输出:<QuerySet [{'name': '孙悟空', 'sums': 6}, {'name': '猪八戒', 'sums': 5}, {'name': '沙悟净', 'sums': 2}, {'name': '小白龙', 'sums': 2}, {'name': '刘德华', 'sums': 0}, {'name': '周杰伦', 'sums': 0}]>
# sql:SELECT `wechat_author`.`name`, COUNT(`wechat_article`.`id`) AS `sums` FROM `wechat_author` LEFT OUTER JOIN `wechat_article` ON (`wechat_author`.`id` = `wechat_article`.`author_id`) GROUP BY `wechat_author`.`id` ORDER BY NULL LIMIT 21;

分组聚合查询示例

查询每个作者发表的文章最少查看数

1
2
3
4
5
6
7
8
9
10
11
12
from django.db.models import Min
query_result = Author.objects.annotate(min_views=Min('articles__views'))
for author_obj in query_result:
print(author_obj.name, author_obj.min_views)
# 输出:
# 孙悟空 56
# 猪八戒 154
# 沙悟净 34
# 小白龙 158
# 刘德华 None
# 周杰伦 None
# sql:SELECT `wechat_author`.`id`, `wechat_author`.`name`, `wechat_author`.`age`, `wechat_author`.`intro_id`, MIN(`wechat_article`.`views`) AS `min_views` FROM `wechat_author` LEFT OUTER JOIN `wechat_article` ON (`wechat_author`.`id` = `wechat_article`.`author_id`) GROUP BY `wechat_author`.`id` ORDER BY NULL;

或者使用values

1
2
3
4
5
from django.db.models import Min
query_result = Author.objects.annotate(min_views=Min('articles__views')).values('name', 'min_views')
print(query_result)
# 输出:<QuerySet [{'name': '孙悟空', 'min_views': 56}, {'name': '猪八戒', 'min_views': 154}, {'name': '沙悟净', 'min_views': 34}, {'name': '小白龙', 'min_views': 158}, {'name': '刘德华', 'min_views': None}, {'name': '周杰伦', 'min_views': None}]>
# sql:SELECT `wechat_author`.`name`, MIN(`wechat_article`.`views`) AS `min_views` FROM `wechat_author` LEFT OUTER JOIN `wechat_article` ON (`wechat_author`.`id` = `wechat_article`.`author_id`) GROUP BY `wechat_author`.`id` ORDER BY NULL LIMIT 21;

查询每个角色的作者数量

1
2
3
4
5
from django.db.models import Count
query_result = Role.objects.annotate(authors_sums = Count('authors')).values('name', 'authors_sums')
print(query_result)
#输出:<QuerySet [{'name': '神仙', 'authors_sums': 4}, {'name': '将军', 'authors_sums': 2}, {'name': '大王', 'authors_sums': 1}, {'name': '龙族', 'authors_sums': 1}, {'name': '歌手', 'authors_sums': 2}]>
#sql:SELECT `wechat_role`.`name`, COUNT(`wechat_role_authors`.`author_id`) AS `authors_sums` FROM `wechat_role` LEFT OUTER JOIN `wechat_role_authors` ON (`wechat_role`.`id` = `wechat_role_authors`.`role_id`) GROUP BY `wechat_role`.`id` ORDER BY NULL LIMIT 21;

统计所有以沙开头的作者对应的角色数

1
2
3
4
5
from django.db.models import Count
query_result = Author.objects.filter(name__startswith='沙').annotate(role_sums = Count('roles')).values('name', 'role_sums')
print(query_result)
# 输出:<QuerySet [{'name': '沙悟净', 'role_sums': 2}, {'name': '沙溢', 'role_sums': 1}]>
# sql:SELECT `wechat_author`.`name`, COUNT(`wechat_role_authors`.`role_id`) AS `role_sums` FROM `wechat_author` LEFT OUTER JOIN `wechat_role_authors` ON (`wechat_author`.`id` = `wechat_role_authors`.`author_id`) WHERE `wechat_author`.`name` LIKE BINARY '沙%' GROUP BY `wechat_author`.`id` ORDER BY NULL LIMIT 21;

查询不止一个角色身份的作者

1
2
3
4
5
from django.db.models import Count
query_result = Author.objects.annotate(role_sums=Count('roles')).filter(role_sums__gt = 1)
print(query_result)
# 输出:<QuerySet [<Author: 孙悟空>, <Author: 猪八戒>, <Author: 沙悟净>, <Author: 小白龙>, <Author: 刘德华>, <Author: 周杰伦>]>
# sql:SELECT `wechat_author`.`id`, `wechat_author`.`name`, `wechat_author`.`age`, `wechat_author`.`intro_id`, COUNT(`wechat_role_authors`.`role_id`) AS `role_sums` FROM `wechat_author` LEFT OUTER JOIN `wechat_role_authors` ON (`wechat_author`.`id` = `wechat_role_authors`.`author_id`) GROUP BY `wechat_author`.`id` HAVING COUNT(`wechat_role_authors`.`role_id`) > 1 ORDER BY NULL LIMIT 21;

查询每一个作者,按其对应的角色数量排序

1
2
3
4
5
from django.db.models import Count
query_result = Author.objects.annotate(role_sums = Count('roles')).order_by('role_sums').values('name', 'role_sums')
print(query_result)
# 输出:<QuerySet [{'name': '沙溢', 'role_sums': 1}, {'name': '刘德华', 'role_sums': 2}, {'name': '周杰伦', 'role_sums': 2}, {'name': '小白龙', 'role_sums': 3}, {'name': '沙悟净', 'role_sums': 3}, {'name': '猪八戒', 'role_sums': 3}, {'name': '孙悟空', 'role_sums': 3}]>
# sql:SELECT `wechat_author`.`name`, COUNT(`wechat_role_authors`.`role_id`) AS `role_sums` FROM `wechat_author` LEFT OUTER JOIN `wechat_role_authors` ON (`wechat_author`.`id` = `wechat_role_authors`.`author_id`) GROUP BY `wechat_author`.`id` ORDER BY `role_sums` ASC LIMIT 21;

F查询

在上面所有的例子中,我们构造的过滤器都只是将字段值与某个常量做比较。如果我们要对两个字段的值做比较,那该怎么做呢?

Django 提供 F() 来做这样的比较。F() 的实例可以在查询中引用字段,来比较同一个 model 实例中两个不同字段的值。

查询收藏数大于查看数的文章

1
2
3
4
5
6
# 查询收藏数大于查看数的文章
from django.db.models import F
query_result = Article.objects.filter(fav__gt=F('views')).values('title', 'fav', 'views')
print(query_result)
# 输出:<QuerySet [{'title': '孙悟空大闹天宫', 'fav': 161, 'views': 56}, {'title': '猪八戒自我介绍', 'fav': 759, 'views': 662}, {'title': '沙悟净之形象分析', 'fav': 4856, 'views': 4565}, {'title': '白龙马之原著记载', 'fav': 258, 'views': 158}, {'title': '猪八戒之性格特点', 'fav': 5638, 'views': 2326}, {'title': '孙悟空之猴王出世', 'fav': 2626, 'views': 356}, {'title': '孙悟空之形象演变', 'fav': 2623, 'views': 1418}, {'title': '猪八戒之身世来历', 'fav': 4154, 'views': 965}, {'title': '沙溢之早年经历', 'fav': 587, 'views': 352}]>
# sql:SELECT `wechat_article`.`title`, `wechat_article`.`fav`, `wechat_article`.`views` FROM `wechat_article` WHERE `wechat_article`.`fav` > `wechat_article`.`views` LIMIT 21;

Django 支持 F() 对象之间以及 F() 对象和常数之间的加减乘除和取模的操作。

查询收藏数大于查看数2倍的文章

1
2
3
4
5
from django.db.models import F
query_result = Article.objects.filter(fav__gt=F('views')*2).values('title', 'fav', 'views')
print(query_result)
# 输出:<QuerySet [{'title': '孙悟空大闹天宫', 'fav': 161, 'views': 56}, {'title': '猪八戒之性格特点', 'fav': 5638, 'views': 2326}, {'title': '孙悟空之猴王出世', 'fav': 2626, 'views': 356}, {'title': '猪八戒之身世来历', 'fav': 4154, 'views': 965}]>
# sql:SELECT `wechat_article`.`title`, `wechat_article`.`fav`, `wechat_article`.`views` FROM `wechat_article` WHERE `wechat_article`.`fav` > (`wechat_article`.`views` * 2) LIMIT 21;

将每篇文章的查看数加100

1
2
3
4
5
from django.db.models import F
query_result = Article.objects.all().update(views = F('views')+100)
print(query_result)
# 输出:17
# sql:UPDATE `wechat_article` SET `views` = (`wechat_article`.`views` + 100);

Q查询

filter() 等方法中的关键字参数查询都是一起进行“AND” 的。 如果你需要执行更复杂的查询(例如OR 语句),你可以使用Q 对象

Q 对象可以使用&| 操作符组合起来。当一个操作符在两个Q 对象上使用时,它产生一个新的Q 对象。

查询标题以孙悟空或者小白龙开头的文章

1
2
3
4
5
from django.db.models import Q
query_result = Article.objects.filter(Q(title__startswith='孙悟空')|Q(title__startswith='小白龙'))
print(query_result)
# 输出:<QuerySet [<Article: 孙悟空大闹天宫>, <Article: 小白龙之角色解读>, <Article: 孙悟空之猴王出世>, <Article: 孙悟空之原型结论>, <Article: 孙悟空之形象演变>, <Article: 孙悟空之新野猴戏>]>
# sql:SELECT `wechat_article`.`id`, `wechat_article`.`title`, `wechat_article`.`body`, `wechat_article`.`cratetd_time`, `wechat_article`.`views`, `wechat_article`.`fav`, `wechat_article`.`author_id` FROM `wechat_article` WHERE (`wechat_article`.`title` LIKE BINARY '孙悟空%' OR `wechat_article`.`title` LIKE BINARY '小白龙%') LIMIT 21;

查询标题以孙悟空且以结论结尾的文章

1
2
3
4
5
from django.db.models import Q
query_result = Article.objects.filter(Q(title__startswith='孙悟空')&Q(title__endswith='结论'))
print(query_result)
# 输出:<QuerySet [<Article: 孙悟空之原型结论>]>
# sql:SELECT `wechat_article`.`id`, `wechat_article`.`title`, `wechat_article`.`body`, `wechat_article`.`cratetd_time`, `wechat_article`.`views`, `wechat_article`.`fav`, `wechat_article`.`author_id` FROM `wechat_article` WHERE (`wechat_article`.`title` LIKE BINARY '孙悟空%' AND `wechat_article`.`title` LIKE BINARY '%结论') LIMIT 21;

可以组合&| 操作符以及使用括号进行分组来编写任意复杂的Q 对象。同时,Q 对象可以使用~ 操作符取反,这允许组合正常的查询和取反(NOT) 查询:

查询标题以孙悟空且时间不为2020年发表的文章

1
2
3
4
5
from django.db.models import Q
query_result = Article.objects.filter(Q(title__startswith='孙悟空') & ~Q(cratetd_time__year=2020)).values('title', 'cratetd_time')
print(query_result)
# 输出:<QuerySet [{'title': '孙悟空之猴王出世', 'cratetd_time': datetime.datetime(2019, 4, 21, 2, 46, 20, tzinfo=<UTC>)}, {'title': '孙悟空之形象演变', 'cratetd_time': datetime.datetime(2019, 4, 21, 3, 7, 19, tzinfo=<UTC>)}]>
# sql:SELECT `wechat_article`.`title`, `wechat_article`.`cratetd_time` FROM `wechat_article` WHERE (`wechat_article`.`title` LIKE BINARY '孙悟空%' AND NOT (`wechat_article`.`cratetd_time` BETWEEN '2019-12-31 16:00:00' AND '2020-12-31 15:59:59.999999')) LIMIT 21;

查询函数可以混合使用Q 对象和关键字参数。所有提供给查询函数的参数(关键字参数或Q 对象)都将”AND”在一起。但是,如果出现Q 对象,它必须位于所有关键字参数的前面

查询标题以孙悟空且时间不为2020年发表的且标题中包含原型文章

1
2
3
4
5
from django.db.models import Q
query_result = Article.objects.filter(Q(title__startswith='孙悟空') & ~Q(cratetd_time__year=2020), title__contains='原型').values('title', 'cratetd_time')
print(query_result)
# 输出:<QuerySet [{'title': '孙悟空之原型结论', 'cratetd_time': datetime.datetime(2018, 4, 21, 2, 46, 48, tzinfo=<UTC>)}, {'title': '孙悟空之原型形象演变', 'cratetd_time': datetime.datetime(2019, 4, 21, 3, 7, 19, tzinfo=<UTC>)}]>
# sql:SELECT `wechat_article`.`title`, `wechat_article`.`cratetd_time` FROM `wechat_article` WHERE (`wechat_article`.`title` LIKE BINARY '孙悟空%' AND NOT (`wechat_article`.`cratetd_time` BETWEEN '2019-12-31 16:00:00' AND '2020-12-31 15:59:59.999999') AND `wechat_article`.`title` LIKE BINARY '%原型%') LIMIT 21;

关联管理器

“关联管理器”是在一对多或者多对多的关联上下文中使用的管理器。它存在于下面两种情况:

一对多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Author(models.Model):
"""
作者表
"""
id= models.AutoField(primary_key=True)
name = models.CharField(max_length=50, verbose_name='姓名')
age = models.IntegerField(null=True, blank=True, verbose_name='年龄')

class Article(models.Model):
'''
文章
'''
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=200, verbose_name='文章标题')
# 关联作者,多对一,外键保存在多的表中
author = models.ForeignKey(to='Author', on_delete=models.CASCADE, related_name='articles', verbose_name='作者')

在上面的例子中,管理器author_obj.articles(默认author_obj.article_set,因为Article模型中使用了related_name,它替代了表名_set方法)拥有下面的方法。

多对多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Author(models.Model):
"""
作者表
"""
id= models.AutoField(primary_key=True)
name = models.CharField(max_length=50, verbose_name='姓名')
age = models.IntegerField(null=True, blank=True, verbose_name='年龄')

class Role(models.Model):
'''
角色表
'''
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=50, verbose_name='角色名')
authors = models.ManyToManyField(to='Author', related_name='roles', verbose_name='角色')

这个例子中,author_obj.roles(默认author_obj.role_set,因为Role模型中使用了related_name,它替代了表名_set方法)和role_obj.authors都拥有下面的方法。

add(obj1[, obj2, ...])

把指定的模型对象添加到关联对象集中

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查询主键为16的文章的文章,其原始作者为沙溢
article_obj = Article.objects.filter(pk=16).first()
print(article_obj, article_obj.author.name)
# 查询主键为6的作者对象
author_obj = Author.objects.filter(pk=6).first()
print(author_obj)
# 将主键为6的作者关联到主键为16的文章对象
author_obj.articles.add(article_obj)
print(article_obj, article_obj.author.name)
# 输出:沙溢之个人介绍 沙溢
# 周杰伦
# 沙溢之个人介绍 周杰伦
# sql:UPDATE `wechat_article` SET `author_id` = 6 WHERE `wechat_article`.`id` IN (16);

在上面的例子中,对于ForeignKey关系,article_obj.save()由关联管理器调用,执行更新操作。然而,在多对多关系中使用add()并不会调用任何 save()方法,而是由QuerySet.bulk_create()创建关系。

批量关联对象

批量关联文章到周杰伦下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查询姓名为周杰伦的作者
author_obj = Author.objects.get(name='周杰伦')
# 输出作者姓名和关联的文章列表
print(author_obj.name, author_obj.articles.all())
# 查询主键大于10小于15的文章
article_lists = Article.objects.filter(pk__gt=10, pk__lt=15)
print(article_lists)
# 将上述查询到的文章关联到作者对象周杰伦
author_obj.articles.add(*article_lists)
print(author_obj.name, author_obj.articles.all())
# 输出:周杰伦 <QuerySet []>
# <QuerySet [<Article: 孙悟空之猴王出世>, <Article: 孙悟空之原型结论>, <Article: 孙悟空之原型形象演变>, <Article: 孙悟空之新野猴戏>]>
#周杰伦 <QuerySet [<Article: 孙悟空之猴王出世>, <Article: 孙悟空之原型结论>, <Article: 孙悟空之原型形象演变>, <Article: 孙悟空之新野猴戏>]>
# sql:UPDATE `wechat_article` SET `author_id` = 6 WHERE `wechat_article`.`id` IN (11, 12, 13, 14); args=(6, 11, 12, 13, 14)

给太少老君批量关联角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 取出太上老君对象,可以看出开始没有关联任何角色
author_obj = Author.objects.get(name='太上老君')
print(author_obj.name, author_obj.roles.all())
# 取出几个角色列表
role_list = Role.objects.filter(pk__lt=3)
print(role_list)
# 给太上老君关联角色
author_obj.roles.add(*role_list)
# 再次查询太上老君关联的角色
print(author_obj.name, author_obj.roles.all())
# 输出:太上老君 <QuerySet []>
# <QuerySet [<Role: 神仙>, <Role: 将军>]>
# 太上老君 <QuerySet [<Role: 将军>, <Role: 神仙>]>
# sql:INSERT IGNORE INTO `wechat_role_authors` (`role_id`, `author_id`) VALUES (1, 8), (2, 8);

或使用主键绑定

1
2
3
4
5
6
7
8
9
10
# 取出太上老君对象,可以看出开始没有关联任何角色
author_obj = Author.objects.get(name='太上老君')
print(author_obj.name, author_obj.roles.all())
# 给太上老君关联主键为1和6的角色
author_obj.roles.add(*[1, 6])
# 再次查询太上老君关联的角色
print(author_obj.name, author_obj.roles.all())
# 输出:太上老君 <QuerySet []>
# 太上老君 <QuerySet [<Role: 演员>, <Role: 神仙>]>
# sql:INSERT IGNORE INTO `wechat_role_authors` (`role_id`, `author_id`) VALUES (1, 8), (6, 8);

*create(*kwargs)

创建一个新的对象,保存对象,并将它添加到关联对象集之中。返回新创建的对象:

为太上老君直接创建关联文章

1
2
3
4
5
6
7
8
9
10
11
12
# 取出作者名为太少老君的对象,并查看其关联的文章
author_obj = Author.objects.get(name='太上老君')
print(author_obj.name, author_obj.article_set.all())
article_obj = author_obj.article_set.create(title='太上老君的炼丹炉', body='太上老君的炼丹炉被孙悟空踢坏了')
# 再次查看太上老君关联的文章
print(author_obj.name, author_obj.article_set.all())
# 刚才添加的文章对象,模型中使用了__str__方法,故输出结果看起来像文章名称
print(article_obj)
# 输出:太上老君 <QuerySet []>
太上老君 <QuerySet [<Article: 太上老君的炼丹炉>]>
太上老君的炼丹炉
# sql:INSERT INTO `wechat_article` (`title`, `body`, `cratetd_time`, `views`, `fav`, `author_id`) VALUES ('太上老君的炼丹炉', '太上老君的炼丹炉被孙悟空踢坏了', '2020-04-22 07:44:28.106224', 5, 3, 8);

上例等价于

1
2
3
# 取出作者名为太少老君的对象,并查看其关联的文章
author_obj = Author.objects.get(name='太上老君')
article_obj = Article.objects.create(title='太上老君的炼丹炉', body='太上老君的炼丹炉被孙悟空踢坏了', author=author_obj)

remove(obj1[, obj2, ...])

从关联对象集中移除执行的模型对象:

移除太上老君的神仙角色

1
2
3
4
5
6
7
8
9
author_obj = Author.objects.get(name='太上老君')
# 太上老君当前角色演员和神仙
print(author_obj.name, author_obj.role_set.all())
role_obj = Role.objects.get(name='神仙')
author_obj.role_set.remove(role_obj)
print(author_obj.name, author_obj.role_set.all())
# 输出:太上老君 <QuerySet [<Role: 演员>, <Role: 神仙>]>
# 太上老君 <QuerySet [<Role: 演员>]>
# sql:DELETE FROM `wechat_role_authors` WHERE (`wechat_role_authors`.`author_id` = 8 AND `wechat_role_authors`.`role_id` IN (1));

批量移除太上老君的角色

1
2
3
4
5
6
7
8
9
10
author_obj = Author.objects.get(name='太上老君')
# 太上老君当前角色演员和神仙
print(author_obj.name, author_obj.role_set.all())
# 移除太上老君现有角色
author_obj.role_set.remove(*author_obj.role_set.all())
# 打印太上老君现有角色
print(author_obj.name, author_obj.role_set.all())
# 输出:太上老君 <QuerySet [<Role: 演员>, <Role: 神仙>]>
# 太上老君 <QuerySet []>
# sql:DELETE FROM `wechat_role_authors` WHERE (`wechat_role_authors`.`author_id` = 8 AND `wechat_role_authors`.`role_id` IN (1, 6));

clear()

从关联对象集中移除一切对象。

移除太上老君所有角色

1
2
3
4
5
6
7
8
author_obj = Author.objects.get(name='太上老君')
# 太上老君当前角色演员和神仙
print(author_obj.name, author_obj.role_set.all())
author_obj.role_set.clear()
print(author_obj.name, author_obj.role_set.all())
# 输出:太上老君 <QuerySet [<Role: 演员>, <Role: 神仙>]>
# 太上老君 <QuerySet []>
# sql:DELETE FROM `wechat_role_authors` WHERE `wechat_role_authors`.`author_id` = 8;

注意:这样不会删除对象 —— 只会删除他们之间的关联。

set()方法

先清空,在设置。

给太上老君附上新的角色

1
2
3
4
5
6
7
8
9
author_obj = Author.objects.get(name='太上老君')
# # 太上老君当前角色演员和神仙
print(author_obj.name, author_obj.role_set.all())
author_obj.role_set.set([6, 4, 2])
print(author_obj.name, author_obj.role_set.all())
# 输出:太上老君 <QuerySet [<Role: 演员>, <Role: 神仙>, <Role: 大王>, <Role: 歌手>]>
# 太上老君 <QuerySet [<Role: 演员>, <Role: 龙族>, <Role: 将军>]>
# sql:DELETE FROM `wechat_role_authors` WHERE (`wechat_role_authors`.`author_id` = 8 AND `wechat_role_authors`.`role_id` IN (1, 3, 5));
# INSERT IGNORE INTO `wechat_role_authors` (`role_id`, `author_id`) VALUES (2, 8), (4, 8);