Django 3.2.2 Making queries 进行查询

译者注: 本文翻译自Django的官方文档2.1版本。因此只适用于2.1版本和其它未改动本章节的版本。
文档尽量全文翻译,但是以下情况下可能没有翻译:章节的开头和结尾没有实质内容的引导语和结束语,原文中的所有标题、一些斜体和粗体的术语或链接,代码中的部分注释,一些和Django本身关系不太大的技术细节。
文中的部分英文斜体和粗体在原文中是转到相关章节的超链接,但译文中没做链接,后期翻译的章节足够时会逐渐添加。
受译者水平所限,文中一些术语和专业的表达可能有错误,欢迎通过简信或评论提出错误和修改意见。
祝各位Django开发者和爱好者阅读使用愉快!

3.2.2 Making queries

纵观整个教程,我们都要依赖下面的模型,这些模型构造了一个网络博客应用:

from django 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)        
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()
    
    def __str__(self):
        return self.headline
Creating objects

为创建一个对象,使用模型类的关键字参数来实例化它,然后调用save() 来保存到数据库。
假设模型在文件mysite/blog/models.py中,但这有一个例子:

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

这会在SQL中执行INSERT语句。Django直到你显式调用save() 时才会和数据库交互。
save() 方法没有返回值。
亦见:
save() 有许多高级选项这里没有提及。见文档中save() 的完整细节。
为一步创建和保存一个对象,使用create() 方法。

Saving changes to objects

想保存对已在数据中的对象的修改,使用save()
给定一个已在数据库中保存的Blog实例b5,这个例子改变了它的名字并在数据库中更新:

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

这执行了SQL的UPDATE语句。Django直到你显式调用save() 时才会和数据库交互。

Saving ForeignKey and ManyToManyField fields

更新ForeignKey字段和保存普通字段的方式一样,把一个正确类型的对象赋给要求的字段就可以了。这个例子更新了blog属性的Entry的实例entry,假设数据库中已经保存了合适的EntryBlog实例:

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

更新ManyToManyField有些不同——在字段上使用add() 方法来增加一条关系的记录。下例向entry对象增加了Author实例joe

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

为在ManyToManyField中一次添加多条记录,在一次add() 调用中传入多个参数,如下:

>>> 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会抱怨。

Retrieving objects

为从数据库获取对象,通过模型类的Manager构造一个QuerySet
一个QuerySet代表数据库对象的一个集合。可以包括0个,一个或更多filters。过滤器基于给定的参数缩减了查询结果的范围。在SQL术语中,一个QuerySet相当于一个SELECT语句,一个过滤器相当于一个限制语句比如WHERELIMIT
通过使用模型的Manger来获取QuerySet。每个模型有至少一个Manager,默认被称为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 instance."

注意: Managers只能通过模型类访问,而不是从模型实例,这使得“表级别”和“记录级别”的操作分离。


Manager是模型的QuerySets的主要来源。例如Blog.objects.all()返回包含数据库中Blog所有对象的QuerySet

Retrieving all objects

最简单的获取方式是获取一个表中所有对象。使用Manager中的all() 方法。

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

all()方法返回数据库中所有对象的QuerySet

Retrieving specific objects with filters

两个最常用的过滤条件:

  • filter(**kwargs) 返回新的Queryset包含匹配给定查询参数的对象。
  • exclude(**kwargs) 返回新的Queryset包含排除匹配给定查询参数的对象。

查询参数应该按照Field lookup中的格式。
例如为获取2006年的博客入口的QuerySet,这样使用filter()

Entry.objects.filter(pub_date__year=2006)

使用默认管理器类时,它等同于:

Entry.objects.all().filter(pub_date__year=2006)
Chaining filters

QuerySet提炼的结果还是QuerySet,所以可以链式提炼,比如:

>>> Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.date.today()
... ).filter(
...     pub_date__gte=datetime(2005, 1, 30)
... )
Filtered QuerySets are unique

每次提炼QuerySet时你都得到了一个全新的QuerySet,它和之前的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())

这三个QuerySet是独立的。第一个是一个基本的QuerySet办函了所有包括以'What'开头的标题。第二个是第一个的子集,但有一个筛选,排除了pub_date是今天或是未来的记录。第三个是第一个的子集,额外的筛选是只要pub_date是今天或未来的记录。最初的QuerySet(q1)没有受到提炼过程的影响。

QuerySet are lazy

QuerySets是懒惰的——创建QuerySet的行为不含任何数据库活动。你可以一直堆栈过滤器,Django直到QuerySet被计算之前都不会真的进行查询。看一眼实例:

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

看起来像是访问了数据库三次,但是实际上只访问了一次,在最后一行print(q)的时候。通常来说,QuerySet的结果在你真的要用到他们之前不会被取得。当你用到时,QuerySet才通过访问数据库计算。对于更多信息,参见When QuerySets are evaluated

Retrieving a single object with get()

filter()总会给你一个QuerySet,即使只有一个对象匹配查询条件。在这种情况下,返回的是只含一个元素的QuerySet
如果你事先知道只有一个结果匹配你的查询,你可以在Manager上使用get() 方法直接返回对象。

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

你可以在get()使用任意表达式,就像使用filter()——详见Field lookups
注意在使用get()filter()\[0\]切片是不同的。如果没有结果匹配查询,get()会引发DoesNotExist异常。该异常是查询语句操作的模型类的一个属性。所以上方的Entry对象没有主键1的话,Django会抛出Entry.DoesNotExist
类似地,Django会抱怨如果有多余一个对象匹配了get() 查询。在这种情况下,会抛出MultipleObjectsReturned,仍然是模型类的属性。

Other QuerySet methods

大多时候当你想从数据库查找对象时,你会使用all(), get(),filter()exclude()。但查询远远不止这些;见QuerySet API Reference来获得多种QuerySet方法的完整列表。\

Limiting QuerySet

使用Python的数组切片语法的一部分来限制QuerySet的特定数量的结果。这等同于SQL的LIMITOFFSET语句。
比如,这会返回前5个对象(LIMIT 5):

>>> Entry.objects.all()[:5]

这返回了第六个到第十个对象(OFFSET 5 LIMIT 5):

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

负索引(例如:Entry.objects.all()[-1])并不支持。
通常,切片QuerySet返回一个新的QuerySet,并不会计算查询。唯一的意外是如果你是用了Python切片中的“步数”参数时。比如,下面为了返回前十中每两个元素的列表会真的进行查询:

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

为获得一个对象而不是列表(比如:SELECT foo FROM bar LIMIT 1),使用单个索引而不是切片。例如,这会在把入口按照标题的字母顺序排序后返回数据库中的首个Entry

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

这大致上相当于:

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

但是注意如果匹配对象不存在,第一个会引发IndexError而第二个会引发DoesNotExist。详见get()

Field lookups

字段查询是你如何指定SQL的WHERE语句。他们被指定为QuerySet的方法filter(),exclude()get() 参数。
基本的查询语句关键字按照field__lookuptype=value的形式(注意是双下划线)。例如:

Entry.objects.filter(pub_date__lte='2006-01-01')

大致上会被翻译成SQL语句:

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

这是怎么做到的
Python有能力定义接收任意键值对参数的函数,能实时解析键值对。想知道更多信息,见官方Python教程Keyword Arguments


查询中指定的字段必须是模型的字段名。尽管在ForeignKey情况下有例外,你可以指定字段名跟后缀_id。在这种情况下,值参数应该是外键模型的主键原始值。比如:

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

如果你传入了非法的键值对,查询函数会抛出TypeError
数据库接口支持二十多个查询类型:完整的参考可见field lookup reference。为先介绍个大概,这里举些常用的例子:

  • exact 一个完全匹配,例如:
>>> Entry.objects.get(headline__exact="Cat bites dog")

会形成SQL语句:

SELECT ... WHERE headline = 'Cat bites dog';

如果你没有提供查询类型,也就是说查询参数没有双下划线,查询就会假设使用了exact
例如,下面的两个语句是等价的:

>>> Blog.objects.get(id__exact=14)  # 显式形式
>>> Blog.objects.get(id=14)         # __exact是隐式的

这是为了方便,因为exact查询是最常见的。

  • iexact 一个大小写不敏感匹配,查询:
>>> Blog.objects.get(name__iexact="beatles blog")

这会匹配一个标题为"Beatles Blog","beatles blog"甚至"BeAtlES blOG"的Blog。

  • contains 大小写敏感的包含语句。例如:
Entry.objects.get(headlines__contains='Lennon')

大致会翻译成:

SELECT ... WHERE headline LIKE '%Lennon%';

注意这会匹配标题'Today Lennon honored'而不是'today lennon honored'。
还有个大小写不敏感的版本,icontains。

  • startswith,endswith以...开头,以...结尾的独立查询。也有不敏感版本,istartswithiendswith
Lookups that span relationship

Django提供了一种强有力且符合直觉的方式在查询中来跟踪关系,自动为你处理了SQL的JOIN语句。为跨越表关系,只要使用跨模型中相关字段名。用双下划线分割,直到你达到了你想要的字段。
下例获取了name为'Beatles Blog'的Blog有关的所有Entry对象:

>>> Entry.objects.filter(blog__name='Beatles Blog')

跨表的深度可以如你所愿。
它也能反过来运作。为查询一个反向的关系,使用模型的小写名就可以。
下例获取了所有至少有一个headline包含了‘Lennon’的Entry的Blog对象:

>>> Blog.objects.filter(entry__headline__contains='Lennon')

如果你在通过多重关系进行过滤且其中一个内部关系模型并没有满足过滤关系的值,Django会处理成一个空的(所有值为NULL)但有效的对象。就是说不会抛出错误:比方说:

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

(假设有一个相关联的Author模型)。如果没有和entry相关联的author,也会被当做没有相关的name,而不是因为没找到author抛出异常。通常这就是你想要的,但有种情况可能会让你弄混,就是使用isnull时,也就是:

Blog.objects.filter(entry__authors__name__isnull=True)

这会返回关联authorname为空的Blog对象,也会返回关联entryauthor为空的对象。如果不想后者也包括在内,应该写:

Blog.objects.filter(entry__author__isnull=False, entry__authors__name__isnull=True)
Spanning multi-valued relationships

当你基于ManyToManyField或反向的ForeignKey时,有两个不同类型的过滤器可能是你想要的。考虑下Blog/Entry关系(Blog对Entry是一对多关系)。我们可能想找到在标题中含有'Lennon'且在2008发布的文章的博客,也有可能想找到那些博客的文章标题中含有'Lennon'或是文章发布于2008的。因为多个文章都和一个Blog相关联,这两种查询都是可能发生的且在某些情况下都是有意义的。
同样的情况也会在ManyToManyField上发生。比如如果一个EntryManyToManyField叫做tags,我们可能想找到连接到称为"music"和"bands"标签的文章,也可能想找到含"music"的文章或是含"public"的文章。
为了处理这两种情况,Django有一个处理起来比较一致的调用filter() 的方式。一个filter() 中的所有条件需要同时满足。成功的filter() 的调用会显示对象集合,但还是对于多值关系,他们只适用于连接到主模型的对象,而不是从早先的filter() 调用开始限制。
听起来可能有些迷惑,所以但愿能用个例子说清楚。为了选在同时含有'Lennon'的标题且在2008年发布的文章的博客,我们这样写代码:

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

为了选择有标题包含'Lennon'的文章和2008年发布的文章的博客,我们这样写:

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

假设只有一个博客有各满足其中一个条件的文章,而没有任何一个博客有同时满足两个条件的文章。第一个查询不会返回任何博客,而第二个查询会返回前面提到的那个博客。
在第二个例子中,第一个过滤器限制了查询集为所有连接到了标题含"Lennon"的文章的博客。第二个过滤器限制为连接到发布时间为2008年的文章的博客。第二个过滤器和第一个过滤器可能有也可能没有交集。每个过滤器过滤的是Blog元素,而不是Entry元素。


注意:filter() 的跨表行为和exclude() 是不同的。单个exclude() 查询不需要指同一个对象。
例如,下面的查询排除了标题包含"Lennon"的文章和2008年发布的文章:

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

然而,不像使用filter() ,这不会限制到满足两个条件的文章的博客,为了这么做,也就是说为了选择所有不包含满足两个条件的,需要做两次查询:

Blog.objects.exclude(
    entry__in=Entry.objects.filter(
        headline__contains='Lennon',
        pub_date__year=2008,
    ),
)
Filters can reference fields on the model

到目前为止,构造的过滤器都是字段值和常量比较。但是如果你想比较字段值和同模型中其它字段值呢?
Django提供了F expressions来允许这样的比较。F()的实例像是查询语句时模型字段的引用。这些应用可以用在查询过滤器中来比较同一个模型实例的不同字段的值的比较。
比如,为了找出所有博客文章中评论数多于转发数的,我们构造F()对象来引用转发的计数,然后在查询中使用F()对象:

>>> from django.db.models import F
>>> Entry.objects.filter(n_comments_gt=F('n_pingbacks'))

Django支持对F()对象的加减乘除求余和幂计算,参与计算的可以是常量和F()对象。为了找到评论数多于两倍转发数的文章的博客,修改查询语句:

>>> Entry.objects.filter(n_comments_gt=F('n_pingbacks') * 2)

为了找出所有文章排名小于转发和评论数之和的文章:

>>> Entry.objects.filter(rating__lt=F('n_comments')+F('n_pingbacks'))

也可以在F()对象中是用双下划线来跨表。一个含双下划线的F()对象会引入必要的联表操作来访问相关对象。。比如说,为了取得所有作者名和博客名相同的文章,我们可以:

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

对于日期和日期时间字段,你可以加减一个timedelta对象,下例会返回所有在发布后超过3天又修改的文章:

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

F()对象支持按位运算,通过.bitand().bitor()。例如:

>>> F('somefield').bitand(16)
The pk lookup shortcut

为了方便,Django提供一个pk查询快捷方式,pk代表"primary key"(主键)。
在示例的Blog模型中,主键是id字段,所以这三个语句是等价的:

>>> Blog.objects.get(id__exact=14)  # 显式形式
>>> Blog.objects.get(id=14)  # __exact是隐式的
>>> Blog.objects.get(pk=14)  # pk 暗示 id__exact

pk的使用并不仅限于__exact查询,所有的查询术语都能和pk组合形成主键的查询语句:

# 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)

pk查询也可以跨表,比如,这三个语句是等价的:

>>> Entry.objects.filter(blog__id__exact=3)  # Explicit form
>>> Entry.objects.filter(blog__id=3)  # __exact是隐式的
>>> Entry.objects.filter(blog__pk=3)  # __pk 暗示了__id__exact
Escaping percent signs and underscores in LIKE statements

等价于LIKESQL语句的字段查询(iexact,contains,icontains,startswith,istartswith,endswithiendswith)会自动转义LIKE语句里两个特殊的字符——百分号和下划线。(在LIKE语句中,百分号代表多个字符通配而下划线代表单个字符通配。)
例如,为获得所有包含百分号的文章,就像其他符号一样使用百分号就行了:

>>> Entry.objects.filter(headline__contains='%')

Django为你处理了引用部分;最终的SQL看起来是这样的:

SELECT ... WHERE headline LIKE '%\%%';

下划线也是同理。下划线和百分号的处理都是透明的。

Caching and QuerySet

每个QuerySet包含一个缓存来最小化数据库访问。理解他是如何工作的能帮你写出更有效率的代码。
在一个新创建的QuerySet中,缓存是空的。第一次QuerySet计算后,数据库查询就进行了——Django把查询结果保存到QuerySet的缓存中并且返回显式请求的结果。接下来的对这个QuerySet计算会使用缓存的结果。
踩坑举例,下例会创建两个QuerySet,计算,然后抛弃掉:

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

这意味着同个数据库查询执行了两次,加重了数据库负担。还有,有可能两个列表的数据记录可能是不相同的,因为Entry有可能在两次请求间做了增减。
为避免这个问题,保存QuerySet然后反复使用:

>>> queryset = Entry.objects.all()
>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])
When QuerySets are not cached

查询集并不总是缓存结果。当只计算查询集的一部分时,会查询缓存。但是如果没有填充完整,接下来的查询不会缓存。就是说,使用数组切片或索引的limiting the queryset不会填充缓存。
例如,重复获取查询对象的某个索引每次都会查询数据库:

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

然而如果整个查询集被计算过,就会检查缓存了:

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

下面是其它会引起查询集计算并填充缓存的情况:

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

注意:单纯地打印查询集不会填充缓存,因为调用__repr__()只返回整个查询集的一部分。

Complex lookups with Q objects

关键字查询语句——比如说filter() 里的——都是“AND”关系。如果需要更复杂的查询(比如说OR语句),使用Q objects
Q object(django.db.models.Q)是一个用来包装关键字参数的集合。这些关键字可以像上面的字段查询一样指定。
比如,这个 Q 对象包装了一个LIKE查询:

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

Q对象可以使用&|操作符结合。当在两个Q对象间使用操作符时,会产生一个新的Q对象。
比如说,接下来的语句产生了一个Q对象代表了两个"question__startswith"的"OR"关系:

Q(question__startswith='Who') | Q(question__startswith='What')

这相当于下面的SQL WHERE语句:

WHERE question LIKE 'Who%' OR question LIKE 'What%'

Q对象也能使用~操作符来表示否定,允许组合正常查询和一个否定(NOT)查询:

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

每个接收关键字参数的查询函数也能传送一个或不止一个Q对象作为位置参数。如果你给一个查询函数提供多个Q对象参数,参数间则为"AND"关系:

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

大致可以翻译成SQL语句:

SELECT * from polls WHERE question LIKE 'Who%'
    AND (pub_date = '2005-05-02' OR pubdate = '2005-05-06')

查询函数可以混合使用Q对象和关键字参数。所有提供给查询函数的参数都是"AND"关系。然而如果提供了Q对象,必须定义在关键字参数前:

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

上面是一个合法查询但是:

# 非法查询
Poll.objects.get(
    question__startswith='Who',
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

这就是个非法查询了。
亦见:Django单元测试里的OR lookups examples展示了Q的更多用法。

Comparing objects

为比较两个模型实例,可以使用标准的Python比较操作符,双等号:==。在后台,进行的是两个模型的主键值的比较。
使用Entry例,下面两个句子是等价的:

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

如果一个模型的主键不叫id,没关系。无论主键叫什么,总是会拿主键进行比较。比如,如果一个模型的主键字段叫做name,这两个语句是等价的:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name
Deleting objects

删除模型的方法叫做delete()。这个方法会立刻删除对象并返回对象名和一个字典,字典里时删除的每个对象类型的数量。比如:

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

返回值增加了删除对象的数量。
也可以批量删除对象。每个QuerySet有一个delete() 方法,删除所有QuerySet的成员。
例如,接下来会删除所有pub_date为2005的Entry对象:

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

记住,在可能的情况下,完全用SQL来操作,这样在进程中独立的对象实例就不会需要调用delete()方法。如果你为一个模型类提供了delete()方法并想保证它被调用了,你需要手动删除每个实例(通过迭代QuerySet并在每个独立对象上调用delete())而不是使用QuerySet的批量delete() 方法。
返回值增加了删除对象的数量。
当Django删除对象时,默认情况下它触发了SQL限制ON DELETE CASCADE行为——换句话说,对象被删除时,外键指向该对象的对象也会被删除。
例如:

b = Blog.objects.get(pk=1)
# 这会删除Blog对象和所有它的Entry对象
b.delete()

这个事务行为通过ForeignKeyon_delete参数定义。
注意delete() 是唯一的没有提供给ManagerQuerySet方法。这是防止意外请求Entry.objects.delete()删除了所有文章的安全机制。如果你确实想删除所有对象,可以显示请求一个完整的查询集:

Entry.objects.all().delete()
Copying model instances

尽管没有复制实例的内置方法,可以简单的复制所有字段值来创建新实例。在最简单的情况下,你可以把pk设为None就行了。使用博客例子:

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有个链接到AuthorManyToManyField的字段。在复制文章后,你必须手动为新文章设置多对多关系:

entry = Entry.objects.all()[0]  # 某个先前的文章
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()
Updating multiple objects at once

有时你想把QuerySet里的所有对象的某个字段设置为新的值。你可以通过update() 方法。比如:

# Update all the headline with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

使用这个方法你只能设置非关字段和ForeignKey字段。更新非关系字段时给新的值提供常量。更新ForeignKey的字段时,把新值设为想指定的新的模型实例。例如:

>>> b = Blog.objects.get(pk=1)

# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.all().update(blog=b)

update()方法立刻生效,并且返回匹配的行数(但不一定是更新的行数因为有的行值早已是新值了)。更新的QuerySet唯一的限制是它只能访问一个数据库表:模型主表。你可以基于关系字段来过滤,但是只能更新模型主表里的列,比如:

>>> 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语句。这是直接更新的批量操作。他不会运行模型的save() 方法,或是触发pre_savepost_save信号(他们是调用save() 的序列),或是接受auto_now() 字段选项。如果你想保存QuerySet里的每个项目并且确保每个实例都调用了save() 方法。不用特殊的函数来做这件事,只要循环然后调用save()

for item in my_queryset:
    item.save()

更新也可以使用F expression来基于同模型的另一个字段值来更新字段。这对于基于字段当前值来增加计数尤其有用。比如,为了增加博客中每个文章的转发数:

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

然而,不像F()在过滤和排除语句中那样,在更新中使用F()时你不能引入联表——你只能引用待更新的模型内部的字段。如果你想对F()引入联表,就会抛出FieldError异常:

# This will raise a FieldError
>>> Entry.objects.update(headline=F('blog__name'))
Related objects

当在模型中定义关系后,模型的实例有很方便的接口访问相关对象。
比如使用开头的模型,一个Entry对象e可以获得相关联的Blog对象通过访问blog属性:e.blog。
Django在关系的另一边也创建了接口——从相关模型到定义关系模型的。比如一个Blog对象b能访问所有相关的Entry对象的列表,通过entry_set属性:b.entry_set.all()。

One-to-many relationships
Forward

如果一个模型有一个ForeignKey,模型实例就能通过一个简单的模型属性访问相关的外部对象。
例如:

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

你可以获取和设置外键属性。直到调用save() 方法时才会改动外键。例子:

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

如果ForeignKey字段有null=True设置(也就是允许NULL值),你可以设置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)  # 没有访问数据库,使用的缓存版本

注意select_related() QuerySet方法提前递归的填充所有一对多关系的缓存:

>>> e = Entry.objects.select_related().get(id=2)
>>> print(e.blog)  # 没有访问数据库,使用缓存
>>> print(e.blog)  # 没有访问数据库,使用缓存
Following relationships "backward"

如果一个模型有ForeignKey,外键模型的实例将访问Manager来返回第一个模型的所有实例。默认情况下,这个管理器称为FOO_setFOO是原模型名,小写。这个Manager返回QuerySet,可以用上面描述的“取得对象”部分过滤和操作。
举例:

>>> b = Blog.objects.get(id=1)
>>> b.entry_set.all()  # 返回关联博客的所有文章入口

# b.entry_set is a Manager that returns QuerySets.
>>> 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 is a Manager that returns QuerySets.
>>> b.entries.filter(headline__contains='Lennon')
>>> b.entries.count()
Using a custom reverse manager

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

from django.db import models

class Entry(models.Model):
    #...
    objects = models.Manager()  # Default Manager
    entries = EntryManager()    # Custom Manager
    
b = Blog.objects.get(id=1)
b.entry_set(manager='entries').all()

如果EntryManager在它的get_queryset()方法上进行了默认的过滤,过滤会影响到all()的调用。
当然,指定一个自定义反向管理器能让你也是用它的自定义方法:

b.entry_set(manager='entries').is_published()
Additional methods to handle related objects

除了上面在“取回对象”中定义的QuerySet方法外,ForeignKey Manager还有其他的方法来处理关联对象集合。下面是个总览,完整列表可以在related objects reference中找到。\

  • add(obj1, obj2, ...) 向相关对象集合增加指定的模型对象。
  • create(**kwargs) 创建一个新的对象,保存并把它放进相关对象集合。返回新创建的对象。
  • remove(obj1, obj2, ...) 从相关对象集合中移除指定的模型对象。
  • clear() 从相关对象集合中移除所有对象。
  • set(objs) 替代相关对象集合。

为指定相关集合的的成员,使用set()方法,参数应该是可迭代的对象实例或者主键值的列表,比如:

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

在这个例子中,e1e2可以是完整的文章实例,也可以是整数主键值。
如果clear()方法可用,任何在所有可迭代对象中的对象加入集合前,先前存在的对象都会从entry_set移除。如果clear()方法不可用,所有可迭代对象中的对象会加入集合但是不移除先前存在的元素。
这一部分描述的每个“反向”操作都是立刻作用于数据库的。每个增加,创建,删除操作都是立刻自动存到数据库的。

Many-to-many relationships

多对多关系的两端都有自动化的接口来访问另一端。接口就像上面“逆向”的一对多关系一样进行。
唯一的区别在于属性名:定义ManyToManyField的模型使用的是字段名本身,而“反向”模型使用的是原模型的模型名的小写,追加'_set'。
举个例子可能更好理解些:

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.

就像ForeignKey, ManyToManyField可以指定related_name。在上面的例子中,如果Entry中的ManyToManyField制定了related_name='entries',那么每个Author实例就会有个entries属性而不是entry_set

One-to-one relationships

一对一关系和多对一关系很类似。如果你定义了一个OneToOneField,模型的实例能够通过一个简单的模型属性访问相关对象。
比如:

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.

区别在于反向查询。一对一关系的相关模型也能访问Manager对象,但是Manager代表了一个对象,而不是对象的集合:

e = Entry.objects.get(id=2)
e.entrydetail  # returns the related EntryDetail object

如果没有对象指定到关系中,Django会抛出DoesNotExist异常。
在逆向关系中指定实例的方式和正向的关系的指定一样:

e.enrtydetail = ed
How are backward relationships possible?

给定的一个模型是如何得知别的加载的模型是关联到自己的呢?
答案是app registry。当Django启动时,它导入INSTALLED_APPS中的每个应用,然后导入每个应用中的models模块。当一个新模型类创建时,Django向相关模型增加反向关系。如果相关模型还没被导入,Django保持跟踪关系并在相关模型最终导入时添加关系。
因此,所有你定义模型的应用都要在INSTALLED_APPS中。不然逆向查询无法正常工作。

Queries over related objects

包含相关对象的查询遵从普通字段的查询规则。当指定查询的值时,你可以使用对象实例本身,或是对象的主键值。
例如,如果你有一个博客对象bid=5,下面三个查询是等价的:

Entry.objects.filter(blog=b)  # Query using object instance
Entry.objects.filter(blog=b.id)  # Query using id from instance 
Entry.objects.filter(blog=5)  # Query using id directly
Falling back to raw SQL

Django 有很多写原生SQL查询的选项;见Performing raw SQL queries

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 原文:https://my.oschina.net/liuyuantao/blog/751438 查询集API 参...
    阳光小镇少爷阅读 3,809评论 0 8
  • Django 1.8.2 文档Home | Table of contents | Index | Modules...
    轩辕小爱阅读 2,343评论 0 2
  • 模块间联系越多,其耦合性越强,同时表明其独立性越差( 降低耦合性,可以提高其独立性)。软件设计中通常用耦合度和内聚...
    riverstation阅读 2,059评论 0 8
  • Django ORM用到三个类:Manager、QuerySet、Model。Manager定义表级方法(表级方法...
    廖马儿阅读 4,669评论 1 4
  • 某年某月某日 与某人初见 小城春日旧时光 可某个路口 丁香依旧
    笔耕翰墨阅读 243评论 0 2