模型层进阶相关

Posted yscl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模型层进阶相关相关的知识,希望对你有一定的参考价值。

模型层进阶相关

选择合适的层级工作

要在对应的level(MVC) 做对应的事. 例如计算 count, 在最低的数据库 level 里是最快的 (如果只需要知道此记录是否存在的话,用 exists() 会更快).
但要 注意: queryset 是 lazy 的,所以有时候在 higher level (例如模板) 里控制 queryset 是否真的执行,说不定会更高效.

下面这段代码很好的解释了不同 level 的意思:

# QuerySet operation on the database
# fast, because that's what databases are good at
# 执行效率最快, 属于数据库层级
my_bicycles.count()

# counting Python objects
# slower, because it requires a database query anyway, and processing
# of the Python objects
# 很慢, 属于在Python对象的级别层次处理
len(my_bicycles)

# Django template filter
# slower still, because it will have to count them in Python anyway,
# and because of template language overheads
# 仍然很慢, 模板层的本质还是需要在Python层面上进行数据的处理
{{ my_bicycles|length }}

理解QuerySet对象

切片

QuerySet可以支持切片语法, 这等同于SQL的limit和offset. 但是它不支持负数索引

print(models.Book.objects.all()[:4])
print(models.Book.objects.all()[4:8])

(0.002) SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 4; 
(0.000) SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 4 OFFSET 4; 

可迭代

QuerySet对象支持迭代.

for book in models.Book.objects.filter(pk__gt=5):
    print(book)

惰性查询

惰性查询是Queryset对象的一个比较重要的特性. 看下面这个例子

q = models.Book.objects.filter(name__startswith='西')
q = q.filter(pk__gt=5)
q = q.filter(pub_time__year=2018)

print(q)

这上面的例子看起来是对数据库进行了3次查询, 但是实际上只有执行打印的时候才真正查询数据库了. 创建查询集后只有我们需要获取具体的数据, 然后orm才会去数据库"请求"值给我们.

官方推荐写法:

q = models.Book.objects.filter(
    name__startswith='西'
).filter(
    pk__gt=5
).filter(
    pub_time__year=2018
)

那么什么才是后去具体数据的时机呢? 官方文档描述了下列几种情形.

  1. Iteration: ie. 对 Queryset 进行 For 循环的操作.
  2. slice: q = models.Book.objects.all()[5:10:2] 当指定了步长的切片才会马上去执行数据库查询.
  3. picling/caching
  4. repr/str
  5. len (Note: 如果你只想知道这个 queryset 结果的长度的话,最高效的还是在数据库的层级调用 count () 方法,也就是 sql 中的 COUNT ().)
  6. list()
  7. bool()

缓存机制

每个查询集都包含一个缓存来最小化对数据库的请求, 充分理解缓存的工作机制能帮助我们写出高效的代码.

当我们创建了一个新的查询集之后, 一旦发生了上面描述的7种情形, 就会在请求数据库之后, 可能生成cache(保存在查询集对象内),之后对相同的查询集做操作就不会重新去请求数据库获取数据了.

可以看看下面的结果

# 第一种方式
print([p.name for p in models.Publish.objects.all()])
print([p.addr for p in models.Publish.objects.all()])

# 第二种方式
q = models.Publish.objects.all()
print([p.name for p in q])
print([p.addr for p in q])

第一种方式实际是请求了两次数据库, QuerySet对象生成之后就直接弃用了, 缓存机制没有用上.

第二种方式只请求了一次数据库, 在第一次遍历QuerySet之后, 就将结果缓存起来了, 接下来就是对同一个QuerySet对象进行Python层面上的操作了.

会发生缓存的情形

[entry for entry in queryset]  # 遍历整个查询集
bool(queryset)                # 做布尔值运算
entry in queryset             # in运算
list(queryset)                # 转换成列表

特别要注意一下这些是不会发生缓存的.

q = models.Publish.objects.all()

print(q[2:])  # 做切片操作, 这里会查询数据库, 但不会将结果缓存到原来的查询集中
print(q[2])   # 做索引操作, 也会查询数据库, 也不会将结果缓存.

print(q)      # 这里单纯的打印不会发生缓存
print(q)

# values, values_list都不会发生缓存. 下面也会发生
print(q.values('name', 'addr'))
print(q.values('addr'))

查询优化

官方提供的几种优化策略

  • 利用 queryset lazy 的特性去优化代码,尽可能的减少连接数据库的次数.
  • 如果查出的 queryset 只用一次,可以使用 iterator () 去来防止占用太多的内存,
  • 尽可能把一些数据库层级的工作放到数据库,例如使用 filter/exclude, F, annotate, aggregate (可以理解为 groupby)
  • 一次性拿出所有你要的数据,不去取那些你不需要的数据.
    意思就是要巧用 select_related (), prefetch_related () 和 values_list (), values (), 例如如果只需要 id 字段的话,用 values_list (‘id‘, flat=True) 也能节约很多资源。或者使用 defer()only() 方法:不加载某个字段 (用到这个方法就要反思表设计的问题了) / 只加载某些字段.
  • 如果不用 select_related 的话,去取外键的属性就会连数据再去查找.
  • bulk (批量) 地去操作数据,比如 bulk_create
  • 查找一条数据时,尽量用有索引的字段去查询,O (1) 或 O (log n) 和 O (n) 差别还是很大的
  • count() 代替 len(queryset), 用 exists() 代替 if queryset:

下面再详细总结其中几种优化方式

对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用 select_related 来对QuerySet进行优化。

select_related 返回一个QuerySet,当执行它的查询时它沿着外键关系查询关联的对象的数据。它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询。

简单说,在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。

下面是它和普通查询的区别

# 普通查询
book = models.Book.objects.filter(pk=2).first()  # type: models.Book
print(book.publish.name)
SELECT
    `app01_book`.`id`,
    `app01_book`.`name`,
    `app01_book`.`pub_time`,
    `app01_book`.`publish_id` 
FROM
    `app01_book` 
WHERE
    `app01_book`.`id` = 2 
ORDER BY
    `app01_book`.`id` ASC 
    LIMIT 1;
    
SELECT
    `app01_publish`.`id`,
    `app01_publish`.`name`,
    `app01_publish`.`addr`,
    `app01_publish`.`pub_detail_id` 
FROM
    `app01_publish` 
WHERE
    `app01_publish`.`id` = 2;

上面的查询一共执行了两句sql语句.

使用select_related方法来执行查询的效率之比较.

books = models.Book.objects.filter(pk__lt=4).select_related('publish')
    
for book in books:
    print(book.publish.name)
SELECT
    `app01_book`.`id`,
    `app01_book`.`name`,
    `app01_book`.`pub_time`,
    `app01_book`.`publish_id`,
    `app01_publish`.`id`,
    `app01_publish`.`name`,
    `app01_publish`.`addr`,
    `app01_publish`.`pub_detail_id` 
FROM
    `app01_book`
    INNER JOIN `app01_publish` ON ( `app01_book`.`publish_id` = `app01_publish`.`id` ) 
WHERE
    `app01_book`.`id` < 4;

由于使用了select_related提前将字段关联, 后面的跨表查询并没有继续操作数据库.

select_related还支持连接多个外键, 可以通过一个外键字段一直关联下去. 下面就是跨了3张表

books = models.Book.objects.filter(pk__lt=3).select_related('publish__pub_detail')

for book in books:
    print(book.publish.pub_detail.email)

小结:

  1. select_related主要针一对一和多对一关系进行优化。
  2. select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
  3. 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询。
  4. 没有指定的字段不会缓存,如果要访问的话Django会再次进行SQL查询。

对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。

prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。

prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。

# 只查询了两次数据库
books = models.Book.objects.prefetch_related('authors')
for book in books:
    print(book.authors.all())

defer与only

only(*field): 返回一个对象, 只对括号内的字段属性做了查询优化

defer(*field): 返回一个对象, 对括号外的字段属性做了优化, 与only相反

上面依然可以获取优化之外的字段属性, 但是却需要进行数据库的查询获取.

books = models.Book.objects.only('name', 'pk')
books2 = models.Book.objects.values('name', 'pk')
books3 = models.Book.objects.defer('id')
print(books)
print(books2)
print(books3)
SELECT `app01_book`.`id`, `app01_book`.`name` FROM `app01_book` LIMIT 21;
SELECT `app01_book`.`name`, `app01_book`.`id` FROM `app01_book` LIMIT 21;
SELECT `app01_book`.`id`, `app01_book`.`name`, `app01_book`.`pub_time`, `app01_book`.`publish_id` FROM `app01_book` LIMIT 21;

从上面的执行sql语句可以上看出来, only和values执行的是一样的, 只是only返回的是列表套对象, 而values是列表套字典的形式. defer原理与only一样, 查询的是与only相反的数据. 所以如果只需要用到很少的数据, 又需要一个对象的形式, 就可以用到上面两个方法.

only, defer不能跨表优化, 就像下面这样, 有多少书, 就需要执行多少次数据库, 效率非常低下.

books = models.Book.objects.only('pk', 'publish')
for book in books:
    print(book.pk, book.publish.name)

事务优化

事务操作不仅能够保证数据的安全, 还有一个很有用过的作用就是, 可以通过事务隔离Django默认的autocommit, 来避免Django频繁的向数据库提交数据. 这也能够很好的提升性能.

在Django中开启事务的语法非常简单.

from django.db import transaction
with transaction.atomic():
    pass

批量操作

在QuerySet中有许多批量操作的方式, 例如delete update bulk_create...

这些批量操作对应于数据库层面的批量操作, 能够有效的防止批频繁请求数据库.

details = [models.PublishDetail(email=f'email{i}') for i in range(5)]
for d in details:
    d.save()

# 批量操作
models.PublishDetail.objects.bulk_create(details)

上面for循环5次, 需要请求数据库5次, 而bulk_create只需要请求数据库一次. 数据越多, 效率上的差距越明显.

以上是关于模型层进阶相关的主要内容,如果未能解决你的问题,请参考以下文章

使用Architecture Components构建的应用程序是否使其成为“MVVM”,如果是,应用程序的哪些部分与哪个MVVM层相关?

AJAX相关JS代码片段和部分浏览器模型

Python面试重点(进阶篇)

04 Django模型层: Django-model进阶

Django 进阶

django进阶