经常在网上看到开发者们抱怨 JPA 性能低下的帖子或文章,但如果仔细查看这些性能问题,常会发现导致问题的根本原因大致包括以下几个:

  • 使用过多的 SQL 查询从数据库中获取所需的实体信息,即我们常说的n+1查询问题

  • 逐个更新实体,而不是使用单条语句进行更新

  • 使用 Java 应用程序而非数据库进行大量数据处理

JPA提供了处理这类问题的方法,并给 JPA2.1 增加了一些额外功能,可以极大地提升性能表现,笔者将在本文中解释如何利用 JPA2.1 的功能避免上述问题。

顺便提一下,如果想了解Java项目中更多的典型性能问题,可以参考笔者最近发布的基于性能调查结果的深度报告,如果你在寻找 JPA 资源,点击此链接便可获取JPA2.1特征的备忘清单。接下来我们来看看如何用JPA来解决现有的性能问题。

解决「SQL 查询过多」的问题

根据以往的经验,使用过多的 SQL 查询获取所要求的实体是导致性能问题最普遍的原因。

即使是看起来最简单的查询,如果操作不当,也会触发几十次甚至上百次的 SQL 查询。而且,你在本节中可以看到,这类不当操作不一定会出现在查询语句中,而可能只是几个配置不当的注解。所以,如果你觉得这个问题不会造成影响,请三思。

如果在你的项目中出现以下几段代码,你会怎么想?

List authors = this.em.createQuery("SELECT a FROM Author a",
Author.class).getResultList(); for (Author a : authors) {
System.out.println("作者 "
+ a.getFirstName()
+ " "
+ a.getLastName()
+ " 书籍信息 "
+ a.getBooks()
.stream()
.map(b -> b.getTitle() + "("
+ b.getReviews().size() + " 评论)")
.collect(Collectors.joining(", ")));
}

上面的代码段会打印所有作者的姓名及其书名,看起来非常简单,但你是否想过它给数据库发送了多少次查询?一次?还是两次?或者 Author、Book、Review 实体各一次?

实际上,这取决于数据库中作者的人数。如果数据库较小,里面只有11名作者和6本书。那么这段代码会触发12次查询,其中1次用于获取所有作者姓名,另外11次给每位作者匹配书名。这一问题被称作 n+1 查询问题,无论我们使用的是 MySQL、SqlServer 还是其他数据库,都容易出现此类问题。因此在生产环境中,随着数据量不断增大,代码的性能就越差。

我们可以通过多种方法,用一次查询获取所有要求的实体信息 ,从而避免这一情况。在笔者看来,使用 @NamedEntityGraph 来解决此问题是最新,也最好的方法。

实体图通过独立于查询的方法指定应该从数据库中获取的实体的图。这意味着,你需要为实体图创建一个独立的定义,并在需要时与查询合并。下段代码展示了如何定义根据作者名提取书名的 @NamedEntityGraph

@Entity
@NamedEntityGraph(name = "graph.AuthorBooks", attributeNodes = @NamedAttributeNode("books"))
public class Author implements Serializable {

}

现在,实体管理器可以用这个图为参考,通过一次查询获取所有作者和书名。在图的定义中可以看到,笔者只提供了包含相关实体的属性名称。因此,笔者将@NamedEntityGraph作为loadgraph (负载图),这样便可提取其他所有属性及其定义的获取类型,如下所示:

EntityGraph graph = this.em.getEntityGraph("graph.AuthorBooks");

List authors = this.em
.createQuery("SELECT DISTINCT a FROM Author a", Author.class)
.setHint("javax.persistence.loadgraph", graph).getResultList();

该示例展示了一个非常简单的实体图,在实际的应用中,很可能会用到更复杂的图,但这也不成问题。你可以定义多个 @NamedAttributeNodes 以定义更复杂的图,也可以用 @NamedSubGraph 注解来创建多层次的图。如果想了解更多关于 @NamedEntityGraphs 的信息,请点击实体图使用方式详解

在某些使用案例中,你可能还需要用更动态的方式来定义实体图,比如,根据一些输入参数进行定义。在此类案例中,通过 Java API 用编程的方式定义实体图效果更佳。

解决「逐个更新实体」的问题

逐个更新实体是造成 JPA 性能问题的另一个常见原因。作为 Java 开发者,我们习惯处理对象,并用面向对象的方式思考问题。尽管这是实现复杂逻辑和应用的好方法,但也是处理数据库时导致性能退化的一个常见原因。

从面向对象的角度来看,对实体进行更新和删除操作是完全可以接受的。但当你不得不更新一大组实体时,这种操作就会非常低效。持久性提供者(Persistence Provider)将为每个更新实体创建一个更新语句,并在下一次 flush 操作时发送至数据库中。

然而,SQL 提供了一个更为高效的方式。它允许你创建可一次性更新多个实体的更新语句。你还可以对 JPA 2.1 引入的 CriteriaUpdateCriteriaDelete 语句进行同样的操作。

如果你之前用过 criteria 条件查询,肯定对新的 CriteriaUpdate 以及 CriteriaDelete 语句非常熟悉,更新和删除操作的创建方式几乎与 JPA 2.0 中引入的 criteria 条件查询创建方式一样。

在下面的代码段中可以看到,你需要从实体管理器中获取 CriteriaBuilder 并用它创建 CriteriaUpdate 对象,对 CriteriaQuery 进行的操作与此类似,主要区别在于用于定义更新操作的 set 方法。

CriteriaBuilder cb = this.em.getCriteriaBuilder();
// create update
CriteriaUpdate update = cb.createCriteriaUpdate(Author.class);
// set the root class
Root a = update.from(Author.class);
// set update and where clause
update.set(Author_.firstName, cb.concat(a.get(Author_.firstName), " - updated"));
update.where(cb.greaterThanOrEqualTo(a.get(Author_.id), 3L)); // perform update
Query q = this.em.createQuery(update);
q.executeUpdate();

CriteriaDelete 操作中,你只需要在实体管理器中调用 createCriteriaDelete 方法以获取 CriteriaDelete 对象,并用它来定义与上例类似的 FROMWHERE 查询部分。

在数据库中处理数据

作为 Java 开发者,我们倾向于在 Java 中实现所有的应用逻辑,这也是造成性能问题的一大常见原因。别误会,在 Java 中实现逻辑的好处很多,但如果将部分逻辑实现在数据库中,只把结果发送到业务逻辑层,也能得到很好的效果。

在数据库中执行逻辑的方法很多。只用 SQL 语句,也能完成很多事情,如果不够,你还可以调用数据库的特定功能和存储过程。在本文中,笔者将仔细探讨存储过程,更确切地说是探讨调用存储过程的方式。

在 JPA 2.0 中,并没有针对存储过程的实际支持,本地查询是调用存储过程的唯一方式。JPA 2.1.引入了 @NamedStoredProcedureQuery 和更为动态的 StoredProcedureQuery,改变了这一现状。在本文中,笔者将重点关注基于注解的、用 @NamedStoredProcedureQuery 进行调用的存储过程的定义。笔者在自己的博客中详细介绍了动态存储过程查询

在下面代码段中可以看到, @NamedStoredProcedureQuery 的定义非常简洁,你需要指定查询的名称、数据库中的存储过程名称以及输入和输出参数。在本例中,笔者用输入参数 xy 调用存储过程 calculate,期望的输出参数为 sum,其它支持的参数类型还有用于输入和输出的参数 INPUT 和用于检索结果集的 REF_COURSOR

@NamedStoredProcedureQuery(
name = "calculate",
procedureName = "calculate",
parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, type = Double.class, name = "x"),
@StoredProcedureParameter(mode = ParameterMode.IN, type = Double.class, name = "y"),
@StoredProcedureParameter(mode = ParameterMode.OUT, type = Double.class, name = "sum") })

@NamedStoredProcedureQuery 的使用方法与 @NamedQuery 相似,你需要向实体管理器的createNamedStoredProcedureQuery 方法提供查询名称,以便在本次查询中获取 StoredProcedureQuery 对象,然后,用 setParameter 方法设定输入参数,之后再用 execute 方法调用存储过程。

StoredProcedureQuery query = this.em.createNamedStoredProcedureQuery("calculate");
query.setParameter("x", 1.23d);
query.setParameter("y", 4.56d);
query.execute();
Double sum = (Double) query.getOutputParameterValue("sum");

总结

JPA 给数据库存储和检索带来诸多便利。通过这一工具,可快速开展项目,解决大部分问题,但也更容易导致实现非常低效的持久层。由此,普遍存在的问题包括:使用过多查询获取所需数据、逐个更新实体以及在 Java 中执行所有逻辑。

JPA 2.1规范引入了几个新的功能以应对这些低效操作,比如实体图(entity graphs),条件更新(criteria update)和存储过程查询(stored procedure queries)。笔者的JPA2.1新功能备忘单囊括了JPA 2.1的这些功能及其他新功能,你可以免费下载。

(编译自:http://zeroturnaround.com/rebellabs/three-jpa-2-1-features-that-will-boost-your-applications-performance/

OneAPM 为您提供端到端的 Java 应用性能解决方案,我们支持所有常见的 Java 框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java 监控从来没有如此简单。想阅读更多技术文章,请访问 OneAPM 官方技术博客

本文转自 OneAPM 官方博客

JPA2.1 中三个提升应用性能的新功能的更多相关文章

  1. 三个值得期待的JavaScript新功能!

    让我们来看看JavaScript中一些有用的即将推出的功能.您将看到他们的语法,链接以及时了解他们的进度,我们将编写一个小型测试套件,以展示如何立即开始使用这些提案! JavaScript是如何更新迭 ...

  2. 十个技巧迅速提升JQuery性能

    本文提供即刻提升你的脚本性能的十个步骤.不用担心,这并不是什么高深的技巧.人人皆可运用!这些技巧包括: 使用最新版本 合并.最小化脚本 用for替代each 用ID替代class选择器 给选择器指定前 ...

  3. 十个迅速提升JQuery性能的技巧

    本文提供即刻提升你的脚本性能的十个步骤.不用担心,这并不是什么高深的技巧.人人皆可运用!这些技巧包括: 使用最新版本 合并.最小化脚本 用for替代each 用ID替代class选择器 给选择器指定前 ...

  4. 04-TypeScript中的方法新功能(上)

    在TypeScript中,提供了一些函数的新功能,能够简化JavaScript中的一些比较复杂代码才能实现的一些能力. 在C#后端语言中,能够对方法传递的参数指定params关键字,也就是可以传递任意 ...

  5. 精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能)

    原文:精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能) 本篇博客主要说明如何使用 UI Virtualization(以下简称为 UIV) 来提 ...

  6. [.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能

    [.net 面向对象程序设计进阶] (15) 缓存(Cache)(二) 利用缓存提升程序性能 本节导读: 上节说了缓存是以空间来换取时间的技术,介绍了客户端缓存和两种常用服务器缓布,本节主要介绍一种. ...

  7. 提升Web性能的8个技巧总结

    提升Web性能的8个技巧总结 在互联网盛行的今天,越来越多的在线用户希望得到安全可靠并且快速的访问体验.针对Web网页过于膨胀以及第三脚本蚕食流量等问题,Radware向网站运营人员提出以下改进建议, ...

  8. iOS开发UI篇—iOS开发中三种简单的动画设置

    iOS开发UI篇—iOS开发中三种简单的动画设置 [在ios开发中,动画是廉价的] 一.首尾式动画 代码示例: // beginAnimations表示此后的代码要“参与到”动画中 [UIView b ...

  9. 提升PHP性能的21种方法

    提升PHP性能的21种方法. 1.用单引号来包含字符串要比双引号来包含字符串更快一些.因为PHP会在双引号包围的字符串中搜寻变量,单引号则不会.2.如果能将类的方法定义成static,就尽量定义成st ...

随机推荐

  1. &&与&

    if((2>1)&&(4>3))System.out.printf("两边都是true"); else   System.out.println(&qu ...

  2. Java基础知识强化之IO流笔记31:转换流出现的原因和格式

    1. 由于字节流操作中文不是特别方便,所以Java就提供了转换流.  字符流 = 字节流 + 编码表 2. 编码表 由字符及其对应数值组成的一张表 常见的编码表: • ASCII/Unicode字符集 ...

  3. Jquery调用从ashx文件返回的jsonp格式的数据处理实例

    开发环境:vs2010+jquery-1.4.min.js 解决问题:网上代码比较少,好多调试不通,返回数据不用json而用jsonp主要考虑解决跨域问题 开发步骤:打开VS2010,新建一web站点 ...

  4. shell中exit命令不退出脚本

    好久不用shell了,今天碰到一个坑,发现exit后,shell脚本还会运行. $JAVA_HOME/bin/jps | while read RES do PID=`echo $RES | awk ...

  5. LCA问题

    基本概念 LCA:树上的最近公共祖先,对于有根树T的两个结点u.v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u.v的祖先且x的深度尽可能大. RMQ:区间最小值查询问题.对于长度为n的 ...

  6. git使用备忘

    简单的记录下现在使用git的时候,暂时或者常用到的命令,纯粹自己备忘,没用到过的命令就不写了,日后有用到了在补充. 1.把远程仓库克隆到本地:git clone 远程地址 2.分支操作 新建分支:gi ...

  7. c# 格式化百分比

    代码示例: <%#Eval("total").ToString() == "0" ? "00.00%" : (Convert.ToDe ...

  8. 【原创教程】JavaScript详解之语法和对象

    JavaScript的好的想法:函数.弱类型.动态对象和一个富有表现力的对象字面量表示法. JavaScript的坏的想法:基于全局变量的编程模型.   好了,不管他是好的还是坏的,都是我的最爱,下面 ...

  9. xml--小结③DTD的基本语法(看懂即可)

    四.DTD的基本语法(看懂即可)1.DTD:Document Type Definition2.作用:约束XML的书写规范.3.DTD文件保存到磁盘时,必须使用UTF-8编码 4.如何引入外部的DTD ...

  10. Windows I/O模型、同步/异步、阻塞/非阻塞

    转载自:http://www.cppblog.com/tx7do/articles/5954.html 同步 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.按照这个定义,其实 ...