外键的QuerySet查询

  • 发布时间:2017年4月26日 10:17
  • 作者:杨仕航
  • 分类标签: Django
  • 阅读(11147)
  • 评论(0)

前面写了一篇有关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实现联合统计查询

上一篇:ContentTypes及其QuerySet查询

下一篇:QuerySet查询基础

相关专题: Django QuerySet查询   

评论列表

智慧如你,不想发表一下意见吗?

新的评论

清空