Alan Hou的个人博客

Odoo 12开发者指南第二十一章 性能优化

全书完整目录请见:Odoo 12开发者指南(Cookbook)第三版

借助于Odoo框架,你可以开发大型且复杂的应用。任何项目成功的关键是良好的性能。本章中,我们将探讨你需要来优化性能的套路和工具。本章中包含的各节是为提升ORM级别的性能,而非客户端或部署端的性能。

本章中,我们将讲解如下小节:

记录集的预提取模式

在通过记录集访问数据时,它对数据库进行查询。使用预提取模式,可以通过批量提取数据在减少查询的次数。

如何实现…

在操作多记录集时,预提取有助于减少SQL查询的次数。它通过一次提取所有数据来实现。通常,Odoo自动使用预提取,但在某些情况下会无法使用这一功能。本节中我们将探讨如何有效使用预提取。

下例是一种常规的计算方法。在这一方法中,self是多条记录的记录集。在这种模式中,预提取可完美运作。查看运行原理版块的第一段来了解更多知识:

在某些情况下,预提取变得更为复杂,如通过browse方法提取数据的时候。在下例中,我们在for循环中逐一浏览记录。这不会有效使用预提取并且它会比平常执行更多的查询:

通过向browse方法传递ID列表,你可以创建多条记录的记录集。如果对这个记录集执行操作,预提取和完全正常运行:

运行原理…

预提取极大地提升了ORM的性能。让我们来探讨预提取底层的运行原理。

在通过for循环对记录集进行遍历并在第一个迭代中访问字段值时,预提取就开始它的魔力之旅了。不是在迭代中针对当前记录获取数据,预提取会获取所有记录的数据。这背后的逻辑是你在for循环中访问字段,而你又很可能会在迭代中获取下一条记录的数据。在for循环的第一次迭代中,预提取会获取所有记录集中的数据并将其保存在缓存中。在for循环的下一次迭代中,数据会从这个缓存中提供,而不是进行新的SQL查询。这会将查询数从O(n)减少为O(1)。

我们假定记录集中有10条记录。当你处于第一次循环并访问记录的name字段时,它会获取所有10条记录的数据。这不仅仅是针对name字段,它还会获取这10条记录的所有记录。在后续的for循环迭代中,数据会从缓存中进行提供。这会将查询的次数由10次减少到1次。

注意即使这些字段没有在for循环体中使用,预提取也会获取所有字段的值(除*2many字段外)。这是因为获取额外字段相比对每个字段进行多次查询而言对性能的影响微乎其微。有时预获取的字段会降低性能。在这些情况下,你可以通过在上下文中向预提取传递字段列表来控制预提取的字段。实现这一功能,你需要像下面这样在上下文中传递prefetch_fields:

如果你想要禁用预提取,可以在prefetch_fields的上下文中传递False,如下:

ℹ️如果想要知道当前记录集的预提取上下文,可以使用recordset._prefetch 属性。它包含一个字典,以模型名为键,记录ID列表为值。

扩展知识…

如果想要分割记录集,ORM会使用一个新的预提取上下文生成新的记录集。对这种记录集进行操作仅会为它们各自的记录预提取数据。如是想在这个prefetch后预提取所有的记录,可以通过在with_prefetch() 方法中传递这个prefetch字典进行实现。在给定的示例中,我们将记录集分割为两部分。这里,我们在两个记录集中传递了一个共同的预提取上下文,因此在从其中某一个获取取数据时,ORM会对另一个获取数据并将数据放到缓存中以供未来使用:

小贴士:预提取上下文不仅限于分割记录集。你也可以使用with_prefetch() 方法来在多条记录集之间拥有共用预提取上下文。这表示在你从一条记录提取数据时,它也会从所有其它记录集中提取数据。

内存内缓存 – ormcache

Odoo框架提供ormcache装饰器来管理内存内缓存。本节中,我们将探讨如何为你的函数管理缓存。

如何实现…

这个ORM缓存类可在/odoo/tools/cache.py中进行查看。为在任意文件中使用它们,你需要像下面这样进行导入:

在导入这些类之后,可以使用ORM缓存装饰器。Odoo提供了不同类型的内存内缓存装饰器。我们将在下面的各块中了解其中的各个装饰器。

ormcache

这是最简单也最常用的缓存装饰器。你需要传递方法输出所依赖的参数名。以下为使用ormcache装饰器的示例方法:

在你初次调用这个方法时,它会进行执行并返回结果,ormcache会根据mode参数的值来存储这一结果 。再次使用相同的mode值调用该方法时,结果就会在不实际执行方法的情况下通过缓存来提供。

有时,你的方法的结果依赖于环境属性。在这些情况下,可以声明方法如下:

本例中给出的方法会根据环境用户和mode参数的值存储缓存。

ormcache_context

这种缓存与ormcache的运作非常相似,不同在于它依赖于参数加上下文中的值。在这种缓存装饰器中,你需要传递参数名和一个上下文键列表。例如,如果你的方法输出依赖于上下文中的lang和website_id键,可以使用ormcache_context:

ormcache_multi

有些方法对多条记录或ID执行操作。如果你希望对这类方法添加缓存的话,可以使用ormcache_multi装饰器。需要传递multi参数,并且在方法调用过程中,ORM会通过迭代这一参数生成缓存键。在这个方法中,你会需要以字典格式返回结果,其中multi参数的元素作为键。看一下如下示例:

假定我们使用 [1,2,3]作为 ID 来调用前面的方法。该方法返回结果的格式为{1:… , 2:…, 3:… }。ORM会根据这些键来缓存结果。如果你以 [1,2,3,4,5] 作为 ID 来进行另一次调用,你的方法会在ID参数中接收到[4, 5],因此该方法会针对 4,5 ID来执行操作,并且剩下的结果会通过缓存来进行提供。

运行原理…

ORM缓存以字典格式(缓存查询)在缓存中保存。这个缓存的键将根据所装饰方法的签名来生成,值为结果。简单地说,在你使用x, y 参数调用该方法时且该方法的结果为x+y,缓存查询即为{(x, y): x+y}。这表示下一次你使用相同参数调用这个方法时,结果会通过这个缓存直接进行提供。它节约了计算时间并且让响应更为快速。

ORM缓存是一种内存内缓存,因此它在RAM中进行存储并占用内存空间。不要使用ormcache来提供大型数据,如图片或文件。

小贴士:使用这一装饰器的方法永远不应返回一个记录集。如果这么做,它们会因底层的记录集游标关闭而产生psycopg2.OperationalError。

你应该对纯函数使用ORM缓存。纯函数是一种对相同参数一直返回相同结果的方法。这类方法的输出仅依赖于参数,因此它们会返回相同的结果。如果情况不是如此,你就需要在执行让缓存状态无效的操作时手动清除缓存。调用clear_caches()方法来清除缓存:

扩展知识…

ORM缓存是一种最近最少使用(LRU)缓存,这表示如果缓存中的一个键不被经常使用,它就会被删除。如果不恰当地使用ORM缓存,它可能会弊大于得。如果想要了解缓存是如何执行的,可以向Odoo进程传递SIGUSR1信号:

此处的2697是进程ID。在执行这条命令后,你将可以在日志中查看ORM缓存的状态:

如果缓存的查找成功率过你,应当在方法中删除这个ORM缓存。

生成图像缩略图

大图对于网站会是一个问题。它们增加了网页的大小并让最终让网站加载速度变慢。这会导致很差的SEO排名及增加用户放弃率。本节中,我们将探讨如何创建不同大小的图像。

如何实现…

Odoo中,所有的图像工具都放在odoo/tools/image.py 文件中。你需要添加import语句来使用这些图像工具:

图像工具中的image_resize_images()方法有助于我们管理三种不同大小的图像。要实现这个功能,你需要添加三个二进制字段来存储不同大小的图像:

在用户通过表单视图上传了图像时,上传的为完整尺寸。我们需要重载create和write方法来将这个图像转化为三种不同的尺寸:

这会调整所上传图像的大小,并将它们保存在image, image_medium和image_small字段中。

运行原理…

Odoo使用PIL库来处理图像。默认image_resize_images()方法将实际图像的大小调整为1024 x 1024 px, 128 x 128 px和64 x 64 px三种尺寸。然后它们会分别保存到image, image_medium和image_small三个字段中。但这一行为可进行自定义。看一下image_resize_images()的方法声明:

在给出的方法声明中,vals是write或create方法所发送字典值。这个字典将包含实际的图像。big_name, medium_name和small_name参数用于定义调整过尺寸的图像将返回的字段名。sizes参数用于修改默认图像大小。假设你想要使用自定义大小将图像保存为image_lg, image_md和image_sm字段。可以调用如下方法来进行实现:

这会将图像保存为不同字段及不同的尺寸。

扩展知识…

还有一些其它方法可作为图像工具。以下是一个方法列表及它们的使用方法:

如果想要学习这些方法的更多内容,可参见odoo/tools/image.py。

访问分组数据

在想要进行数据统计时,经常需要对数据进行分组,如月销售报表或显示每个客户销售状况的报表 。搜索记录来手动分组会非常耗时。本节中,我们将探讨如何使用 read_group()方法来访问分组数据。

如何实现…

read_group()方法广泛用于数据统计和智能统计按钮。假设你想要在伙伴表单中显示销售订单的数量。这可通过搜索某客户的销售这个问题然后对长度计数来实现:

这个示例可以运行,但不够优化。当我们在列表视图中显示so_count字段时,它会为列表中的所有客户获取并过滤销售订单。对于这个少量的数据, read_group()方法并不会带来质的变化,但在数据量上升时这就是一个问题了。我们可以使用read_group方法来解决这一问题。下例中我们将做和前例同样的事情,但即使用对大型数据集它也仅消耗一次SQL查询:

运行原理…

read_group()方法在内部使用SQL的GROUP BY功能。这会让GROUP BY 方法即使数据集很大时也更为快速。内部,Odoo的网页客户端在图表及分组列表视图中使用这一方法。可以通过使用不同的参数来调整read_group方法的行为。

让我们来探讨下read_group方法的声明:

针对read_group方法的不同可用参数如下:

ℹ️如果想要学习PostgreSQL聚合函数更多的知识,请参见文档:https://www.postgresql.org/docs/current/functions-aggregate.html。

以下是read_group方法所支持的参数列表:

扩展知识…

对date字段分组会比较复杂,因为可以根据日、周、季度、月或年来对记录进行分组。可以通过在groupby参数的 : 后传递groupby_function来改变日期的分组行为。如果想要对销售订单的月汇总进行分组,可以使用下面的read_group方法:

创建或写入多条记录

如果你是Odoo开发的新手,可能会执行如条查询来写入或创建多条记录。本节中,我们将了解如何批量创建和写入记录。

如何实现…

Odoo v12添加了对批量创建记录的支持。如果你在创建单条记录,只需要传入带有字段值的字典即可。而批量创建记录也只需要传递这些词典的列表来代替原来的单个字典。下例中使用单个create调用创建了三条图书记录:

写操作在记录集上执行。如果在多条记录集中写入相同数据应避免在for循环中使用写操作:

如果你在写入单个值,可以通过self.name = ‘Admin’这样的方式赋值来写入数据。查看如下示例来了解写入操作的正确用法:

运行原理…

要批量创建多条记录,你需要以列表的形式传递值字典来新建记录。这会自动管理批量创建的记录。在批量创建记录时,内部会为每条记录插入一个查询。这表示批量的记录创建不是由单条查询实现的。但这并不意味着批量创建不会提升性能。性能的提升是通过批量计算计算字段来实现的。

对于write方法则不同。在对多条记录执行写操作时,它是通过单条查询来实现的。在create方法中,可以为记录传递不同值,但在write方法中,你能在对所有记录写入相同值时使用批量写操作。

扩展知识…

如果你想要复制已有记录,可以使用copy()方法如下。内部这个方法获取数据,然后调用create方法。这个方法会返回带有复制的值的新记录集:

通过数据库查询访问记录

Odoo ORM拥有有限的方法,并且有时通过ORM来获取某些数据会非常困难。在这些情况下,可以按想要的格式获取数据并需要对数据执行运算来获取特定的结果。因此,速度会变慢。要处理这些特殊情况,可以在数据库中执行SQL查询。本节中我们将探讨如何通过Odoo运行SQL查询。

如何实现…

可以通过self._cr.execute方法来执行数据库查询:

查询的结果格式为元组列表。元组中的数据和查询中字段顺序相同。如果想要以字典格式获取数据,可以使用dictfetchall()方法。参见下例:

如果希望仅获取单条记录,可以使用fetchone() 和dictfetchone()方法。这些方法与fetchall()和dictfetchall()相似,但仅会返回单条记录,如果想要获取多条记录则需要对它们进行多次调用。

运行原理…

通过记录集有两种访问数据库游标的方式:一种是通过记录集本身,例如self._cr,另一种是通过环境,例如self.env.cr。游标用于执行数据库查询。在前例中,我们看到了如何通过原生查询语句获取数据。表名是将模型名中的 . 替换为 _ 之后的名称,因此library.book模型变成了library_book。

在执行原生查询前需要考虑一些事项。仅有没有其它选择时使用原生查询。通过执行原生查询,你跳过了ORM层。因此也跳过了安全规则以及丧失了ORM的性能优势。有时错误的查询会带来SQL注入的风险。考虑以下示例,其中查询会允许攻击者执行SQL注入:

也不要使用字符串格式函数,这会允许攻击者执行SQL查询。使用SQL查询会让其他开发者更难阅读和理解你的代码,因此应尽可能地避免使用。

小贴士:有些Odoo开发者认为执行SQL查询会让操作更快速,因为它绕过了ORM层。但并非完全如此,取决于具体的情况。在某些操作中,执行ORM要比原生查询要更好且更快,因为数据从记录集缓存中进行提供。

扩展知识…

一个事务中的操作仅在事务结束时才执行提交。如果在ORM中发生错误,事务会进行回滚。如果你使用INSERT或UPDATE查询并且想让其持久化,可以使用self._cr.commit()来提交这个修改。

ℹ️注意使用commit() 可能带来风险,因为它将记录置于不连续的状态。ORM中的错误会导致不完整的回滚,因此仅能非常清楚的情况下使用commit()。

Python代码性能测试

有时,你会无法定位到问题的原因。尤其是对于性能问题更是如此。Odoo提供了一些内置的性能测试工具来帮助你发现问题的真实原因。

如何实现…

Odoo的性能分析工具在odoo/tools/profiler.py中。要在代码中使用这个工具,需要在文件中进行导入:

在导入之后,可以对方法使用profile装饰器。要对指定方法进行性能测试,需要对其添加profile装饰器。参见下例。我们对make_available方法添加了profile装饰器:

因此,在调用这个方法时,可以在日志中打印出完整的统计数据:

运行原理…

在对你方法添加profile装饰器之后,调用该方法时Odoo会在日志中打印完整的统计数据,如前例所示。它会在三栏中打印统计数据。第1行包含调用的次数或某行执行的次数。(这个数据在某行代码位于for循环内或方法是递归的时会逐渐增加)。第2栏表示给定行中查询的次数。最后一栏是给定行所花费的毫秒数。注意该栏中显示的时间为相对值,在关闭掉性能调试工具后它会更快速。

profiler装饰器接收一些可选参数,这帮助我们获取方法的具体统计数据。以下是profile装饰器的定义:

下面是profile()方法所支持的参数列表:

扩展知识…

Odoo中的性能工具的另一种类型是为执行的方法生成图形。这个性能工具在misc包中,因此需要从那里进行导入。它会生成一个进一步生成图形文件的包含统计数据的文件。要使用这个性能工具,你需要传递文件路径来作为参数。在调用该函数时,它会在给定位置生成一个文件。参见下例,它会在桌面生成一个make_available.prof文件:

在调用make_available方法时,它会在桌面生成一个文件。要将这个数据转化为图形数据,需要安装gprof2dot工具,然后执行给定的命令来生成图形:

这条命令会在桌面上生成prof.xdot 文件。然后,你可以使用下面的命令来通过xdot显示图形:

以上的xdot命令会生成下图中的图形:

TODO

这里你可以放大、查看调用栈,并查看方法执行时间的详情。

退出移动版