Django基础:QuerySet查询基础与技巧

发布于 2020-12-07  2,349 次阅读


一开始使用Django的QuerySet获取数据不太习惯。简单的条件可以通过get或filter获取,但复杂的我总是用SQL语句查询得到。随着接触QuerySet越来越多,发现它相当锋利、健全。

QuerySet是Django的查询集,可以通过QuerySet条件查询得到对应模型的对象集合。

相对直接使用SQL而言,QuerySet可以防止大部分SQL注入,而且提高代码可读性。毕竟Django的模型和具体表名不太一样,需要查阅才得知表名,而且写一大串SQL代码可能直接把人看晕。假如碰到模型变动(改名、增删字段等),SQL可能又要重新调整。所以,能使用QuerySet的情况下,最好使用QuerySet。

还有重要一点,QuerySet是懒惰的。创建一个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,blank=True)

        

        #发表时间

        publish_time = models.DateTimeField(auto_now_add=True)

        

    #博客分类标签

    class Tag(models.Model):

        #分类名

        tag_name = models.CharField(max_length=20,blank=True)

1、QuerySet和SQL语句

首先讲如何将SQL语句解析成RawQuerySet对象。先让不会使用QuerySet的同学顺利使用SQL语句查询。

不熟悉SQL的可以跳过该部分,如下代码:

sql = 'select * from blog_blog' #需要查询数据库具体Blog对应表名

qs = Blog.objects.raw(sql) #将sql语句转成RawQuerySet对象

该SQL是获取全部记录,相当于QuerySet如下查询:

qs = Blog.objects.all()

以上的方法可将SQL语句转成Blog模型的查询集。

不过这个RawQuerySet和QuerySet稍微有些不同,属性和方法没有QuerySet多。例如我想知道这个查询有多少条记录。QuerySet可以直接使用count方法或者len方法获取,而RawQuerySet没有。这个也有方法处理,如下代码:

    #给rawqueryset对象加上len()方法

    def add_len_to_raw_query(query):

        from django.db.models.query import RawQuerySet

        def __len__(self): 

            from django.db import connection

            sql = 'select count(*) from (%s) as newsql' % query.raw_query

            with connection.cursor() as cursor:

                cursor.execute(sql)

                row = cursor.fetchone()

            return row[0]

        setattr(RawQuerySet, '__len__', __len__)

        

    sql = 'select * from blog_blog'

    qs = Blog.objects.raw(sql)

     

    add_len_to_raw_query(qs)  #给qs加上len()方法

    print(len(qs))

我们可通过该方法设置并获取相关对象。另外,使用SQL查询,我们还可以通过SQL语句给RawQuerySet对象添加额外的属性。raw方法会解析SQL语句中的字段,将字段转成属性的方式方便调用。例如:

    sql = '''

        Select blog_blog.id, blog_blog.caption, count(blog_blog_tags.id) as tag_count

        From blog_blog

        Left join blog_blog_tags on blog_blog.id = blog_blog_tags.blog_id

        Group by blog_blog.id, blog_blog.caption

    '''

     

    qs = Blog.objects.raw(sql)

    print(qs[0].tag_count)

上面SQL语句是得到每篇Blog对应分类标签的个数,是不是觉得SQL很复杂了。

不过,我们可以在查询结果直接使用tag_count属性。

另外,为了方便测试,我们可以输出QuerySet对象的SQL语句:

qs = Blog.objects.filter(id=1)print(qs.query)

2、filter和get方法

上面使用了filter。filter是筛选的意思,通过filter筛选得到符合条件的数据集。

例如我分别获取如下情况的数据集。

    #获取blog id为1的数据集

    qs1 = Blog.objects.filter(id=1)

     

    #获取作者user所发表的博客

    user = request.user #先使用当前登录的用户判断

    qs2 = Blog.objects.filter(author=user)

     

    #多个条件用逗号隔开

    qs3 = Blog.objects.filter(id=1, author=user)

Blog id为1相当于SQL:

select * from blog_blog where id = 1

这里你会发现只有一条记录,而filter返回是一个只有一条记录的数据集。获取该数据还需要进一步获取:

q = qs1[0]

这时可以使用get方法直接获取该条记录:

q = Blog.objects.get(id=1)

不过,若符合条件的没有记录或多条记录会抛出异常。我们可以利用这个特点做一些操作:

    try:

        q = Blog.objects.get(caption='test') #找标题为test的博客

    except Blog.DoesNotExist:

        q = Blog(caption='test') #创建标题为test的博客

        #...

        q.save()

当然,django有个get_or_create方法可以简化该过程:

#获取标题为test的博客,不存在则创建q = Blog.objects.get_or_create(caption='test')

3、不等于条件

filter筛选的条件都是符合的条件,也就是我们SQL语句中的等于。有时候,我们希望排除一些条件,也就是不等于或者not条件:

select * from blog_blog where id <> 1select * from blog_blog where not(id = 1)

这种类型的条件,需要使用exclude方法。例如:

qs = Blog.objects.exclude(id=1)

如果我要实现复杂一些的查询,获取当前登录用户发表的博客,并排除id为1的博客。查询如下:

user = request.userqs = Blog.objects.filter(author=user).exclude(id=1)

这里延伸QuerySet另一特性,可以在QuerySet上继续查询,即QuerySet支持链式查询。

4、大于和小于条件

继续延伸,既然QuerySet可以实现等于和不等于。那么大于和小于应当如何实现?

这种filter和exclude都无能为力。需要借助字段条件修饰,例如:

#查询id大于10的记录
qs1 = Blog.objects.filter(id__gt=10) 
#查询id大于等于10的记录
qs2 = Blog.objects.filter(id_gte=10) 
#排除id小于10的记录
qs3 = Blog.objects.exclude(id__lt=10) 
#排除id小于等于10的记录
qs4 = Blog.objects.exclude(id__lte=10)

从这里我们可以总结一下,objects查询基本就3种方法:get、filter、exclude。

条件再用字段修饰拓展,字段修饰是两个下划线加修饰码。

有个方法可以帮助记忆大于、小于的修饰码。

gt 是 greater than 的缩写,或者你熟悉html可以记忆为"&gt;","&gt;"是大于号的转义。

gte 后面的 e 是等于equal单词的第1个字母。

lt 和 lte 是同样方法,less than 的缩写;"&lt;"小于号的转义。

这里还有两个拓展。

1)范围查询

有时候,不单单是大于或小于。可能是一个范围,例如 1<=id<=9。你可以用两个条件组合。

qs = Blog.objects.filter(id__gte=1).filter(id__lte=9)

或者

qs = Blog.objects.filter(id__gte=1, id__lte=9)

可以使用__range范围修饰:

qs = Blog.objects.filter(id__range=[1, 9])

2)in条件查询

有时候不是简单大于或小于,需要从一个列表的值获取。in条件查询用__in修饰,例如:

#获取id为 1、3、6、7、9的记录qs = Blog.objects.filter(id__in=[1, 3, 6, 7, 9])

5、字符串模糊匹配

数值有大于小于等判断,那么字符串有模糊匹配判断的需求。例如开头是什么,结尾是什么,包含什么字符等等。这些同样需要借助字段条件修饰。例如:

#标题包含django,若忽略大小写使用__icontains
qs1 = Blog.objects.filter(caption__contains='django') 
#标题是django开头的,若忽略大小写使用__istartswith
qs2 = Blog.objects.filter(caption__startswith='django') 
#标题是django结尾的,若忽略大小写使用__iendswith
qs2 = Blog.objects.filter(caption__endswith='django')

那"不包含"、"开头不是"、"结尾不是"用什么?当然用exclude啦。

6、日期时间处理

上面Blog模型有个发表日期publish_time字段。该字段是datetime类型。

日期时间类型要不数值、字符串复杂得多。同样使用字段条件修饰实现相应查询:

    import datetime

     

    #获取今天的datetime

    now = datetime.datetime.now()

     

    #找今年发布的博客

    qs1 = Blog.objects.filter(publish_time__year=now.year)

     

    #找本月发布的博客

    qs2 = Blog.objects.filter(publish_time__year=now.year,publish_time__month=now.month)

     

    #找到每月1号发布的博客

    qs3 = Blog.objects.filter(publish_time__day=1)

我们还可以使用使用__range获取某范围内的数据:

    import datetime

    #获取前7天发表的博客
    now = datetime.datetime.now()
    end_date = datetime.datetime(now.year, now.month, now.day, 0, 0) 
    start_date = end_date - datetime.timedelta(7)

    #找今年发布的博客
    qs = Blog.objects.filter(publish_time__range=[start_date, end_date])

7、or条件

上面多个条件出现大多是and逻辑。那我可能使用or逻辑。例如我想模糊匹配标题为"django" 或 "python"。这时只能采用or逻辑处理。如下代码:

qs = Blog.objects.filter(caption__icontains='django')|Blog.objects.filter(caption__icontains='python')

若你嫌弃代码过程,可以使用Q函数:

from django.models import Q
qs = Blog.objects.filter(Q(caption__icontains='django')|Q(caption__icontains='python'))

8、limit获取前几条

有时页面显示只需要显示前10条或前20条记录。这个在SQL语句中可以用limit限制返回的记录数。同样,在QuerySet查询中也可以实现该功能。

qs = Blog.objects.all()[:10]

上面代码是获取全部博客的前10条记录,和切片一样,但索引不能为负数。

另外,这个切片器也不会让QuerySet直接执行,直到使用该查询。

9、外键查找

我上面写的示例模型,有两个外键。

假如我需要获取某一篇博客有什么分类标签,或分类标签下有哪些博客,应当如何获取?

获取某一篇博客有什么分类标签,由于Blog有个tags外键关联Tag模型。可直接获取:

blog = Blog.objects.all()[0]tags = blog.tags.all() #获取第1篇博客全部标签

而分类标签没有直接写一个外键和Blog模型关联,但可以用set通过tag获取全部blog:

tag = Tag.objects.all()[0]blogs = tag.blog_set.all() #获取第1个分类标签全部博客

django默认情况下每一个主表的对象都有一个是外键的属性,可以通过它查询到所有关于子表的信息,这个属性的名字就是子表的名称小写加上_set。

使用related_name属性定义名称(related_name是关联对象反向引用描述符)。

在model显示使用related_name就不能使用表名_set这种方式

10、排序

SQL语句可以使用Order by排序。QuerySet查询也有order_by与之对应:

#按照日期正序排序qs1 = Blog.objects.all().order_by('publish_time') 
#安装日期倒序排序(all可以省略)qs2 = Blog.objects.order_by('-publish_time')

针对哪个排序,就写该字段名称即可。默认正序排序,若加个负号表示倒序排序。

正序一般是数值由小到大,日期由远到近。

排序还有一个神奇的用法,在SQL我还没见过:随机排序。通过该功能,我们可以随机获取记录:

#随机获取前10条记录qs = Blog.objects.order_by('?')[:10]

11、其他

有些知识过于零碎,没有和上面放在一起讲。随便也罗列出来:

1)获取值为null的数据

qs = Blog.objects.filter(content__isnull=True)

2)去重

qs = Tag.objects.filter(id__in=[1, 3]).blog_set().distinct()

因为这里通过Tag获取的博客可能重复,可以用distinct去重。

3)反向排序

qs = Blog.objects.filter().reverse()

4) 返回第一条记录

qs = Blog.objects.filter().first()

5)返回最后一条记录

qs = Blog.objects.filter().last()

6)exists()

如果QuerySet包含数据,就返回True,否则返回False

7) 特殊的QuerySet

values()       返回一个可迭代的字典序列

values_list() 返回一个可迭代的元祖序列

其他查询方法还有

exact # 完全匹配
iexact # 不区分大小写的完全匹配
contains # 包含
icontains # 不区分大小写的包含
in # 被包含在给定的集合中,如 Post.objects.filter(id__in=[2, 3, 4])
gt # 大于,如 Post.objects.filter(id__gt=3)
gte # 大于或等于
lt # 小于
lte # 小于或等于
startswith # 以 xx 开头
istartswith # 不区分大小写的以 xx 开头
endswith
iendswith
range # 在范围内
date # 日期字段使用,如 Post.objects.filter(created__date=datetime.date(2020, 1, 1))
year, month, day...
isnull
regex # 匹配正则
iregex

12.鲜为人知的操作

在模型查询API不够用的情况下,我们还可以使用原始的SQL语句进行查询。

Django 提供两种方法使用原始SQL进行查询:一种是使用raw()方法,进行原始SQL查询并返回模型实例;另一种是完全避开模型层,直接执行自定义的SQL语句。

执行原生查询

raw()管理器方法用于原始的SQL查询,并返回模型的实例:

注意:raw()语法查询必须包含主键。

这个方法执行原始的SQL查询,并返回一个django.db.models.query.RawQuerySet 实例。 这个RawQuerySet 实例可以像一般的QuerySet那样,通过迭代来提供对象实例。

原生SQL还可以使用参数,注意不要自己使用字符串格式化拼接SQL语句,防止SQL注入!

直接执行自定义SQL

有时候raw()方法并不十分好用,很多情况下我们不需要将查询结果映射成模型,或者我们需要执行DELETE、 INSERT以及UPDATE操作。在这些情况下,我们可以直接访问数据库,完全避开模型层。

我们可以直接从django提供的接口中获取数据库连接,然后像使用pymysql模块一样操作数据库。

from django.db import connection, connections
cursor = connection.cursor()  # cursor = connections['default'].cursor()
cursor.execute("""SELECT * from auth_user where id = %s""", [1])
ret = cursor.fetchone()

QuerySet方法大全

##################################################################
# PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET #
##################################################################

def all(self)
    # 获取所有的数据对象

def filter(self, *args, **kwargs)
    # 条件查询
    # 条件可以是:参数,字典,Q

def exclude(self, *args, **kwargs)
    # 条件查询
    # 条件可以是:参数,字典,Q

def select_related(self, *fields)
    性能相关:表之间进行join连表操作,一次性获取关联的数据。

    总结:
    1. select_related主要针一对一和多对一关系进行优化。
    2. select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。

def prefetch_related(self, *lookups)
    性能相关:多表连表操作时速度会慢,使用其执行多次SQL查询在Python代码中实现连表操作。

    总结:
    1. 对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。
    2. prefetch_related()的优化方式是分别查询每个表,然后用Python处理他们之间的关系。

def annotate(self, *args, **kwargs)
    # 用于实现聚合group by查询

    from django.db.models import Count, Avg, Max, Min, Sum

    v = models.UserInfo.objects.values('u_id').annotate(uid=Count('u_id'))
    # SELECT u_id, COUNT(ui) AS `uid` FROM UserInfo GROUP BY u_id

    v = models.UserInfo.objects.values('u_id').annotate(uid=Count('u_id')).filter(uid__gt=1)
    # SELECT u_id, COUNT(ui_id) AS `uid` FROM UserInfo GROUP BY u_id having count(u_id) > 1

    v = models.UserInfo.objects.values('u_id').annotate(uid=Count('u_id',distinct=True)).filter(uid__gt=1)
    # SELECT u_id, COUNT( DISTINCT ui_id) AS `uid` FROM UserInfo GROUP BY u_id having count(u_id) > 1

def distinct(self, *field_names)
    # 用于distinct去重
    models.UserInfo.objects.values('nid').distinct()
    # select distinct nid from userinfo

    注:只有在PostgreSQL中才能使用distinct进行去重

def order_by(self, *field_names)
    # 用于排序
    models.UserInfo.objects.all().order_by('-id','age')

def extra(self, select=None, where=None, params=None, tables=None, order_by=None, select_params=None)
    # 构造额外的查询条件或者映射,如:子查询

    Entry.objects.extra(select={'new_id': "select col from sometable where othercol > %s"}, select_params=(1,))
    Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
    Entry.objects.extra(where=["foo='a' OR bar = 'a'", "baz = 'a'"])
    Entry.objects.extra(select={'new_id': "select id from tb where id > %s"}, select_params=(1,), order_by=['-nid'])

 def reverse(self):
    # 倒序
    models.UserInfo.objects.all().order_by('-nid').reverse()
    # 注:如果存在order_by,reverse则是倒序,如果多个排序则一一倒序


 def defer(self, *fields):
    models.UserInfo.objects.defer('username','id')
    或
    models.UserInfo.objects.filter(...).defer('username','id')
    #映射中排除某列数据

 def only(self, *fields):
    #仅取某个表中的数据
     models.UserInfo.objects.only('username','id')
     或
     models.UserInfo.objects.filter(...).only('username','id')

 def using(self, alias):
     指定使用的数据库,参数为别名(setting中的设置)


##################################################
# PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
##################################################

def raw(self, raw_query, params=None, translations=None, using=None):
    # 执行原生SQL
    models.UserInfo.objects.raw('select * from userinfo')

    # 如果SQL是其他表时,必须将名字设置为当前UserInfo对象的主键列名
    models.UserInfo.objects.raw('select id as nid from 其他表')

    # 为原生SQL设置参数
    models.UserInfo.objects.raw('select id as nid from userinfo where nid>%s', params=[12,])

    # 将获取的到列名转换为指定列名
    name_map = {'first': 'first_name', 'last': 'last_name', 'bd': 'birth_date', 'pk': 'id'}
    Person.objects.raw('SELECT * FROM some_other_table', translations=name_map)

    # 指定数据库
    models.UserInfo.objects.raw('select * from userinfo', using="default")

    ################### 原生SQL ###################
    from django.db import connection, connections
    cursor = connection.cursor()  # cursor = connections['default'].cursor()
    cursor.execute("""SELECT * from auth_user where id = %s""", [1])
    row = cursor.fetchone() # fetchall()/fetchmany(..)


def values(self, *fields):
    # 获取每行数据为字典格式

def values_list(self, *fields, **kwargs):
    # 获取每行数据为元祖

def dates(self, field_name, kind, order='ASC'):
    # 根据时间进行某一部分进行去重查找并截取指定内容
    # kind只能是:"year"(年), "month"(年-月), "day"(年-月-日)
    # order只能是:"ASC"  "DESC"
    # 并获取转换后的时间
        - year : 年-01-01
        - month: 年-月-01
        - day  : 年-月-日

    models.DatePlus.objects.dates('ctime','day','DESC')

def datetimes(self, field_name, kind, order='ASC', tzinfo=None):
    # 根据时间进行某一部分进行去重查找并截取指定内容,将时间转换为指定时区时间
    # kind只能是 "year", "month", "day", "hour", "minute", "second"
    # order只能是:"ASC"  "DESC"
    # tzinfo时区对象
    models.DDD.objects.datetimes('ctime','hour',tzinfo=pytz.UTC)
    models.DDD.objects.datetimes('ctime','hour',tzinfo=pytz.timezone('Asia/Shanghai'))

    """
    pip3 install pytz
    import pytz
    pytz.all_timezones
    pytz.timezone(‘Asia/Shanghai’)
    """

def none(self):
    # 空QuerySet对象


####################################
# METHODS THAT DO DATABASE QUERIES #
####################################

def aggregate(self, *args, **kwargs):
   # 聚合函数,获取字典类型聚合结果
   from django.db.models import Count, Avg, Max, Min, Sum
   result = models.UserInfo.objects.aggregate(k=Count('u_id', distinct=True), n=Count('nid'))
   ===> {'k': 3, 'n': 4}

def count(self):
   # 获取个数

def get(self, *args, **kwargs):
   # 获取单个对象

def create(self, **kwargs):
   # 创建对象

def bulk_create(self, objs, batch_size=None):
    # 批量插入
    # batch_size表示一次插入的个数
    objs = [
        models.DDD(name='r11'),
        models.DDD(name='r22')
    ]
    models.DDD.objects.bulk_create(objs, 10)

def get_or_create(self, defaults=None, **kwargs):
    # 如果存在,则获取,否则,创建
    # defaults 指定创建时,其他字段的值
    obj, created = models.UserInfo.objects.get_or_create(username='root1', defaults={'email': '1111111','u_id': 2, 't_id': 2})

def update_or_create(self, defaults=None, **kwargs):
    # 如果存在,则更新,否则,创建
    # defaults 指定创建时或更新时的其他字段
    obj, created = models.UserInfo.objects.update_or_create(username='root1', defaults={'email': '1111111','u_id': 2, 't_id': 1})

def first(self):
   # 获取第一个

def last(self):
   # 获取最后一个

def in_bulk(self, id_list=None):
   # 根据主键ID进行查找
   id_list = [11,21,31]
   models.DDD.objects.in_bulk(id_list)

def delete(self):
   # 删除

def update(self, **kwargs):
    # 更新

def exists(self):
   # 是否有结果

QuerySet方法大全

Django终端打印SQL语句

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True, # 开启日志打印
            'level':'DEBUG',
        },
    }
}

部分内容转载自:http://yshblog.com/blog/157

部分内容转载自:https://www.cnblogs.com/liwenzhou/p/8660826.html


一名测试工作者,专注接口测试、自动化测试、性能测试、Python技术。