前面写了一篇有关QuerySet查询的基础知识文章:QuerySet查询基础。
其中,没怎么提及有外键的情况。本篇文章详细讲解如何处理该类型。
先给出示例模型,如下代码:
#coding:utf-8 from django.db import models from django.contrib.auth.models import User #博客模型 class Blog(models.Model): #标题 caption = models.CharField(max_length=50) #作者(外键关联User模型) author = models.ForeignKey(User) #内容 content = models.TextField() #分类(和Tag模型关联,多对多) tags = models.ManyToManyField(Tag) #发表时间 publish_time = models.DateTimeField(auto_now_add=True) #博客分类标签 class Tag(models.Model): #分类名 tag_name = models.CharField(max_length=20,blank=True)
两个模型:Blog和Tag,其中Blog有个tags多对多字段关联。
先思考一下,如何得到如下数据集:
1)获取包含tag的id为1的全部Blog
2)获取Blog标题包含django的全部Tag
先看第1个问题:获取包含tag的id为1的全部Blog。
Blog模型下有个多对多字段(ManyToManyField) tags。我们可以根据该字段使用Tag模型的字段。QuerySet查询如下:
qs = Blog.objects.filter(tags__id=1) #输出SQL语句 print(qs.query)
用法和字段条件修饰差不多,两个下划线加Tag模型的字段。
得到SQL语句如下:
SELECT "blog_blog"."id", "blog_blog"."caption", "blog_blog"."author_id", "blog_blog"."content", "blog_blog"."publish_time" FROM "blog_blog" INNER JOIN "blog_blog_tags" ON ("blog_blog"."id" = "blog_blog_tags"."blog_id") WHERE "blog_blog_tags"."tag_id" = 1 ORDER BY "blog_blog"."publish_time" DESC
其where条件为tag_id等于1。同样,我可以是哟个Tag其他字段作为条件。
当然,还有一种解法是通过Tag模型用set反向获取Blog:
tag = Tag.objects.get(id=1) blogs = tag.blog_set.all()
这个在QuerySet查询基础文中也有提到。但为什么是blog_set,而不是blogs_set, blog_tags_set。这点没提到,待会看第2个问题详细讲解。
第2个问题:获取Blog标题包含django的全部Tag。
这个需要通过判断Blog模型返回Tag模型。问题多对多字段在Blog模型上。Tag不能使用Blog模型的tags字段。也许你可能会采取先判断Blog,再获取Tag,如下代码:
#获取标题包含django的Blog blogs = Blog.objects.filter(caption__icontains='django') #创建集合(集合的元素是不重复的) tags = set() #遍历blogs,获取tag for blog in blogs: for tag in blog.tags.all(): tags.add(tag)
这种代码过于低效且繁琐。QuerySet查询其实可以直接实现我们的需求。
qs = Tag.objects.filter(blog__caption__icontains='django') #输出SQL语句 print(qs.query)
输出的SQL语句有点复杂,还是给大家看看:
SELECT "blog_tag"."id", "blog_tag"."tag_name" FROM "blog_tag" INNER JOIN "blog_blog_tags" ON ("blog_tag"."id" = "blog_blog_tags"."tag_id") INNER JOIN "blog_blog" ON ("blog_blog_tags"."blog_id" = "blog_blog"."id") WHERE "blog_blog"."caption" LIKE %django% ESCAPE '\'
看inner join和where部分即可,从中可看出确实实现了我们的需求。
“blog__caption__icontains”中blog对应Blog模型;caption是Blog模型的caption字段;icontains是包含字符条件(可参考QuerySet查询基础)。
那为什么可以使用blog?为什么是blog而不是别的名称呢?
我在研究测试时发现一个方法:
print(Tag._meta.get_fields()) #输出Tag模型可用字段
得到如下结果:
<ManyToManyRel: blog.blog>, <django.db.models.fields.AutoField: id>, <django.db.models.fields.CharField: tag_name>
原本Tag模型设计只写了tag_name字段。id字段是自动追加的主键。而blog.blog是多对多字段的引用(注意类型后面3字母“Rel”),我们可以通过blog引用字段直接访问Blog模型。就和上面我们Blog模型的tags字段一样使用方法。
blog.blog这个名称有点意思。第1个blog是我这个app应用名,第2个blog是Blog模型的小写。所以上面的blog_set和这里的blog__caption__icontains为什么使用“blog”的原因。
那我们可否修改该引用名称?
可以,这个需要修改Blog模型的外键字段。tags字段修改如下:
tags = models.ManyToManyField(Tag, related_query_name='blogs')
修改完成之后,我们可以使用blogs__caption__icontains作为条件。
ps:可能有人问怎么实现分组统计功能,即Group by。可参考Django用annotate实现联合统计查询。