django模型查询操作

一旦创建好了数据模型,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(to=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)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()
    def __str__(self):
        return self.headline

1、创建对象

Django使用更直观的系统:模型类表示数据库表,该类的实例表示数据库表中的特定记录也就是数据值

要创建对象,请使用模型类的关键字参数对其进行实例化,然后调用save()以将其保存到数据库中,假设模型存在于mysite/app01/models.py文件中

from app01.models import Blog
b = Blog(name='beatles blog',tagline='all the latest beatles news.')
b.save()

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

#修改对象的值,在后台会执行一条SQL语句的UPDATE
b.name = 'python is django'
b.save()
Blog.objects.all()
<QuerySet [<Blog: python is django>]>

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

Blog.objects.create(name='Blog title',tagline='this is one blog title name')
Blog.objects.filter(name='Blog title')  #查询数据
<QuerySet [<Blog: name:Blog title;tagline:this is one blog title name>]>

2、保存ForeignKey和ManyToManyField字段

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

from app01.models import Blog,Entry
entry = Entry.objects.get(pk=1)
cheese_blog = Blog.objects.get(name='Blog title')
entry.blog = cheese_blog
entry.save()

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

from app01.models import Author
joe = Author.objects.create(name='Joe')
entry.authors.add(joe)

在对对多关系中可以一次性添加多条记录,只需在调用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)

#如指定了错误的类型对象,将引发异常

3、检索对象

想要从数据库中检索对象,需要基于模型类,通过管理器(Manager)构造一个查询结果集(QuerySet),每个QuerySet代表一些数据库对象的集合,它可以包含零个、一个或多个过滤器(filters),Filters缩小查询结果的范围,在SQL语法中,一个QuerySet相当于一个SELECT语句,而filter相当于WHERE或者LIMIT一类的子句

通过模型的Manager获得QuerySet,每个模型至少具有一个Manager,默认情况下,它被称作为objects,可以通过模型类直接调用它,但不能通过模型类的实例调用它,以此实现‘表级别’操作和‘记录级别’操作的强制分离,如下所示:

Blog.objects
<django.db.models.manager.Manager object at 0x000001E3583EFDD8>
b = Blog(name='FOO',tagline='Bar')
b.objects
Traceback (most recent call last):
......
AttributeError: Manager isn't accessible via Blog instances

 Managers只能通过模型类访问,不能通过模型实例访问,它强制表级操作和记录操作之间的分离

#检索所有对象,可使用all()方法,可获取某张表的所有记录
alll_entries = Entry.objects.all()

#过滤对象,有两种方法用来过滤QuerySet的结果:
filter(**kwargs) :返回一个根据指定参数查询出来的QuerySet
exclude(**kwargs) :返回给定查找参数不匹配的QuerySet
#例如:获取2006年的博客条目,使用filter()
Entery.objects.filter(pub_date__year=2006)
#使用默认的manager类,它等同于上
Entery.objects.all().filter(pub_date__year=2006)

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

Entry.objects.filter(headline__startswith='what').exclude(pub_date__gte=datetime.date.today()).filter(pub_date__gte=datetime.date(2005.1.30))

#它将获取数据库中所有条目中带'what'的首字母的QuerySet,然后排除,添加过滤器然后过滤是2005年1月30日的条目

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

QuerySets是懒惰的,一个创建QuerySet的动作不会立刻导致任何数据库的行为,你可以不断的进行filter动作,Django不会运行任何实际的数据库查询操作,直到QuerySet被提交(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语句时才执行数据库操作,通常QuerySet的检索不会立刻执行实际的数据库操作,知道出现类似print的请求,也就是evaluated

检索单个对象:filter方法始终返回的是QuerySet,哪怕只有一个对象符合顾虑条件,返回的也是包含一个对象的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方法要慎用!

其他QuerySet方法:大多数情况下,需要从数据库中查找对象时,使用all()、get()、filter()和exclude()就行,针对QuerySet的方法还有很多高级用法可以参考官网

QuerySet使用限制:使用类似于python对列表进行切片的方法可以对QuerySet进行范围取值,它相当于SQL语句中的LIMIT和OFFSET子句

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

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

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

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

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

Entry.objects.order_by('headline')[0]
#相当于:
Entry.objects.order_by('headline')[0:1].get()

#注意:如果没有匹配到对象,第一种方法会抛出IndexError异常,而第二种
#方法会抛出DoesNotExist异常,所以在使用get和切片时,需要注意查询结果的元素个数

字段查询:字段查询是指SQL WHERE子句内容的方式,也就是filter()、exclude()和get()等方法的关键字参数,其基本格式是:field__lookuptype=value(注意是双下划线)

#pub_date为字段名链接双下划线再指定查找类型的关键字参数
Entry.objects.filter(pub_date__lte='2006-01-01')
#相当于SQL语句:
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

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

#blog为外键模型
Entry.objects.filter(blog_id=4)
#如果你传递了一个非法的键值,查询函数会抛出TypeError异常

Django的数据库API支持20多种查询类型,下面介绍一些常用的:

#exact为模型类型,如果不提供查询类型,那查询类型就是这个默认的exact,精确匹配
Entry.objects.get(headline__exact="Cat bites dog")
#SQL语句:
SELECT ... WHERE headline='Cat bites dog';
#下面两个相当:
Blog.objects.get(id__exact=4)
Blog.objects.get(id=4)

#iexact:不区分大小写的完全匹配
Blog.objects.get(name__iexact='beatles blog')
SELECT ... WHERE name ILIKE 'beatles blog'

#contains:区分大小写
Entry.objects.get(headline__contains='Lennon')
SELECT ... WHERE headline LIKE '%Lennon%';

#icontains:不区分大小写
Entry.objects.get(headline__icontains='Lennon')
SELECT ... WHERE headline ILIKE '%Lennon%';

#in:包含在列表、元组或集合的,也可接受字符串(可迭代)
Entry.objects.filter(id__in=[1,3,4])

#gt:大于
Entry.objects.filter(id__gt=3)

#gte:大于或等于
Entry.objects.filter(id__gte=3)

#lt:小于
Entry.objects.filter(id__lt=3)

#lte:小于或等于
Entry.objects.filter(id__lte=3)

#startswith:区分大小写的开头
Entry.objects.filter(headline__startswith='Lennon')

#istartswith:不区分大小写的开头
Entry.objects.filter(headline__istartswith='Lennon')

#endswith:区分大小写的结尾;iendswith:不区分大小写的结尾
Entry.objects.filter(headline__endswith='Lennon')
Entry.objects.filter(headline__iendswith='Lennon')

#range:范围在内
import datetime
start_date = datetime.date(2005,1,1)
end_date = datetime.date(2005,12,31)
Entry.objects.filter(pub_date__range=(start_date,end_date))

#date:对于datetime字段,将值转换为日期,采用日期值查找
Entry.objects.filter(pub_date__date=datetime.date(2005,1,1))
Entry.objects.filter(pub_date__date__gt=datetime.date(2005,1,1))

#year:对于日期和时间字段,确切的年份匹配,需要整数年
Entry.objects.filter(pub_date__year=2005)
Entry.objects.filter(pub_date__year__gte=2005)

#iso_year:精确的ISO 8601周编年份匹配,需要整数年
Entry.objects.filter(pub_date__iso_year=2005)
Entry.objects.filter(pub_date__iso_year__gte=2005)

#month:日期和日期时间字段,确切的月份匹配
Entry.objects.filter(pub_date__month=1)
Entry.objects.filter(pub_date__month__gte=5)

#day:确切的天匹配
Entry.objects.filter(pub_date__day=3)
Entry.objects.filter(pub_date__day__gte=3)

#week:日期和时间字段,根据ISO-8601返回周数
Entry.objects.filter(pub_date__week=52)
Entry.objects.filter(pub_date__week__gte=32,pub_date__week__lte=38)

#week_day:匹配星期几,从1(星期日)到7(星期六)的整数值
Entry.objects.filter(pub_date__week_day=2)
Entry.objects.filter(pub_date__week_day__gte=2)

#quarter:匹配四季,取1到4之间的整数值
Entry.objects.filter(pub_date__quarter=2)

#time:对于datetime字段,将值转换为时间,取一个datetime.time值
Entry.objects.filter(pub_date__time=datetime.time(14,30))

#hour:对日期时间和时间字段,精确的小时匹配,取值0-23
Entry.objects.filter(timestamp__hour=11)

#minute:对于日期时间和时间字段,精确分钟匹配
Entry.objects.filter(timestamp__minute=29)

#second:对于日期时间和时间字段,确切的妙匹配
Entry.objects.filter(timestamp__second=31)

#isnull:采用任一True或False,其对应于SQL查询IS NULLIS NOT NULL
Entry.objects.filter(pub_date__isnull=True)
SELECT ... WHERE pub_date IS NULL

#regex:区分大小写的正则表达式匹配,语法为python re模块语法
Entry.objects.filter(headline__regex=r'^(an?|The)+')

#iregex:不区分大小写的正则表达式匹配
Entry.objects.filter(headline__iregex=r'^(an?|the)+')

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

#先指定字段名然后双下划线分隔指定外键字段名
Entry.objects.filter(blog__name='Blog title')

如果要引用一个反向关联只需要使用模型的小写名即可:

#获取所有的blog对象,但前提是所关联的Entry模型的headline字段包含'django models'
Blog.objects.filter(entry__headline__icontains='django models')

如果多级关联种进行过滤而且其中某个模型没有满足过滤条件的值,Django将把它当作一个空(null)但是合法的对象,不会抛出任何异常或错误:

#查询Blog的所有对象,但关联entry模型种authors字段关联名必须时py.qi
Blog.objects.filter(entry__authors__name='py.qi')

如果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='django models',entry__pub_date__year=2019)

这是一个跨外键、两个过滤参数的查询,此时我们理解两个参数之间属于‘and’的关系,也就是说,过滤出来的Blog对象对用的entry对象必须满足上面两个条件,但是,我们看下面的用法:

Blog.objects.filter(entry__headline__contains='django models').filter(entry__pub_date__year=2019)

把两个参数拆分,放在两个filer调用里面,安装我们前面说过的链式过滤,这里的结果和上面的例子应该一样,可实际上,它不一样,Djang在这种情况下,将两个filter之间的关系设计为'or',多对对关系下的多值查询和外键foreignkey的情况一样。

但是,exclude的策略设计又和filter不一样:

Blog.objects.exclude(entry__headline__contains='django models',entry__pub_date__year=2019)

它会排除headline中包含‘django models'的entry或在2019年发布的Entry,中间是一个'or'的关系

那要排除同时满足上面两个条件的对象,应该这么做:

Blog.objects.exclude(entry__in=Entry.objects.filter(headline__contains='django models',pub_date__year=2019,),)

使用F表达式引用模型的字段:

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

使用Django提供的F表达式;例如:为了查找commnets数目多于pingbacks数目的Entry,可以构造一个F()对象来应用pingback数目,并在查询中使用该F()对象:

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

Django支持对F()对象进行加、减、乘、除、取模以及慕运算等算术操作,两个操作数可以是常数和其他F()对象,如:查找comments数目比pingbacks两倍还要多的Entry,可以这样写:

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

查询rating比pinggback和comments数目综合还要小的Entry:

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

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

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

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

Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

F()对象还支持:.bitand()、.bitor()、.bitrightshift()和.bitleftshift()4种位操作:

F('somefield').bitand(16)

主键的快捷查询方式:pk

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

Blog.objects.get(id__exact=1)
Blog.objects.get(id=1)
Blog.objects.get(pk=1)

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

Blog.objects.filter(pk__in=[1,3,4])
Blog.objects.filter(pk__gt=10)

可以跨表操作:

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

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

Entry.objects.filter(headline__contains='%')
#SQL语句
SELECT ... WHERE headline LIKE '%\%%';

4、缓存和查询集

每个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[2]) #再次查询数据库

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

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

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

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

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

5、使用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关系:

from app01.models import Entry
from django.db.models import Q
from datetime import date
Entry.objects.get(Q(headline__startswith='a'),Q(pub_date=date(2005,1,1)) | Q(pub_date=date(2019,1,1)))

#它相当于SQL的
SELECT * FROM Entry WHERE headline LIKE 'a%' AND (pub_date =  '2005,1,1' OR pub_date = '2019,1,1)

当关键字参数和Q对象组合使用时,Q对象必须放在前面,否则会报错

Entry.objects.filter(Q(pub_date=date(2005,4,2)) | Q(pub_date=date(2019,4,2)),headline__startswith='a')

#如果将headline__startswith='a'放在前面将会报错

6、比较对象

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

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

如果模型的主键不叫'ID'也没关系,后台总是会使用正确的主键名字进行比较,如下:主键名为'name‘时,下面的方法一样

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

7、删除对象

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

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

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

Entry.objects.filter(pub_date__year=2005).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()

8、复制模型实例

虽然没有内置的方法用于复制模型的实例,但还是很容易创建一个新的实例并将原实例的所有字段都拷贝过来,最简单的方法是将原实例的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()

9、批量更新对象

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

# 更新所有2007年发布的entry的headline
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

只可以对普通字段和ForignKey字段使用这个方法,若要更新一个普通字段,只需提供一个新的常数值,若要更新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语句,并立刻批量执行,它不会运行模型的save()方法,或者产生pre_save或post_save信号(调用save()方法产生)或者服从auto_now字段选项;如果你想保存QuerySet中的每个条目并确保每个实例的save()方法被调用,你不需要使用任何特殊的函数来处理,只需要迭代它们并调用save()方法:

for item in my_queryset:
    item.save()

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

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

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

# THIS WILL RAISE A FieldError
 Entry.objects.update(headline=F('blog__name'))

10、关系的对象

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

1)一对多外键

正向查询:

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

e = Entry.objects.get(id=2)
e.blog

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

e = Entry.objects.get(id=2)
other_blog = Blog.objects.get(name='django')
e.blog = other_blog
e.save()

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

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

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

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=7)  
b.entry_set.all()  #Returns all Entry objects related to Blog
b.entry_set.filter(headline__contains='Every')  #b.entries is a Manager that returns QuerySets
b.entry_set.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()

处理关联对象的其他方法:

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

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

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

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

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

set(objs):重置关联的对象集。

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

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

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

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

如果clear()方法不可用,那么将直接添加可迭代对象中的成员而不会删除所有已存在的对象

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

2)多对多

多对多关系的两端都会自动获取访问另一端的API,这些API的工作方式与前面提到的‘反向’一对多关系的用法一样,唯一的区别在于属性的名称,定义ManyToMangField的模型使用该字段的属性名称,而‘反向’模型使用源模型的小写名称加上‘_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.

与外键字段中一样,在多对多的字段中也可以指定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

11、使用原生SQL语句

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

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

 

posted @ 2019-04-18 15:26  Py.qi  阅读(8413)  评论(0编辑  收藏  举报