查询操作

阅读: 134331     评论:21

查询操作是Django的ORM框架中最重要的内容之一。我们建立模型、保存数据为的就是在需要的时候可以查询得到数据。Django自动为所有的模型提供了一套完善、方便、高效的API,一些重要的,我们要背下来,一些不常用的,要有印象,使用的时候可以快速查找参考手册。


本节的内容基于如下的一个博客应用模型:

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __str__(self):
        return self.name

class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    number_of_comments = models.IntegerField()
    number_of_pingbacks = models.IntegerField()
    rating = models.IntegerField()

    def __str__(self):
        return self.headline

一、创建对象

假设模型位于mysite/blog/models.py文件中。

创建一个模型实例,也就是一条数据库记录,最一般的方式是使用模型类的实例化构造方法:

>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

在后台,这会执行一条SQL的INSERT语句。如果你不显式地调用save()方法,Django不会立刻将该操作反映到数据库中。save()方法没有返回值,并且可以接受一些额外的参数。

提醒:在代码中或者shell中使用API的方式直接创建模型实例是不会引发数据验证的。对于Blog模型,name和tagline都是必须填写的字段。但是你完全可以执行b = Blog(name='liujiangblog'),不提供tagline的值,然后save到数据库。而在表单中,比如admin后台,这是不行的,因为Django的表单默认要进行数据完整性、合法性验证。

如果想要一行代码完成上面的操作,请使用creat()方法,它可以省略save的步骤,比如:

b = Blog.objects.create(name='刘江的博客', tagline='主页位于liujiangblog.com.')

有很多人喜欢在模型的初始化上做文章,实现一些额外的业务需求,常见的做法是自定义__init__方法。这不好,容易打断Django源码的调用链,存在漏洞。更推荐的是下面两种方法。

第一种方法:为模型增加create类方法,在其中夹塞你的代码:

from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=100)

    @classmethod
    def create(cls, title):
        book = cls(title=title)
        # 将你的个人代码放在这里
        print('测试一下是否工作正常')
        return book

book = Book.create("liujiangblog.com")   # 注意,改为使用created方法创建Book对象
book.save()           # 只有调用save后才能保存到数据库

第二种方法:自定义管理器,并在其中添加创建对象的方法,推荐!

class BookManager(models.Manager):   # 继承默认的管理器
    def create_book(self, title):
        book = self.create(title=title)
        # 将你的个人代码放在这里
        print('测试一下是否工作正常')
        return book

class Book(models.Model):
    title = models.CharField(max_length=100)

    objects = BookManager()   # 赋值objects

book = Book.objects.create_book("liujiangblog.com")   #改为使用create_book方法创建对象

二、修改对象并保存

Model.save(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None)

使用save()方法,保存对数据库内已有对象的修改。例如如果已经存在b5对象在数据库内:

>>> b5.name = 'New name'
>>> b5.save()

在后台,这会运行一条SQL的UPDATE语句。如果你不显式地调用save()方法,Django不会立刻将该操作反映到数据库中。

我们往往会重写save方法,添加自己的业务逻辑,然后在其中调用原来的save方法,保证Django基本工作机制正常。比如下面的例子:

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def save(self, *args, **kwargs):
        do_something()   # 保存前做点私活
        super().save(*args, **kwargs)  # 一定不要忘记这行代码
        do_something_else()  # 保存后又加塞点东西

这样,任何一篇博客在保存前后都会执行一些额外的代码。

注意,千万不要忘记super().save(*args, **kwargs),这行确保了Django源码中关于save方法的代码会依然被执行。我们是夹带私货,不是不干活。

下面的例子则是对博客名字做限制,只有刘江的博客才可以保存:

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def save(self, *args, **kwargs):
        if self.name != "刘江的博客":
            return # 只有刘江的博客才可以保存
        else:
            super().save(*args, **kwargs)  # 调用真正的save方法

*args, **kwargs的参数设计,确保我们自定义的save方法是个万金油,不论Django源码中的save方法的参数怎么变,我们自己的save方法不会因为参数定义的不正确而出现bug。

此外,模型实例的保存过程有一些值得探究的细节。

执行save方法后,Django才会真正为对象设置自增主键的值:

>>> b2 = Blog(name='Cheddar Talk', tagline='Thoughts on cheese.')
>>> b2.id     # 返回None,因为此时b2还没有写入数据库,没有id值
>>> b2.save()
>>> b2.id     # 这回有了

当然,你也可以自己指定主键的值,不需要等待数据库为主键分配值,如下所示:

>>> b3 = Blog(id=3, name='Cheddar Talk', tagline='Thoughts on cheese.')
>>> b3.id     # 返回 3.
>>> b3.save()
>>> b3.id     # 返回 3.

很显然,你必须确保分配的主键值是没有被使用过的,否则肯定出问题,因为在这种情况下,Django认为你是在更新一条已有的数据对象,而不是新建对象,比如下面的操作:

b4 = Blog(id=3, name='Not Cheddar', tagline='Anything but cheese.')
b4.save() 
# 实际上是更新了上面的b3,而不是新建,此时b4==b3

上面现象的本质是Django对SQL的INSERT和UPDATE语句进行了抽象合并,共用一个save方法。

有些罕见情况下,可能你必须强制进行INSERT或者UPDATE操作,而不是让Django自动决定。这时候可以使用save方法的force_insertforce_update参数,将其中之一设置为True,强制指定保存模式。

另外,有一种常见的需求是根据现有字段的值,更新成为新的值,比如点赞数+1的操作,通常我们可能写成如下的代码:

>>> entry = Entry.objects.get(name='刘江的博客')
>>> entry.number_of_pingbacks += 1
>>> entry.save()

看起来没有什么问题,但实际上这里有个漏洞。首先会访问一次数据库,将number_of_pingbacks的值取出来,然后在Python的内存中进行加一操作,最后将新的值写回到数据库。两次读写倒还算好,关键是可能存在数据冲突,比如在同一时间有很多人点赞,肯定会出现错误。那如何解决这个问题呢?最简单的方法是使用Django的F表达式

>>> from django.db.models import F

>>> entry = Entry.objects.get(name='刘江的博客')
>>> entry.number_of_pingbacks = F('number_of_pingbacks') + 1
>>> entry.save()

为什么F表达式就可以避免上面的问题呢?因为Django设计的这个F表达式在获取关联字段值的时候不用先去数据库中取值然后在Python内存里计算,而是直接在数据库中取值和计算,直接更新数据库,不需要在Python中操作,自然就不存在数据竞争和冲突问题了。

save方法的最后一个参数是update_fields,它用于指定你要对模型的哪些字段进行更新,这对于性能可能有细微地提升,比如:

product.name = 'Name changed again'
product.save(update_fields=['name'])

一些update_fields参数的说明:

  • 接收任何的可迭代对象,每个元素都是字符串
  • 参数值为空的迭代对象时,相当于跳过save方法
  • 参数值为None时,默认更新所有字段
  • 将强制为UPDATE方式

那么当你调用save()方法的时候,Django内部是怎么的执行顺序呢?

  1. 触发pre-save信号,让任何监听此信号者执行动作
  2. 预处理数据。触发每个字段的pre-save()方法,用于实施自动地数据修改动作,比如时间字段处理auto_now_add或者auto_now参数。
  3. 准备数据库数据。 要求每个字段提供的当前值是能够写入到数据库中的类型。类似整数、字符串等大多数类型不需要处理,只有一些复杂的类型需要做转换,比如时间。
  4. 将数据插入到数据库内。
  5. 触发post_save信号。

注意:对于批量创建和批量更新操作,save()方法不会调用,甚至连pre_delete或者post_delete信号都不会触发,此时自定义的代码都是无效的。

保存外键和多对多字段:

保存一个外键字段和保存普通字段没什么区别,只是要注意值的类型要正确。下面的例子,有一个Entry的实例entry和一个Blog的实例cheese_blog,然后把cheese_blog作为值赋给了entry的blog属性,最后调用save方法进行保存。

>>> from blog.models import Blog, Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

多对多字段的保存稍微有点区别,需要调用一个add()方法,而不是直接给属性赋值,但它不需要调用save方法。如下例所示:

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

在一行语句内,可以同时添加多个对象到多对多的字段,如下所示:

>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)

如果你指定或添加了错误类型的对象,Django会抛出异常。

三、检索对象

想要从数据库内检索对象,你需要基于模型类,通过管理器(Manager)操作数据库并返回一个查询结果集(QuerySet)。

每个QuerySet代表一些数据库对象的集合。它可以包含零个、一个或多个过滤器(filters)。Filters缩小查询结果的范围。在SQL语法中,一个QuerySet相当于一个SELECT语句,而filter则相当于WHERE或者LIMIT一类的子句。

每个模型至少具有一个Manager,默认情况下,Django自动为我们提供了一个,也是最常用最重要的一个,99%的情况下我们都只使用它。它被称作objects,可以通过模型类直接调用它,但不能通过模型类的实例调用它,以此实现“表级别”操作和“记录级别”操作的强制分离。

默认的objects管理器带有一系列方法和属性,能够满足绝大多数场景的业务需求。

>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
...
AttributeError: "Manager isn't accessible via Blog instances."

在构造自己的检索语句之前,请记住:希望结果是哪个模型的实例,就用哪个模型去调用!。比如,我想获得满足某些条件的Blog,那么就是Blog.objects.xxx,如果想获得某些Entry,那么就是Entry.objects.xxx

1. 检索所有对象

使用all()方法,可以获取某张表的所有记录。

>>> all_entries = Entry.objects.all()

2. 过滤对象

有两种方法可以用来过滤QuerySet的结果,分别是:

  • filter(**kwargs):返回一个根据指定参数查询出来的QuerySet
  • exclude(**kwargs):返回除了根据指定参数查询出来结果的QuerySet

其中,**kwargs参数的格式必须是Django设置的一些字段格式。

例如要获取2020年的所有文章:

Entry.objects.filter(pub_date__year=2020)

它等同于:

Entry.objects.all().filter(pub_date__year=2020)

链式过滤

filter和exclude的结果依然是个QuerySet,因此它可以继续被filter和exclude,这就形成了链式过滤:

>>> Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.date.today()
... ).filter(
...     pub_date__gte=datetime(2020, 1, 30)
... )

最终的 QuerySet 包含标题以 "What" 开头的,发布日期介于 2020 年 1 月 30 日与今天之间的所有条目。

(需要注意的是,当在进行跨关系的链式过滤时,结果可能和你想象的不一样,参考下面的跨多值关系查询)

被过滤的QuerySets都是唯一的

每一次过滤,你都会获得一个全新的QuerySet,它和之前的QuerySet没有任何关系,可以完全独立的被保存,使用和重用。例如:

>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())

这三个 QuerySets 是独立的。第一个是基础 QuerySet,包含了所有标题以 "What" 开头的文章。第二个是第一个的子集,带有额外条件,排除了 pub_date 是今天和今天之后的所有记录。第三个是第一个的子集,带有额外条件,只筛选 pub_date 是今天或未来的所有记录。最初的 QuerySet (q1) 不受筛选操作影响。

QuerySets都是懒惰的

一个创建QuerySets的动作不会立刻导致任何的数据库行为。你可以不断地进行filter动作一整天,Django不会运行任何实际的数据库查询动作,直到QuerySets被提交(evaluated)。

简而言之就是,只有碰到某些特定的操作,Django才会将所有的操作体现到数据库内,否则它们只是保存在内存和Django的层面中。这是一种提高数据库查询效率,减少操作次数的优化设计。看下面的例子:

>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)

上面的例子,看起来执行了3次数据库访问,实际上只是在print语句时才执行1次访问。通常情况,QuerySets的检索不会立刻执行实际的数据库查询操作,直到出现类似print的请求,也就是所谓的evaluated。

那么如何判断哪种操作会触发真正的数据库操作呢?简单的逻辑思维如下:

  • 第一次需要真正操作数据的值的时候。比如上面print(q),如果你不去数据库拿q,print什么呢?
  • 落实修改动作的时候。你不操作数据库,怎么落实?

3. 检索单一对象

filter方法始终返回的是QuerySets,那怕只有一个对象符合过滤条件,返回的也是包含一个对象的QuerySets,这是一个集合类型对象,你可以类比Python列表,可迭代可循环可索引。

如果你确定你的检索只会获得一个对象,那么你可以使用Manager的get()方法来直接返回这个对象。

>>> one_entry = Entry.objects.get(pk=1)

在get方法中你可以使用任何filter方法中的查询参数,用法也是一模一样。

注意:使用get()方法和使用filter()方法然后通过[0]的方式分片,有着不同的地方。看似两者都是获取单一对象。但是,如果在查询时没有匹配到对象,那么get()方法将抛出DoesNotExist异常。这个异常是模型类的一个属性,在上面的例子中,如果不存在主键为1的Entry对象,那么Django将抛出Entry.DoesNotExist异常。

类似地,在使用get()方法查询时,如果结果超过1个,则会抛出MultipleObjectsReturned异常,这个异常也是模型类的一个属性。

所以:get()方法要慎用!

4. 其它QuerySet方法

大多数情况下,需要从数据库中查找对象时,使用all()、 get()、filter() 和exclude()就行。针对QuerySet的方法还有很多,都是一些相对高级的用法。

5. QuerySet的切片

使用类似Python对列表进行切片的方法可以对QuerySet进行范围取值。它相当于SQL语句中的LIMIT和OFFSET子句。参考下面的例子:

>>> Entry.objects.all()[:5]      # 返回前5个对象
>>> Entry.objects.all()[5:10]    # 返回第6个到第10个对象

注意:不支持负索引!例如 Entry.objects.all()[-1]是不允许的

通常情况,切片操作会返回一个新的QuerySet,并且不会被立刻执行。但是有一个例外,那就是指定步长的时候,查询操作会立刻在数据库内执行,如下:

>>> Entry.objects.all()[:10:2]

注意:由于 queryset 切片工作方式的模糊性,禁止对其进行进一步的排序或过滤。

若要获取单一的对象而不是一个列表(例如,SELECT foo FROM bar LIMIT 1),可以简单地使用索引而不是切片。例如,下面的语句返回数据库中根据标题排序后的第一条Entry:

>>> Entry.objects.order_by('headline')[0]

它相当于:

>>> Entry.objects.order_by('headline')[0:1].get()

注意:如果没有匹配到对象,那么第一种方法会抛出IndexError异常,而第二种方式会抛出DoesNotExist异常。

也就是说在使用get和切片的时候,要注意查询结果的元素个数。

6. 字段查询

字段查询是指如何指定SQL WHERE子句的内容。它们用作QuerySet的filter(), exclude()和get()方法的关键字参数。

其基本格式是:field__lookuptype=value注意其中是双下划线

默认查找类型为exact。

例如:

>>> Entry.objects.filter(pub_date__lte='2020-01-01')
# 相当于:
SELECT * FROM blog_entry WHERE pub_date <= '2020-01-01';

其中的字段必须是模型中定义的字段之一。但是有一个例外,那就是ForeignKey字段,你可以为其添加一个_id后缀(单下划线)。这种情况下键值是外键模型的主键原生值。例如:

>>> Entry.objects.filter(blog_id=4)

如果你传递了一个非法的键值,查询函数会抛出TypeError异常。

Django的数据库API支持20多种查询类型,下表列出了所有的字段查询参数:

字段名 说明
exact 精确匹配
iexact 不区分大小写的精确匹配
contains 包含匹配
icontains 不区分大小写的包含匹配
in 在..之内的匹配
gt 大于
gte 大于等于
lt 小于
lte 小于等于
startswith 从开头匹配
istartswith 不区分大小写从开头匹配
endswith 从结尾处匹配
iendswith 不区分大小写从结尾处匹配
range 范围匹配
date 日期匹配
year 年份
iso_year 以ISO 8601标准确定的年份
month 月份
day 日期
week 第几周
week_day 周几
iso_week_day 以ISO 8601标准确定的星期几
quarter 季度
time 时间
hour 小时
minute 分钟
second
regex 区分大小写的正则匹配
iregex 不区分大小写的正则匹配
  • exact

默认类型。如果你不提供查询类型,或者关键字参数不包含一个双下划线,那么查询类型就是这个默认的exact。

>>> Entry.objects.get(headline__exact="Cat bites dog")
# 相当于
# SELECT ... WHERE headline = 'Cat bites dog';
# 下面两个相当
>>> Blog.objects.get(id__exact=14)  # 显示指定
>>> Blog.objects.get(id=14)         # 隐含__exact
  • iexact

不区分大小写的查询。注意:SQLite3数据库不支持大小写区分。

>>> Blog.objects.get(name__iexact="liujiang blog")
# 匹配"liujiang Blog", "liujiang blog",甚至"LIUJIANG blOG".
  • contains

包含类型的匹配测试!大小写敏感!

Entry.objects.get(headline__contains='Lennon')
# 相当于
# SELECT ... WHERE headline LIKE '%Lennon%';
# 匹配'Today Lennon honored',但不匹配'today lennon honored'
  • icontains

contains的大小写不敏感模式。

  • startswith和endswith

以什么开头和以什么结尾。大小写敏感!

  • istartswith和iendswith

不区分大小写的模式。

  • in

在给定的列表里查找。

Entry.objects.filter(id__in=[1, 3, 4])

还可以使用动态查询集,而不是提供文字值列表:

inner_qs = Blog.objects.filter(name__contains='jodan')  # 结果是一个blog的queryset
entries = Entry.objects.filter(blog__in=inner_qs)  # 结果是一个entry的queryset

或者从values()或values_list()中获取的QuerySet作为比对的对象:

inner_qs = Blog.objects.filter(name__contains='Ch').values('name')
entries = Entry.objects.filter(blog__name__in=inner_qs)

下面的例子将产生一个异常,因为试图提取两个字段的值,但是查询语句只需要一个字段的值:

# 错误的实例,将弹出异常。
inner_qs = Blog.objects.filter(name__contains='Ch').values('name', 'id')
entries = Entry.objects.filter(blog__name__in=inner_qs)
  • gt

大于

Entry.objects.filter(id__gt=4)
  • gte

大于或等于

  • lt

小于

  • lte

小于或等于

  • range

范围测试(包含于之中)。

import datetime
start_date = datetime.date(2005, 1, 1)
end_date = datetime.date(2005, 3, 31)
Entry.objects.filter(pub_date__range=(start_date, end_date))

警告:过滤具有日期的DateTimeField不会包含最后一天,因为边界被解释为“给定日期的0am”。

  • date

进行日期对比。

Entry.objects.filter(pub_date__date=datetime.date(2005, 1, 1))
Entry.objects.filter(pub_date__date__gt=datetime.date(2005, 1, 1))

USE_TZ为True时,字段将转换为当前时区,然后进行过滤。

  • year

对年份进行匹配。

Entry.objects.filter(pub_date__year=2005)
Entry.objects.filter(pub_date__year__gte=2005)

USE_TZ为True时,在过滤之前,datetime字段将转换为当前时区。

  • iso_year

以ISO 8601标准确认的年份。整数类型。

Entry.objects.filter(pub_date__iso_year=2005)
Entry.objects.filter(pub_date__iso_year__gte=2005)
  • month

对月份进行匹配。取整数1(1月)至12(12月)。

Entry.objects.filter(pub_date__month=12)
Entry.objects.filter(pub_date__month__gte=6)

当USE_TZ为True时,在过滤之前,datetime字段将转换为当前时区。

  • day

对具体到某一天的匹配。

Entry.objects.filter(pub_date__day=3)
Entry.objects.filter(pub_date__day__gte=3)

当USE_TZ为True时,在过滤之前,datetime字段将转换为当前时区。

  • week

根据ISO-8601返回周号(1-52或53),即星期一开始的星期,星期四或之前的第一周。

Entry.objects.filter(pub_date__week=52)
Entry.objects.filter(pub_date__week__gte=32, pub_date__week__lte=38)

当USE_TZ为True时,字段将转换为当前时区,然后进行过滤。

  • week_day

进行“星期几”匹配。 取整数值,星期日为1,星期一为2,星期六为7。

Entry.objects.filter(pub_date__week_day=2)
Entry.objects.filter(pub_date__week_day__gte=2)

当USE_TZ为True时,在过滤之前,datetime字段将转换为当前时区。

  • iso_week_day

以ISO8601标准确定的星期几。取值范围1-7,分别表示星期一到星期日。

Entry.objects.filter(pub_date__iso_week_day=1)
Entry.objects.filter(pub_date__iso_week_day__gte=1)
  • quarter

季度。取值范围1-4。

Entry.objects.filter(pub_date__quarter=2)
  • time

将字段的值转为datetime.time格式并进行对比。

Entry.objects.filter(pub_date__time=datetime.time(14, 30))
Entry.objects.filter(pub_date__time__between=(datetime.time(8), datetime.time(17)))

USE_TZ为True时,字段将转换为当前时区,然后进行过滤。

  • hour

对小时进行匹配。 取0和23之间的整数。

Event.objects.filter(timestamp__hour=23)
Event.objects.filter(time__hour=5)
Event.objects.filter(timestamp__hour__gte=12)

当USE_TZ为True时,值将过滤前转换为当前时区。

  • minute

对分钟匹配。取0和59之间的整数。

Event.objects.filter(timestamp__minute=29)
Event.objects.filter(time__minute=46)
Event.objects.filter(timestamp__minute__gte=29)

当USE_TZ为True时,值将被过滤前转换为当前时区。

  • second

对秒数进行匹配。取0和59之间的整数。

Event.objects.filter(timestamp__second=31)
Event.objects.filter(time__second=2)
Event.objects.filter(timestamp__second__gte=31)

当USE_TZ为True时,值将过滤前转换为当前时区。

  • regex

区分大小写的正则表达式匹配。

Entry.objects.get(title__regex=r'^(An?|The) +')

建议使用原始字符串(例如,r'foo'而不是'foo')来传递正则表达式语法。

  • iregex

不区分大小写的正则表达式匹配。

Entry.objects.get(title__iregex=r'^(an?|the) +')

7. 跨越关系查询

Django提供了强大并且直观的方式解决跨越关联的查询,它在后台自动执行包含JOIN的SQL语句。要跨越某个关联,只需使用关联的模型字段名称,并使用双下划线分隔,直至你想要的字段(可以链式跨越,无限跨度)。例如:

# 返回所有Blog的name为'Beatles Blog'的Entry对象
# 一定要注意,返回的是Entry对象,而不是Blog对象。
# objects前面用的是哪个class,返回的就是哪个class的对象。
>>> Entry.objects.filter(blog__name='Beatles Blog')

反之亦然,如果要引用一个反向关联,只需要使用模型的小写名!

# 获取所有的Blog对象,前提是它所关联的Entry的headline包含'Lennon'
>>> Blog.objects.filter(entry__headline__contains='Lennon')

如果你在多级关联中进行过滤而且其中某个中间模型没有满足过滤条件的值,Django将把它当做一个空的(所有的值都为NULL)但是合法的对象,不会抛出任何异常或错误。例如,在下面的过滤器中:

Blog.objects.filter(entry__authors__name='Lennon')

如果Entry中没有关联任何的author,那么它将当作其没有name,而不会因为没有author 引发一个错误。通常,这是比较符合逻辑的处理方式。唯一可能让你困惑的是当你使用isnull的时候:

Blog.objects.filter(entry__authors__name__isnull=True)

这将返回Blog对象,它关联的entry对象的author字段的name字段为空,以及Entry对象的author字段为空。如果你不需要后者,你可以这样写:

Blog.objects.filter(entry__authors__isnull=False,entry__authors__name__isnull=True)

跨越多值的关系查询

最基本的filter和exclude的关键字参数只有一个,这种情况很好理解。但是当关键字参数有多个,且是跨越外键或者多对多的情况下,那么就比较复杂,让人迷惑了。我们看下面的例子:

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

这是一个跨外键、两个过滤参数的查询。此时我们理解两个参数之间属于-与“and”的关系,也就是说,过滤出来的BLog对象对应的entry对象必须同时满足上面两个条件。这点很好理解。也就是说上面要求entry同时满足两个条件

但是,当采用链式过滤就不一样了。

假设只有一个博客,取名a,与它关联的文章中有一些标题含有 "Lennon" 但不是2008年发布的,有一些是2008年发布但标题不包含"Lennon",没有2008年发布标题包含"Lennon"的文章。看下面的用法:

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
  • Blog.objects.filter(entry__headline__contains='Lennon')的结果是a
  • a.filter(entry__pub_date__year=2008)的结果依然是a

所以Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)的结果最终是a。

理解这个结果的核心是要理解第一个filter的过滤集合返回的是Blog而不是Entry实例,不是过滤了Entry,而是过滤了Blog。

但此时Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)的结果是空集。

把两个参数拆开,放在两个filter调用里面,按照我们前面说过的链式过滤,我们想当然的认为结果应该和上面的例子一样。可实际上,它不一样,这真是让人头疼。

多对多关系下的多值查询和外键foreignkey的情况一样。

但是,更头疼的来了,看看exclude的情况!

Blog.objects.exclude(entry__headline__contains='Lennon',entry__pub_date__year=2008,)

这会排除一些blog,这些blog的关联文章中同时有下面两种类型:

  • headline中包含“Lennon”
  • 在2008年发布的

而不管关联文章中是否有同时具备以上两个条件的文章。

那么要排除关联有同时满足上面两个条件的文章的blog,该怎么办呢?看下面:

Blog.objects.exclude(
entry=Entry.objects.filter(
    headline__contains='Lennon',
    pub_date__year=2008,
),
)

(有没有很坑爹的感觉?所以,建议在碰到跨关系的多值查询时,尽量使用Q查询)

8. F表达式

到目前为止的例子中,我们都是将模型字段与常量进行比较。但是,如果你想将模型的一个字段与同一个模型的另外一个字段进行比较该怎么办?

使用Django提供的F表达式!

例如,为了查找comments数目多于pingbacks数目的Entry,可以构造一个F()对象来引用pingback数目,并在查询中使用该F()对象:

>>> from django.db.models import F
>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks'))

Django支持对F()对象进行加、减、乘、除、求余以及幂运算等算术操作。两个操作数可以是常数和其它F()对象。

例如查找comments数目比pingbacks两倍还要多的Entry,我们可以这么写:

>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks') * 2)

为了查询rating比pingback和comment数目总和要小的Entry,我们可以这么写:

>>> Entry.objects.filter(rating__lt=F('number_of_comments') + F('number_of_pingbacks'))

你还可以在F()中使用双下划线来进行跨表查询。例如,查询author的名字与blog名字相同的Entry:

>>> Entry.objects.filter(authors__name=F('blog__name'))

对于date和date/time字段,还可以加或减去一个timedelta对象。下面的例子将返回发布时间超过3天后被修改的所有Entry:

>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

F()对象还支持.bitand().bitor().bitxor().bitrightshift().bitleftshift()等位操作,例如:

>>> F('somefield').bitand(16)

9. pk

pk就是primary key的缩写。通常情况下,一个模型的主键为“id”,所以下面三个语句的效果一样:

>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

可以联合其他类型的参数:

# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])
# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)

可以跨表操作:

>>> Entry.objects.filter(blog__id__exact=3) 
>>> Entry.objects.filter(blog__id=3) 
>>> Entry.objects.filter(blog__pk=3)

当主键不是id的时候,请注意了!

10. 转义百分符号和下划线

在原生SQL语句中%符号有特殊的作用。Django帮你自动转义了百分符号和下划线,你可以和普通字符一样使用它们,如下所示:

>>> Entry.objects.filter(headline__contains='%')
# 它和下面的一样
# SELECT ... WHERE headline LIKE '%\%%';

11. 缓存与查询集

每个QuerySet都包含一个缓存,用于减少对数据库的实际操作。理解这个概念,有助于你提高查询效率。

对于新创建的QuerySet,它的缓存是空的。当QuerySet第一次被提交后,数据库执行实际的查询操作,Django会把查询的结果保存在QuerySet的缓存内,随后的对于该QuerySet的提交将重用这个缓存的数据。

要想高效的利用查询结果,降低数据库负载,你必须善于利用缓存。看下面的例子,这会造成2次实际的数据库操作,加倍数据库的负载,同时由于时间差的问题,可能在两次操作之间数据被删除或修改或添加,导致脏数据的问题:

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

为了避免上面的问题,好的使用方式如下,这只产生一次实际的查询操作,并且保持了数据的一致性:

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # 提交查询
>>> print([p.pub_date for p in queryset]) # 重用查询缓存

何时不会被缓存

有一些操作不会缓存QuerySet,例如切片和索引。这就导致这些操作没有缓存可用,每次都会执行实际的数据库查询操作。例如:

>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # 查询数据库
>>> print(queryset[5]) # 再次查询数据库

但是,如果已经遍历过整个QuerySet,那么就相当于缓存过,后续的操作则会使用缓存,例如:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # 查询数据库
>>> print(queryset[5]) # 使用缓存
>>> print(queryset[5]) # 使用缓存

下面的这些操作都将遍历QuerySet并建立缓存:

>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

注意:简单的打印QuerySet并不会建立缓存,因为__repr__()调用只返回全部查询集的一个切片。

四、查询 JSONField

Django3.1新增了一种字段类型,也就是JSONField 。

注意:SQLite3数据库不支持JSONField字段类型。

JSONField 里的查找实现和其它字段类型不太一样,主要因为存在key转换。为了演示,我们将使用下面这个例子:

from django.db import models

class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = models.JSONField(null=True)

    def __str__(self):
        return self.name

保存和查询 None 值

因为JSON、Python和SQL三者在空值上的定义不太一样,所以需要谨慎处理。

与其他字段一样,当 None 作为字段值来保存时,将像 SQL 的 NULL 值一样保存。虽然不建议这样做,但可以使用 Value('null') 来存储 JSON 的 null 值。

无论存储哪个值,当从数据库中检索时,Python表示JSON的空值和 SQL 里的 NULL 一样,都是 None 。因此,很难区分它们。

这只适用于 None 值作为字段的顶级值。如果 None 被保存在列表或字典中,它将始终被解释为JSON的 null 值。

当查询时,None 值将一直被解释为JSON的null。要查询SQL的NULL,请使用 isnull参数。

请仔细揣摩下面的例子:

>>> Dog.objects.create(name='Max', data=None)  # SQL NULL.
<Dog: Max>
>>> Dog.objects.create(name='Archie', data=Value('null'))  # JSON null.
<Dog: Archie>
>>> Dog.objects.filter(data=None)
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data=Value('null'))
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data__isnull=True)
<QuerySet [<Dog: Max>]>
>>> Dog.objects.filter(data__isnull=False)
<QuerySet [<Dog: Archie>]>

除非你确定要使用SQL 的 NULL 值,否则请考虑设置 null=False 并为空值提供合适的默认值,例如 default=dict()

注意:保存JSON的 null 值不违反Django的 null=False

Key, index, 和路径转换

为了查询给定的字典键,请将该键作为查询名:

>>> Dog.objects.create(name='Rufus', data={
...     'breed': 'labrador',
...     'owner': {
...         'name': 'Bob',
...         'other_pets': [{
...             'name': 'Fishy',
...         }],
...     },
... })
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': None})
<Dog: Meg>
>>> Dog.objects.filter(data__breed='collie')
<QuerySet [<Dog: Meg>]>

可以将多个键用双下划线链接起来形成一个路径查询:

>>> Dog.objects.filter(data__owner__name='Bob')
<QuerySet [<Dog: Rufus>]>

如果键是个整数,那么它将在列表中被解释成一个索引:

>>> Dog.objects.filter(data__owner__other_pets__0__name='Fishy')
<QuerySet [<Dog: Rufus>]>

如果要查询的键与另一个查询的键名冲突,请改用 contains 来查询。

如果查询时缺少键名,请使用 isnull 查询:

>>> Dog.objects.create(name='Shep', data={'breed': 'collie'})
<Dog: Shep>
>>> Dog.objects.filter(data__owner__isnull=True)
<QuerySet [<Dog: Shep>]>

上面给出的例子隐式地使用了exact 查找。Key、索引和路径转换也可以用:icontains, endswith, iendswith, iexact, regex, iregex, startswith, istartswith, lt, lte, gt, gte等查询手段。

包含与键查找

contains

JSONField 上的 contains 查找逻辑被重写了。返回的对象是那些给定的键值对都包含在顶级字段中的对象。例如:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contains={'owner': 'Bob'})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>
>>> Dog.objects.filter(data__contains={'breed': 'collie'})
<QuerySet [<Dog: Meg>]>

Oracle 和 SQLite 在这里不支持 contains

contained_by

将参数值定义为集合A,每个实例的值定义为集合B,如果B是A的子集,那么这个实例将被选中:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie', 'owner': 'Bob'})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie'})
<QuerySet [<Dog: Fred>]>

Oracle 和 SQLite不支持contained_by

has_key

通过键过滤对象,匹配的键必须位于嵌套的最顶层:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_key='owner')
<QuerySet [<Dog: Meg>]>

has_keys

返回同时具备指定的多个键的对象,这些键必须位于最顶层。:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_keys=['breed', 'owner'])
<QuerySet [<Dog: Meg>]>

has_any_keys

只要具备指定键列表中的任何一个键,这个对象就会被返回。这些键必须位于最顶层。

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_any_keys=['owner', 'breed'])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

请思考一个问题,能不能对JSONField中data的二级、三级或更深入层次进行键匹配查询呢?

如果能,怎么做?试试就知道了!

五、Q对象

普通filter查询里的条件都是“and”逻辑,如果你想实现“or”逻辑怎么办?用Q查询!

Q来自django.db.models.Q,用于封装关键字参数的集合,可以作为关键字参数用于filter、exclude和get等函数。 例如:

from django.db.models import Q
Q(question__startswith='What')

可以使用&或者|~来组合Q对象,分别表示与、或、非逻辑。它将返回一个新的Q对象。

Q(question__startswith='Who')|Q(question__startswith='What')
# 这相当于:
WHERE question LIKE 'Who%' OR question LIKE 'What%'

更多的例子:

Q(question__startswith='Who') | ~Q(pub_date__year=2005)

你也可以这么使用,默认情况下,以逗号分隔的都表示AND关系:

Poll.objects.get(
Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)
# 它相当于
# SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

当关键字参数和Q对象组合使用时,Q对象必须放在前面,如下例子:

Poll.objects.get(
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)), question__startswith='Who')

六、比较对象

要比较两个模型实例,只需要使用python提供的双等号比较符就可以了。在后台,其实比较的是两个实例的主键的值。下面两种方法是等同的:

>>> some_entry == other_entry
>>> some_entry.id == other_entry.id

如果模型的主键不叫做“id”也没关系,后台总是会使用正确的主键名字进行比较,例如,如果一个模型的主键的名字是“name”,那么下面是相等的:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name

这其实是Django在内部为models.Model实现了__eq__()魔法方法,用来对模型实例进行是否相等的布尔判断。

判断基于下面的逻辑:

  • 同一模型下,主键相等则实例相等
  • 主键为None时,和任何实例都不相等
  • 实例等于自己本身
  • 代理模型的实例等于相同主键的父类实例
  • 多表继承时,哪怕主键值相等,实例也不相等

参考下面的例子:

from django.db import models

class MyModel(models.Model):
    id = models.AutoField(primary_key=True)

class MyProxyModel(MyModel):
    class Meta:
        proxy = True

class MultitableInherited(MyModel):
    pass

# 主键对比
MyModel(id=1) == MyModel(id=1)
MyModel(id=1) != MyModel(id=2)

# 主键为None
MyModel(id=None) != MyModel(id=None)

# 相同的实例
instance = MyModel(id=None)
instance == instance

# 代理模型和父类
MyModel(id=1) == MyProxyModel(id=1)

# 多表继承
MyModel(id=1) != MultitableInherited(id=1)

七、删除对象

删除对象使用的是对象的delete()方法。该方法将返回被删除对象的总数量和一个字典,字典包含了每种被删除对象的类型和该类型的数量。如下所示:

>>> e.delete()
(1, {'weblog.Entry': 1})

也可以批量删除。每个QuerySet都有一个delete()方法,它能删除该QuerySet的所有成员。例如:

>>> Entry.objects.filter(pub_date__year=2020).delete()
(5, {'webapp.Entry': 5})

需要注意的是,有可能不是每一个对象的delete方法都被执行。如果你改写了delete方法,为了确保对象被删除,你必须手动迭代QuerySet进行逐一删除操作。

当Django删除一个对象时,它默认使用SQL的ON DELETE CASCADE约束,也就是说,任何有外键指向要删除对象的对象将一起被删除。例如:

b = Blog.objects.get(pk=1)
# 下面的动作将删除该条Blog和所有的它关联的Entry对象
b.delete()

这种级联的行为可以通过的ForeignKey的on_delete参数自定义。

注意,delete()是唯一没有在管理器上暴露出来的方法。这是刻意设计的一个安全机制,用来防止你意外地请求类似Entry.objects.delete()的动作,而不慎删除了所有的条目。如果你确实想删除所有的对象,你必须明确地请求一个完全的查询集,像下面这样:

Entry.objects.all().delete()

在多表继承时,如果你只想删除子类的对象,而不想删除父类的数据,可以使用参数keep_parents=True

注意:有些删除模型实例的方法不会触发delete()方法,比如批量删除和级联删除,为了确保你的自定义代码被执行,请使用pre_delete或者post_delete信号机制来替代delete方法。

八、复制模型实例

虽然没有内置的方法用于复制模型的实例,但还是很容易创建一个新的实例并将原实例的所有字段都拷贝过来。最简单的方法是将原实例的pk设置为None,这会创建一个新的实例copy。示例如下:

blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save() # blog.pk == 1
#
blog.pk = None
blog.save() # blog.pk == 2

但是在继承父类的时候,情况会变得复杂,如果有下面一个Blog的子类:

class ThemeBlog(Blog):
    theme = models.CharField(max_length=200)

django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme='python')
django_blog.save() # django_blog.pk == 3

基于继承的工作机制,你必须同时将pk和id设为None:

django_blog.pk = None
django_blog.id = None
django_blog.save() # django_blog.pk == 4

对于外键和多对多关系,更需要进一步处理。例如,Entry有一个ManyToManyField到Author。 复制条目后,您必须为新条目设置多对多关系,像下面这样:

entry = Entry.objects.all()[0] # some previous entry
old_authors = entry.authors.all()
entry.pk = None
entry.save()
entry.authors.set(old_authors)

对于OneToOneField,还要复制相关对象并将其分配给新对象的字段,以避免违反一对一唯一约束。 例如,假设entry已经如上所述重复:

detail = EntryDetail.objects.all()[0]
detail.pk = None
detail.entry = entry
detail.save()

九、批量更新对象

使用update()方法可以批量为QuerySet中所有的对象进行更新操作。

# 更新所有2007年发布的entry的headline
Entry.objects.filter(pub_date__year=2020).update(headline='刘江的Django教程')

只可以对普通字段和ForeignKey字段使用这个方法。若要更新一个普通字段,只需提供一个新的常数值。若要更新ForeignKey字段,需设置新值为你想指向的新模型实例。例如:

>>> b = Blog.objects.get(pk=1)
# 修改所有的Entry,让他们都属于b
>>> Entry.objects.all().update(blog=b)

update方法会被立刻执行,并返回操作匹配到的行的数目(有可能不等于要更新的行的数量,因为有些行可能已经有这个新值了)。

唯一的约束是:只能访问一张数据库表。你可以根据关系字段进行过滤,但你只能更新模型主表的字段。例如:

>>> b = Blog.objects.get(pk=1)
# Update all the headlines belonging to this Blog.
>>> Entry.objects.select_related().filter(blog=b).update(headline='Everything is the same')

要注意的是update()方法会直接转换成一个SQL语句,并立刻批量执行。

update方法不会运行模型的save()方法,或者产生pre_savepost_save信号(调用save()方法产生)或者服从auto_now字段选项。如果你想保存QuerySet中的每个条目并确保每个实例的save()方法都被调用,你不需要使用任何特殊的函数来处理。只需要迭代它们并调用save()方法:

for item in my_queryset:
    item.save()

update方法可以配合F表达式。这对于批量更新同一模型中某个字段特别有用。例如增加Blog中每个Entry的pingback个数:

>>> Entry.objects.all().update(number_of_pingbacks=F('number_of_pingbacks') + 1)

然而,与filter和exclude子句中的F()对象不同,在update中你不可以使用F()对象进行跨表操作,你只可以引用正在更新的模型的字段。如果你尝试使用F()对象引入另外一张表的字段,将抛出FieldError异常:

# 这将会导致一个FieldError异常
>>> Entry.objects.update(headline=F('blog__name'))

十、关联对象的查询

利用本节一开始的模型,一个Entry对象e可以通过blog属性e.blog获取关联的Blog对象。反过来,Blog对象b可以通过entry_set属性b.entry_set.all()访问与它关联的所有Entry对象。

Django的ORM系统是个功能强大、体系复杂的系统,我们在实际使用中不但要理解透彻,还要能正确区分不同的做法,比如这里:

  • Entry.objects.xxx是获取Entry模型的实例集合
  • e.blog是通过正向关联关系,获取Blog模型实例的集合
  • b.entry_set.all()是通过反向关联关系,获取所有的entry组合
  • Entry.objects.filter(blog=xxx)是对关联的blog外键字段进行查询过滤

实际上,如果你理解了b.entry_set等同于Entry.objects.filter(blog=b),就能理解上面的内容。

1. 一对多(外键)

正向查询:

直接通过圆点加属性,访问外键对象:

>>> e = Entry.objects.get(id=2)
>>> e.blog # 返回关联的Blog对象

要注意的是,对外键的修改,必须调用save方法进行保存,例如:

>>> e = Entry.objects.get(id=2)
>>> e.blog = some_blog
>>> e.save()

如果一个外键字段设置有null=True属性,那么可以通过给该字段赋值为None的方法移除外键值:

>>> e = Entry.objects.get(id=2)
>>> e.blog = None
>>> e.save() # "UPDATE blog_entry SET blog_id = NULL ...;"

在第一次对一个外键关系进行正向访问的时候,关系对象会被缓存。随后对同样外键关系对象的访问会使用这个缓存,例如:

>>> e = Entry.objects.get(id=2)
>>> print(e.blog)  # 访问数据库,获取实际数据
>>> print(e.blog)  # 不会访问数据库,直接使用缓存的版本

请注意QuerySet的select_related()方法会递归地预填充所有的一对多关系到缓存中。例如:

>>> e = Entry.objects.select_related().get(id=2)
>>> print(e.blog)  # 不会访问数据库,直接使用缓存
>>> print(e.blog)  # 不会访问数据库,直接使用缓存

反向查询:

如果一个模型有ForeignKey,那么该ForeignKey所指向的外键模型的实例可以通过一个管理器进行反向查询,返回源模型的所有实例。默认情况下,这个管理器的名字为FOO_set,其中FOO是源模型的小写名称。该管理器返回的查询集可以用前面提到的方式进行过滤和操作。

>>> b = Blog.objects.get(id=1)
>>> b.entry_set.all() # 返回一些Entry实例,这些实例都关联到博客b
# b.entry_set 本质上是一个管理器
>>> b.entry_set.filter(headline__contains='Lennon')
>>> b.entry_set.count()

你可以在ForeignKey字段的定义中,通过设置related_name来重写FOO_set的名字。举例说明,如果你修改Entry模型blog = ForeignKey(Blog, on_delete=models.CASCADE, related_name=’entries’),那么上面的例子会变成下面的样子:

>>> b = Blog.objects.get(id=1)
>>> b.entries.all() 

>>> b.entries.filter(headline__contains='Lennon')
>>> b.entries.count()

使用自定义的反向管理器:

默认情况下,用于反向关联的RelatedManager是该模型默认管理器的子类。如果你想为一个查询指定一个不同的管理器,你可以使用下面的语法:

from django.db import models

class Entry(models.Model):
    #...
    objects = models.Manager()  # 默认管理器
    entries = EntryManager()    # 自定义管理器

b = Blog.objects.get(id=1)
b.entry_set(manager='entries').all()

当然,指定的自定义反向管理器也可以调用它的自定义方法:

b.entry_set(manager='entries').is_published()

2. 多对多

多对多关系的两端都会自动获得访问另一端的API。这些API的工作方式与前面提到的“反向”一对多关系的用法一样。

e = Entry.objects.get(id=3)

e.authors.all() # Returns all Author objects for this Entry.
e.authors.count()
e.authors.filter(name__contains='John')
#
a = Author.objects.get(id=5)
a.entry_set.all() # Returns all Entry objects for this Author.

与外键字段中一样,在多对多的字段中也可以指定related_name名。

(注:在一个模型中,如果存在多个外键或多对多的关系指向同一个外部模型,必须给他们分别加上不同的related_name,用于反向查询)

3. 一对一

一对一非常类似多对一关系,可以简单的通过模型的属性访问关联的模型。

class EntryDetail(models.Model):
    entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
    details = models.TextField()

ed = EntryDetail.objects.get(id=2)
ed.entry # Returns the related Entry object.

不同之处在于反向查询的时候。一对一关系中的关联模型同样具有一个管理器对象,但是该管理器表示一个单一的对象而不是对象的集合:

e = Entry.objects.get(id=2)
e.entrydetail # 返回关联的EntryDetail对象

如果没有对象赋值给这个关系,Django将抛出一个DoesNotExist异常。 可以给反向关联进行赋值,方法和正向的关联一样:

e.entrydetail = ed

4. 反向关联是如何实现的?

一些ORM框架需要你在关系的两端都进行定义。Django的开发者认为这违反了DRY (Don’t Repeat Yourself)原则,所以在Django中你只需要在一端进行定义。

那么这是怎么实现的呢?因为在关联的模型类没有被加载之前,一个模型类根本不知道有哪些类和它关联。

答案在app registry!在Django启动的时候,它会导入所有INSTALLED_APPS中的应用和每个应用中的模型模块。每创建一个新的模型时,Django会自动添加反向的关系到所有关联的模型。如果关联的模型还没有导入,Django将保存关联的记录并在关联的模型导入时添加这些关系。

由于这个原因,将模型所在的应用都定义在INSTALLED_APPS的应用列表中就显得特别重要。否则,反向关联将不能正确工作。

5. 通过关联对象进行查询

涉及关联对象的查询与正常值的字段查询遵循同样的规则。当你指定查询需要匹配的值时,你可以使用一个对象实例或者对象的主键值。

例如,如果你有一个id=5的Blog对象b,下面的三个查询将是完全一样的:

Entry.objects.filter(blog=b) # 使用对象实例
Entry.objects.filter(blog=b.id) # 使用实例的id
Entry.objects.filter(blog=5) # 直接使用id

十一、关联对象的增删改

除了在前面定义的QuerySet方法之外,管理器还有其它方法用于处理关联的对象集合。下面是每个方法的概括。

  • add(obj1, obj2, ...):添加指定的模型对象到关联的对象集中。

  • create(**kwargs):创建一个新的对象,将它保存并放在关联的对象集中。返回新创建的对象。

  • remove(obj1, obj2, ...):从关联的对象集中删除指定的模型对象。

  • clear():清空关联的对象集。

  • set([obj1,obj2...]):重置关联的对象集。

若要一次性给关联的对象集赋值,使用set()方法,并给它赋值一个可迭代的对象集合或者一个主键值的列表。例如:

b = Blog.objects.get(id=1)
b.entry_set.set([e1, e2])

在这个例子中,e1和e2可以是完整的Entry实例,也可以是整数的主键值。

如果clear()方法可用,那么在将可迭代对象中的成员添加到集合中之前,将从entry_set中删除所有已经存在的对象。如果clear()方法不可用,那么将直接添加可迭代对象中的成员而不会删除所有已存在的对象。

这节中的每个反向操作都将立即在数据库内执行。所有的增加、创建和删除操作也将立刻自动地保存到数据库内。

下面是更多的使用案例:

# 增加
>>> b = Blog.objects.get(id=1)
>>> e = Entry.objects.get(id=2)
>>> b.entry_set.add(e) # Associates Entry e with Blog b.

# 创建
# 此时不需要调用e.save(),会自动保存
>>> e = b.entry_set.create(
...     headline='Hello',
...     body_text='Hi',
...     pub_date=datetime.date(2005, 1, 1)
... )

# 和上面相当
>>> b = Blog.objects.get(id=1)
>>> e = Entry(
...     blog=b,
...     headline='Hello',
...     body_text='Hi',
...     pub_date=datetime.date(2005, 1, 1)
... )
>>> e.save(force_insert=True)

# 删除
>>> b = Blog.objects.get(id=1)
>>> e = Entry.objects.get(id=2)
>>> b.entry_set.remove(e) # Disassociates Entry e from Blog b.

# 清空
>>> b = Blog.objects.get(id=1)
>>> b.entry_set.clear()

# 批量设置
>>> new_list = [obj1, obj2, obj3]
>>> e.related_set.set(new_list)

十二、使用原生SQL语句

如果你发现需要编写的Django查询语句太复杂,你可以回归到手工编写SQL语句。Django对于编写原生的SQL查询有许多选项。

最后,需要注意的是Django的数据库层只是一个数据库接口。你可以利用其它的工具、编程语言或数据库框架来访问数据库,Django没有强制指定你非要使用它的某个功能或模块。


 验证器 查询集API 

评论总数: 21


点击登录后方可评论

刘老师好,我貌似发现了一个bug,这条语句会报错 Blog.objects.exclude( entry=Entry.objects.filter( headline__contains='Lennon', pub_date__year=2008, ), ) ValueError: The QuerySet value for an exact lookup must be limited to one result using slicing 改成entry__in=就对了,因为里面查询出来的是一个列表。



老师您好!请教一个问题:admin页面中按照您之前的教程,question创建页面下面会带有choice设置。那我再clean()或save()一个question时,能否设置必须至少添加一个选择呢?尝试之后我发现不能再question的models中实现这一功能——因为当我在save一个新问题前后,尽管我在管理员页面(非python shell)添加了choice,打印choice_set均为空。



所以是否只能通过修改管理员页面的源码实现这一功能呢?



可以实现。重写save方法。



打卡



刘老师: 请教你一个问题!我现在公司用的是Oracle ERP,里面的表几乎都没有主键,我又不能给这些原生的表加主键,有什么办法可以使用model操作这些表吗?如果使用原生SQL,那model的特性就利用不上了。用不上model,那使用django是不是就没有啥意义了,有没有什么好的办法呢?



对于公司已有的数据库,而且还是Oracel这种,我建议不要强行上Django,因为这搞不好会带来数据风险。 一般来说,Django项目开发之初,就应该设计好模型,选择合适的数据库,并后期良好维护。 不过就你目前的情况,从已有Oracel数据库,也是可以反推出模型结构的,使用的命令是python manage.py inspectdb,你可以深入研究一下。但是最好的做法还是与领导、DBA共同讨论方案,确保安全可靠。



inspectdb也用了,多数情况都是取不回model的,即使取回来了,也不能对表进行操作,原因是表里面没有主键。ERP里的表没有一个是通过主键和外键定义约束的,而django没有主键又操作不了表,看来是没解了



咱们这样其实是强Django所难,人本来不是专门搞这个的。 你的需求更多的是DBA考虑和解决的问题,而不是Web框架的任务。



请问对于一对多多的Entry如何初始化一条数据呢。e1=Entry.objects.get(id=1)报错:blog.models.Entry.DoesNotExist: Entry matching query does not exist.



执行保存后数据库会自己增加一条心的记录是咋回事呢? ``` python @login_required def con_save(request): con = Contract.objects.get(pk=request.POST['conid']) con.orderno = request.POST['orderno'] con.contractno = request.POST['contractno'] con.factory = request.POST['factory'] con.factory_addr = request.POST['factory_addr'] con.factory_attn = request.POST['factory_attn'] con.goodsname = request.POST['goodsname'] con.qty = request.POST['qty'] con.pol = request.POST['pol'] con.pod = request.POST['pod'] con.op = request.POST['op'] con.pkg = request.POST['pkg'] con.cntr_type = request.POST['cntr_type'] con.save(update_fields=['orderno']) return HttpResponseRedirect(reverse('jask:con_detail', args=(con.id,))) ```



查出原因来了,是其他地方的代码有问题 :(



这个百分号真的有用么。 >>> Entry.objects.filter(headline__contains='baidu') <QuerySet [<Entry: baidu>]> >>> Entry.objects.filter(headline__contains='%') <QuerySet []> >>>



需要先执行下列命令 import os os.environ['DJANGO_SETTINGS_MODULE']= 'mysite.settings' import django django.setup()



不是在python命令行下,而是在python manage.py shell命令行下



实际运用中感觉遇到复杂查询还是回归原生sql了,快捷。



这个章节,刷新很多认知啊~要反复阅读理解呢. 谢谢刘大大



博主您好: 我看了这章“查询操作”->"三、检索对象"->"7. 跨越关系查询"的exclude阐述 和下一章“查询集API”->“三、返回新QuerySets的API”->2. exclude() 的阐述后发现,这两章对exclude的阐述有矛盾。 这一章中: ①Blog.objects.exclude(entry__headline__contains='Lennon',entry__pub_date__year=2008,) 此段exclude阐述为 or 关系。 而下一章中 ②Entry.objects.exclude(pub_date__gt=datetime.date(2005, 1, 3), headline='Hello') 对exclude的阐述却为 and 关系。 这里语句样式是完全一样的均为 class.objects.exclude(item1=equation1,item2=equation2) 仅一点不同,即①为“跨越多值的关系查询” 难道对于多值的跨越和不跨越造成了exclude功能的不同? 望博主解惑,谢谢



这里比较难以理解。所以尽量不要使用多个过滤条件的exclude,而是用Q表达式。 PS:官方文档的解释 For example, the following query would exclude blogs that contain both entries with “Lennon” in the headline and entries published in 2008: https://docs.djangoproject.com/en/1.11/topics/db/queries/#lookups-that-span-relationships



感谢博主的答复,使用Q表达式确实更加容易理解一些。 我今天用一组数据测试了下,两条语句都应为 and 关系。 也就是:①select * from class where not (item1=equation1 and item2=equation2); ②select * from class1 as a,class2 as b where a.id = b.pid and not (b.item1=equation1 and b.item2=equation2) 测试语句模型: Blog.objects.exclude(entry__pub_date__gt=datetime.date(2005,1,3), entry__headline='headtest').values(); Entry.objects.exclude(pub_date__gt=datetime.date(2005,1,3), headline='headtest').values(); 测试数据概况: Blog中两条数据 Entry中一条外键关联Blog_id=1的数据



应该是OR,你再测测