【译】使用ORM/JPA框架时,提升性能的9个优化技巧转载
为什么在面向对象编程范式中开发程序时,需要使用ORM框架?原因有二:
-
关系世界与对象世界不直接兼容。
-
RDBMS(关系数据库管理系统)的多样性,让将对象转换为每个RDBMS期望的特定SQL命令变得更复杂了。
那么,是否只要有一个像Hibernate或EclipseLink这样的ORM框架,就能完全解决以上问题吗?
也不一定,解决一样问题,总是会带来新问题,这些框架的使用自然也会产生新的问题——性能问题。
之前从事的一个项目就有性能问题,由于开发人员一开始的不重视,后来不得不重构。
所以为了防止在生产中需要重构或意外,建议在使用JPA提供商或直接使用ORM框架开发应用程序时采取以下做法:
在关联中使用默认FetchMode值
当你不希望浪费时间评估应用最佳策略时,建议使用JPA中的默认FetchMode值,其中关系类型一对一或多对一(非集合字段/属性)必须映射为EAGER,关系类型一对多和多对多(集合字段/属性)必须映射为LAZY。
如果想优化关系映射,可以考虑使用惰性策略,ORM每个查询执行多个SQL语句,并使用急切策略,ORM可以加载比真正需要的数据多更多。
当然最好的解决方案是评估和平衡每种策略的缺点。
定义批处理大小和获取大小
ORM框架提供配置来定义批处理大小和获取大小。这些配置允许在执行SELECT和INSERT等语句时进行优化。
以Hibernate为例,可以定义以下配置属性:
-
hibernate.default_batch_fetch_size:Hibernate批次获取关联的默认大小。当你配置了带有获取模式LAZY的关联,或者你在查询中不使用JOIN FETCH时,此属性适用。
示例1:当批处理大小配置值为值1时,1个与包含100行和1个关联的表相关的1个查询将产生100个SQL语句。
示例2:当批处理大小配置为值4时,1个与包含100行和1个关联的表相关的查询将产生25个SQL语句。 -
hibernate.jdbc.fetch_size:非零值通过调用Statement.setFetchSize()来确定JDBC获取大小。当你想在内存中加载更多数据时,此属性适用。此属性的值越高,与服务器的通信就越低,从而获得更好的性能。但要小心,高值将在应用程序中消耗更多内存,并可能导致OutOfMemoryError异常,个人认为10到50之间的值是合理的。
-
hibernate.jdbc.batch_size:非零值导致Hibernate使用JDBC2批处理更新。当你想要执行一批INSERT命令时,此属性适用。但要小心,我不建议使用JPQL来执行大量INSERT命令。在这种情况下,尽可能使用本机查询来减少开销。
使用新运算符编写查询
当你编写查询并仅在SELECT子句中声明实体时,ORM框架将加载该表表示的所有列以及使用JOIN声明的关联的所有列。此外,所有未使用JOIN声明的关联都将从分开的查询中加载,并将被视为SELECT子句中的所有列。
为了减少不必要的数据网络中的流量,并减少应用程序服务器中存储的每项请求到RDBMS的数据量,我建议使用NEW运算符。
此运算符允许我们仅声明我们需要通过实体或DTO(数据传输对象)类的构造函数加载的字段/列。
示例:
考虑一下实体Foo,有两个一对多的关联,称为bar和baz。该实体有15个字段映射到RDBMS。
如果您只需要检索3个字段,你可以编写以下优化查询:
SELECT NEW Foo(f.field1, f.field2, f.field3)
FROM Foo f JOIN f.bar bar
WHERE f.field10 = :valueToField10 AND bar.id = :valueToBarId
使用JOIN FETCH为协会写查询
当您编写查询以检索与实体关联的一些字段值时,我建议使用JOIN FETCH语句。使用此语句可以减少ORM框架为实现预期结果而生成的查询数量,甚至可以减少到只有一个查询。
示例:
考虑一下实体Foo,有两个一对多的关联,称为bar和baz。
如果您只需要在一个查询中检索实体Foo的所有关联,您可以编写以下查询:
SELECT DISTINCT f FROM Foo f JOIN FETCH f.bar bar JOIN FETCH f.baz baz
WHERE f.field10 = :valueToField10 AND bar.id = :valueToBarId
注意:包含DISTINCT命令是为了消除由于将JOIN FETCH用于集合所代表的关联而可能出现在结果中的实体的重复。
使用分页查询
当你执行查询以返回一组行而不指定分页参数时,ORM框架将检索与查询相关的所有行(实体),并将其存储在内存中,如果返回未被充分利用,除了产生高内存消耗外,还会导致不必要的开销。
为了减轻或消除这种开销,请考虑JPA规范,使用接口Query或TypedQuery提供的以下方法:
-
setFirstResult(int):期望一个参数指示页面的偏移量。
-
setMaxResults(int):期望一个参数指示页面的极限。
因此,可以使用这些方法来限制查询结果,减少使用的内存量,并减少应用程序和RDBMS之间的网络流量。
示例:
想象一下,你写了一个与100万行的表相关的查询。如果你在WHERE子句上没有任何条件的情况下执行此查询,那么将检索内存中的100万行,其结果就是在应用程序中产生大量开销。
但是,如果你在具有分页参数的相同条件下执行此查询,你就可以控制返回并存储在内存中的行数,在我看来,这是最佳做法。
使用二级缓存(L2/2LC)
默认情况下,一级缓存(L1/1LC)由JPA规范或ORM框架启用。这种类型的缓存适用于每个事务,即缓存在每个事务期间可用,并允许减少对RDBMS的访问。但是,如果你想进一步减少RDBMS的访问次数,则需要使用二级缓存(L2/2LC)。
考虑到一个或多个应用程序服务器,此类缓存用于跨多个事务(EntityManagers)工作。
起初,2LC可以被视为很多;然而,它背后有一些问题需要考虑,如下所述:
-
ORM只读或插入的对象不应该是一个问题。
-
ORM通过其他应用程序或服务器更新或删除的对象可能会导致这些对象过时,即变得不一致。
因此,我建议首先应用2LC来攻击不受不一致影响的问题,并在导致不一致的问题之后,对应用程序操纵的涉及RDBMS对象进行完整评估。
要通过JPA规范启用2LC,请参阅此处的以下说明。
要通过Hibernate启用2LC,请参阅此处的以下说明。
注意:作为缓存的灵活性,ORM框架通常比JPA规范更强大。
尽可能使用本机查询
当你编写复杂查询时,主要是您的应用程序只需要在一个RDBMS供应商中工作,例如PostgreSQL,Oracle数据库,我建议你使用命名的本机查询或本机查询来减少JPQL解析器产生的开销。如果你的应用程序需要与多个RDBMS供应商合作,如果查询是在SQL ANSI中编写的,也可以应用此应用程序。
还有其他方法可以在不通过ORM框架的情况下减少开销,例如:使用缓存准备的语句设置数据源。
禁用SQL调试
在生产环境中,配置为false用于在控制台上显示SQL语句的属性的值。这并不是什么新鲜事,但我认为注册很重要,不要让它被遗忘。
与绩效相关的其他做法
-
在RDBMS上索引表列以提高性能。
-
分析执行计划,以确定如何优化你的查询。
-
使用特定工具对实体字段进行索引,以便在以下情况下更快地搜索:全文和地理位置。请参阅冬眠搜索。
结论
ORM框架功能强大,大大减少了我们的工作,但必须负责任地使用它正在构建的内容,以便在应用程序实际交付生产时不影响其性能。
原文作者: