Django 09 Django进阶-ORM模型-查询方法详解

作者 柚爸

在设计好表和字段之后,下一步最关键的就是要知道如何操作表.其中的核心就是查询,然后是对象(表的数据行)的各种方法.
Django 1.11 官方文档对应这两部分的内容是 QuerySet APIMaking Queries

查询

查询是通过ORM设置好的API,也就是各种方法和属性实现的.查询分为查询方法和查询条件.

查询方法 Methods that return new QuerySets

查询方法返回的都是QuerySet对象(包含一组对象的列表)或者具体的对象(表示一行数据),来看一下基本的查询方法:

ORM一般查询
方法 解释
all() 查询所有结果.objects后边的.all()可以省略,省略的时候就是对所有结果操作.
get(**kwargs): 返回与所给筛选条件相匹配的对象,返回结果有且只有一个,如果符合筛选条件的对象超过一个或者没查找到都会抛出错误。返回具体对象,不是QuerySet
filter(**kwargs): 返回与所给筛选条件相匹配的结果集(QuerySet对象,放有所有查询结果的列表),即使结果只有一个,也是返回一个结果集,需要从中取出每条数据处理.如果没有结果,返回空集.以后多用filter,基本不用get
exclude(**kwargs) 不符合筛选条件的结果.返回QuerySet
values(*field) 返回一个QuerySet,,每一个元素是代表一行数据的一个字典,里边用键=字段名 值= 数据 的方式存放所有查询到的结果.可以加参数指定具体字段,多个字段名用逗号分隔,不指定参数则取出所有字段.只能作用在QuerySet对象上,不能作用于单独的对象.
values_list(*field) 和values类似,只不过QuerySet的每一个元素是一个元组,按参数顺序放着字段的值.只能作用在QuerySet对象上,不能作用于单独的对象.
order_by(*field) 按照指定的字段排序.在字符串开头写-表示倒序如”-price”.不使用order_by的话,默认会按照META里设置的顺序对结果排序.返回QuerySet
reverse() reverse只能在指定顺序的QuerySet上调用,比如用过了order_by方法或者META里写了排序方法.得到反向排序的顺序.返回QuerySet
distinct() 从返回结果中剔除重复纪录.跨表查询的时候经常用此方法去重.
count() 返回数据库中匹配查询结果集(QuerySet)中的对象数量.是一个Int值.
first() 返回QuerySet中第一个对象.返回的是具体对象,不是QuerySet
last() 返回QuerySet中最后一个对象.返回的是具体对象,不是QuerySet
exists() 返回一个布尔值.如果QuerySet包含数据,就返回True,否则返回False.用于快速判断是否查询到结果.

查询条件 Field lookups

基本查询语句的参数是**kwargs的都是各种查询条件,上边例子里是用的=来表示条件,这是精确匹配,还可以采用其他条件.这部分对应的都是MySQL的WHERE语句后边的内容.在Django ROM里叫做 Field lookups.这些Field lookups 关键字都用双下划线加在字段名后边.有多个条件的,将条件用逗号隔开放在上边的基本查询语句中,比如models.Person.objects.filter(id__gt=2,id__lt=5).

Field lookups 查询条件
条件 解释
= 表示精确匹配,只选择符合条件的对象
iexact 大小写不敏感的精确匹配,常用来匹配一段字符串
contains 包含有某段字符串内容的匹配,大小写敏感
icontains 大小写不敏感的icontains
in 在一个列表内.和SQL语句的in一样
gt 大于
gte 大于等于
lt 小于
lte 小于等于
startswith 以字符串开始,大小写敏感
istartswith 大小写不敏感版本的startswith
endswith 以字符串结尾,大小写敏感
iendswith 大小写不敏感版本的endswith
range 是否在一个范围内,包含范围的两端,=号后边是一个两个元素的元组,表示开始和结束
date 用在datetime类别的字段上,单独取出日期(datetime对象)来比较,还可以后续接其他Field lookups,详细看这里
year 和date一样,只不过单独取出来年份比较.是一个INT类型
month 和date一样,只不过单独取出来月份进行比较,是一个INT类型
day 和date一样,只不过单独取出来日进行比较,是一个INT类型.year,month和day经常用在快速过滤日期,比使用datetime对象比较要简单很多.
week 和date一样,只不过单独取出来第几周进行比较,是一个INT类型.
week_day 和date一样,只不过单独取出来星期几日进行比较,是一个INT类型.星期天是1,星期六是7.比较常用.
time 和date一样,只不过单独取出来时间进行比较,是一个 datetime.time对象.
hour 从datetime和time fileds里取小时,范围是0-23,是一个INT类型.
minute 同hour,取分钟,范围0-59,是一个INT类型
second 同hour,取秒,范围0-59,是一个INT类型
isnull 判断字段是否是Null
regex 大小写敏感的正则匹配模式,等号后边需要是一个正则表达式
iregex 大小写不敏感的正则表达式

跨表查询

跨表查询指的是依靠关系进行查询的方法.其核心就是依据外键以及一对一,多对多字段进行的查找.先来看经典的外键一对多查询:

ForeignKey 一对多查询

先来了解一下正向查询和反向查询
正向查询:外键所在的表去查外键关联的表.
逆向查询:一个表去查另外一个外键关联到当前这个表的表.
例子:

class Publisher(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32, unique=True)

    def __str__(self):
        return "我是一个出版社对象:{}".format(self.name)


class Book(models.Model):
    id = models.AutoField(primary_key=True)
    price = models.DecimalField(max_digits=5, decimal_places=2, default=99.99)
    # 库存数
    kucun = models.IntegerField(default=1000)
    # 卖出数
    maichu = models.IntegerField(default=0)

    title = models.CharField(max_length=32)
    # 外键
    # related_name="books" 反向查询是用来代替 book_set的
    publisher = models.ForeignKey(
        to="Publisher",
        on_delete=models.CASCADE)

    def __str__(self):
        return self.title

从Book表去查Publisher查叫做正向查询,从Publisher表去查Book叫做反向查询.

正向查询,在当前表内查到结果之后,直接调用外键对应的属性,就可以得到指向的外键对象.由于这里是一对多的,正向查询回去的结果就是对应的对象,不是QuerySet.例子:

book_obj = models.Book.objects.all().first()
ret = book_obj.publisher  # 和我这本书关联的出版社对象
print(ret, type(ret))

结果直接就是书对应的出版社对象.如果想查对应的出版社的名称,由于查到了对象,可以再取名称,但有简单的方法来查外键对应的具体字段:

ret = models.Book.objects.filter(id=1).values_list("publisher__name")
print(ret)

双下划线就表示跨了表,前边的publisher是表名,后边是表示字段名.由于是拿到具体值,所以只能在values和values_list里应用该方法.

反向查询,如果要查一个出版社所有的书,有个特殊的方法是表名+_set来反着找”一对多”里的”多”,来看例子:

publisher_obj = models.Publisher.objects.first()  # 找到第一个出版社对象
books = publisher_obj.book_set.all()  # 找到第一个出版社出版的所有书
titles = books.values_list("title")

这里的book是表名,后边加上_set就表示来反着找,结果是一个QuerySet,可以把book表里由第一个出版社出版的所有书都找到.
如果在book表的外键里给外键指定了related_name,那么此时就不要用表名,直接用related_name的名字就可以了.比如指定了related_name为all_books.上边例子的第二句话就可以改写成:

books = publisher_obj.allbook.all()

注意,如果起了外键别名,再使用原表名_set会报错.实际上别名代替了原来对象上的表名_set方法.
同样,也可以直接查字段,也是用双下划线来连接表名和字段.

ret = models.Publisher.objects.filter(id=1).values_list("book__title")
print(ret)

book是表名,title是字段名.注意,这里如果外键指定了related_name的值而没有指定related_query_name的值,则需要将book换成其related_name的值,如果同时指定了related_name和related_query_name的值,则需要换成related_query_name的值,如果只指定了related_query_name的值,也需要换成其值.
结果得到的是元素为元组的列表,每个元组内就是查询字段的结果.

ManyToManyField 多对多查询

在ORM中,多对多以及外键的反向查询(结果集都是取到多个对象的时候),内部都是通过一个叫做 RelatedManager类的实例,即关联上下文管理器来操作的.
比如在外键的例子中,如果打印publisher_obj.allbook,可以看到结果是一个 RelatedManager类.针对这个类,像.all()就是取全部结果,除此之外,针对这个类还有一些特殊的操作:

create() 用于从创建一对多关系

models.Author.objects.first().book_set.create(title="番茄物语", publish_date=datetime.date.today())

通过Author.objects.first()取到一个具体的作者对象,然后.book_set取到上下文管理器对象,然后对这个对象进行.create方法,结果是先在book表内建立一本书,然后在多对多的表内建立这本书与这个作者的关系.

add() 操作一个已经存在的对象,将对象添加到对应关系里

new_book = models.book.objects.create(name= '新的书').save()  # 在book表内建立一本新书
models.Author.objects.first().book_set.add(new_book)  # 在多对多表内添加一行,记录第一个作者和这本新书的对应关系.

add的参数可以是对象,可以是解开的QeurySet对象(*ret),也可以是一个数字表示id或者拆解开的列表id,如add(*[3,12,19]).

set() 一次性在多对多表里设置一组对应关系
在之前的项目中已经使用过set了,其结果就是将对应的一组id或者对象代表的内容,在多对多表里去当前对象联系起来/

book_obj = models.Book.objects.first()
book_obj.authors.set([2, 3])

这个例子就是将第一本书与id为2,3的作者在多对多表里联系起来.

remove() 移除参数指定的对象与当前对象的对应关系

book_obj = models.Book.objects.first()
book_obj.authors.remove(3)

remove接受的参数和add一样.

clear() 表示清除当前对象在多对多表里对应某个表的全部对应关系.

book_obj = models.Book.objects.first()
book_obj.authors.clear()

这个操作的意思表示移除第一本书和所有作者的关联关系.也就是在多对多表里删除所有book_id = 1的数据行.

由于上下文管理器在外键的反向查询的过程中也存在,因此也是可以用上边五种方法来操作的,只不过在外键反向查询中,不存在多对多的表格.所以clear()和remove()操作实际上是将外键那一列修改成Null,因此必须要求外键设置成Null=True,否则会报错.

一对一查询

一对一的关系字段是models.OneToOneField(to=).实际上正查反查都是只能出来一条字段.不再赘述.其实用id也可以取得相同的数据.
这里有一篇文章讲了Django中关系的总结.

使用聚合函数与分组查询

在ORM中,一样可以使用便利的聚合函数以及非常常见的分组查询.

聚合函数

聚合函数的方法是aggregate(),在使用这个方法之后,查询语句就结束了,这个方法返回一个字典,里边包含着使用聚合函数的结果.
在使用聚合函数之前,必须导入对应的聚合函数模块:

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

这些函数的参数都是字段名称.对于生成的结果字典,如果没有指定名称,则Django会自动设置一个名称.如果指定了名称,就会使用指定的名称,然后再从字典里取数即可:

models.Book.objects.all().aggregate(Avg("price"))
# 结果是{'price__avg': 13.233333},其中的"price__avg"由Django自动生成

models.Book.objects.aggregate(average_price=Avg('price'))
# 结果是{'average_price': 13.233333}, 其中的"average_price"是自定义的名称.

models.Book.objects.all().aggregate(Avg("price"), Max("price"), Min("price"))
# 同时使用多个聚合函数,可以用逗号分割,结果字典中会包括全部结果.

分组

分组的方法是.annotate().这里可能有点抽象,其实annotate的作用就是先按照之前的查找方式返回数据,再将返回的多行数据按照某个指定的字段进行分组操作,然后将分组过的每一行结果都包在一个QuerySet中返回.其中.annotate之前的部分如果是通过外键查询,其本身就相当于连表操作,再分组之后就是分组后的QuerySet了.
比如在自己编写的行政物品管理系统内,需要列出每一种物品的结余,即通过物品列表查到每一种物品对应的所有出入库记录,再将出库和入库记录求和分别求和并相减.
用SQL语句是先通过move的item_id连表,然后按照item_id分组并对in_num和out_num字段进行求和再相减.
在ORM里,通过物品列表的这一段写在前边,到外键所在的表去查询,则将外键所在的表及字段名称写在annotate内部的聚合函数内,即像这样:

ret = models.Items.objects.all().annotate(num_balance=Sum("moves__in_num") - Sum("moves__out_num"))

前边的ret = models.Items.objects.all()表示按照所有的物品分组.由于是正向查询即通过物品查余额,所以moves__in_nummoves__out_num其中采取表名__字段名的写法..annotate就表示按照之前的对象分组并且按照参数中的某个字段进行处理,这里是分组以后对分组的in_num求和再减去out_num的求和,就得到了按照物品分组的余额.这里是反向操作,直接使用小写的类名即可,不用加__set,如果是正向,用外键也可以.(当然一般分组不会用正向查询)
ret是一个QuerySet,其中的每一个元素就是一个对象,表示一行经过分组过后的数据,这一行里的属性包括Items表里的全部属性,以及自定义的num_balance属性.

这里直接就使用了跨表查询,如果想在同一个表内,按照某一个字段进行分组,像上边例子里的models.Items.objects.all()的部分,就需要改成当前表里字段的用values的查询结果,也就是告诉ORM按照哪个字段来分组.
在自己编写的进销存系统中,查询moves表按照item_id分类的in_sum字段的和:

ret = models.Moves.objects.values("item_id").annotate(insum = Sum("in_num"))

这个就是先用了item_id过滤出内容,结果的QuerySet里只包括item_id字段的值,然后在对其进行分组并对对应的in_num字段求和,求和的结果放到每一个结果里的insum字段名中.
ret实际上就是一个分组并且计算过的表的每一行内容的QuerySet,再从里边取出每一行进行操作和展示即可.

分组的操作不是太直观,用这里分组的例子举一个例子:
在一张表内进行分组,用SQL语句比较直观:

select dept,AVG(salary) from employee group by dept;

如果换成ORM,首先明确是要查那张表.这个查询只在一个表内查询,查询的结果包括dept和AVG(salary),group by 后边是dept.
这说明按照dept先分组,然后对于聚合后的结果,用avg对salary求平均数,最后返回分组后的部门和结果.

SQL语句与ORM的对应关系:

第一步:基础对应关系:
models.Employee 表示从那张表进行查询,即from.
objects是中间件的名称,不用管
.all表示SELECT *
如果是要取具体的字段,就用values(“字段名”),比如values(“dept”)就相当于 SELECT dept.
可见,如果想要达到select dept,AVG(salary) from employee的效果,我的ORM语句对应的部分是:

models.Employee.objects.values("dept,"平均数的别名")

现在平均数的别名还不知道,继续往下看分组:

第二步:分组与聚合处理

models.Employee.objects.values("dept,"平均数的别名").annotate()

这里的annotate()如果不传参数,表示按照.all()全部分组,一般不这样使用..annotate之前的value表示GROUP BY之后的内容,所以values("dept").annotate(my_avg=Avg("salary"))就表示按照dept进行分组,然后对salary进行求和,得到分组的结果.

第三步:取分组后结果
由于分组结果算出来了,是一个QuerySet,按照我们的需求,最前边是SELECT dept, Avg(salary),现在Avg(salary)的别名有了,所以要在最后再加上.values(“dept”,”my_avg”).
最终结果和原来SQL语句用颜色标识的对应关系是:
models.Employee.objects.values("dept").annotate(my_avg=Avg("salary").values(dept, "my_avg")
select dept,AVG(salary) from employee group by dept;
ORM语句橙色的部分其实也涵盖了SQL语句中的Avg(salary),这是因为ORM里使用了别名.实际对应关系还是可以很清楚的看出来.

第二个例子是连表查询,dept_id外键关联到dept表.
如果想得到和上边例子一样的结果,在SQL中就是先连表再查询,到了ORM里,其实就是通过外键进行的双下划线查询去跨表:
select dept.name,AVG(salary) from employee inner join dept on (employee.dept_id=dept.id) group by dept_id;

models.Dept.objects.annotate(avg=Avg("employee__salary")).values("name", "avg")

ORM里从Dept表发起查询并且分组,由于dept_id和dept.name是一一对应的,所以从Dept发起查询后直接接.annotate就可以.后边的对应关系中从dept跨表到employee就相当于连表,然后对salary进行Avg操作,其他的对应关系和上边类似.

F查询和Q查询

F和Q其实就是为了解决以面向对象的方式书写SQL操作的时候,对于语句中的and or 等逻辑控制符不方便书写的问题.

F查询

在之前的查询条件中,使用的都是诸如.filter(id__gt=3)这种将字段值与常量进行比较的条件,如果需要对两个字段做比较,显然在Python中不能写成.filter(id__gt=item_id),因为item_id并没有在python程序中定义.这个时候就要使用F查询来替代对应的字段,,只要给F的参数传入字段名称即可.
比如在自行编写的行政物品查询系统中,如果要查询所有入库 大于 出库的单据,就写成:

ret = ret_temp = models.Moves.objects.filter(in_num__gte=F("out_num"))

在一张表用自身数据做控制的时候,就要用到F查询,经常在各种内容管理系统中使用,比如列出点赞数高于踩数的条目等等.
除了进行比较之外,F查询在根据自己的字段值更新值的时候比较常用,比如要在出入库表上的出库数字增加10:

models.Moves.objects.update(in_num = F("in_num") + 10)

这里要注意的就是update方法,仅可以使用在QuerySet上,单个对象(行数据)没有该方法.该方法返回的是受影响的行数.

Q查询

Q查询是为了解决条件查询的时候方便使用or 等逻辑符的问题,将条件独立出来,然后用逻辑运算符连结来得到最终条件.
默认情况下,filter等条件查询内部可以使用多个条件语句,用逗号分割,这些条件之间是and的关系,比如查询每一张出入库单,要求出库大于入库而且入库大于50,就可以写成:

ret_temp = models.Moves.objects.filter(in_num__gte=F("out_num"), in_num__gt=50)

其中的in_num__gte=F("out_num")in_num__gt=50是and关系.如果要改成or关系,就要使用Q对象将条件独立出来,然后用&(表示and)和|(表示or)还有~(表示取反)来连结各个Q对象.
如果要查询入库小于出库或者出库大于20的单据:

from django.db.models import F,Q
ret_temp = models.Moves.objects.filter(Q(in_num__lte=F("out_num")) | Q(out_num__gt=20))

至此,已经学习完了DJango 1.11 QuerySet API reference 里的Methods that return new QuerySets,也就是返回查询结果的部分.还有一部分API没有接触到,列在下边:

返回QuerySet的方法补充
方法名 解释
dates(field, kind, order=’ASC’) 针对DateField和DateTimeField类型的字段,取出其中和日期相关的部分,去重,然后排序.返回的是一个QuerySet对象,其中的每一个元素是datetime.date()对象.kind表示精确度,只能取”year”,”month”和”day”三个字符串值.order的ASC表示正向排序,DESC表示逆向排序
datetimes(field_name, kind, order=’ASC’, tzinfo=None) 功能与dates类似.区别是针对DateTimeField类型的字段, kind 只能是 “year”, “month”, “day”, “hour”, “minute” 或 “second”.tiinfo的参数必须是一个 datetime.tzinfo 对象,如果设置为None,Django会使用当前时区.
none() 不管前边查询条件如何,带上这个方法就一定返回一个空的QuerySet对象
union(*other_qs, all=False) 左右连两个查询结果,使用方法是 queryset1.union(queryset2).如果all=True,则允许重复项.
intersection(*other_qs) 获取多个QuerySet中相同的查询结果.使用方法是qs1.intersection(qs2, qs3)
difference(*other_qs) 获取只在调用方法的QuerySet中存在,不在参数的QuerySet中存在的结果,使用方法是qs1.difference(qs2, qs3)
select_related(*fields) 相当于通过外键进行连表查询.使用方法e = Entry.objects.select_related(‘blog’).get(id=5),表示选中id为5的Entry记录通过外键关联的blog里的对象.
prefetch_related(*lookups) 这个与select类似,主要是性能相关,会预先通过外键进行查找,再次使用查询的时候会到临时表里查询.详细看这里
extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None) 当ORM有时候难以描述一个复杂的WHERE查询的时候,用extra构造额外的查询条件,比如子查询.详细看这里
defer(*fields) 参数为字段名,用于在结果集中排出某些列,排出之后,列就没有了对应的字段名的属性
only(*fields) 仅取指定的某些字段的信息.
using(alias) 如果Django中设置了超过一个数据库,在查询的时候用此方法指定使用哪个数据库,alias是数据库的别名,不加则表示使用”default”数据库.数据库的别名是指的settings.py里DATABASES参数的键名.
select_for_update(nowait=False, skip_locked=False) 表示所有查询结果在完成事务之前都不能被改动,具体看这里
raw(raw_query, params=None, translations=None) 执行原生的SQL语句,返回一个 django.db.models.query.RawQuerySet对象,可以像QuerySet一样遍历取出各行数据.

上边的所有方法都是用来查询,也就是返回QuerySet对象,或者是QuerySet封装的字典或者其他对象(比如values, values_list, date等方法),相当于在MySQL中查找的结果.对于查询以外的方法,主要是涉及到增删改查,将在下一部分学习.