经常在网上看到开发者们抱怨 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 引入的 CriteriaUpdate
和 CriteriaDelete
语句进行同样的操作。
如果你之前用过 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
对象,并用它来定义与上例类似的 FROM
和 WHERE
查询部分。
在数据库中处理数据
作为 Java 开发者,我们倾向于在 Java 中实现所有的应用逻辑,这也是造成性能问题的一大常见原因。别误会,在 Java 中实现逻辑的好处很多,但如果将部分逻辑实现在数据库中,只把结果发送到业务逻辑层,也能得到很好的效果。
在数据库中执行逻辑的方法很多。只用 SQL 语句,也能完成很多事情,如果不够,你还可以调用数据库的特定功能和存储过程。在本文中,笔者将仔细探讨存储过程,更确切地说是探讨调用存储过程的方式。
在 JPA 2.0 中,并没有针对存储过程的实际支持,本地查询是调用存储过程的唯一方式。JPA 2.1.引入了 @NamedStoredProcedureQuery
和更为动态的StoredProcedureQuery
,改变了这一现状。在本文中,笔者将重点关注基于注解的、用 @NamedStoredProcedureQuery
进行调用的存储过程的定义。笔者在自己的博客中详细介绍了动态存储过程查询 。
在下面代码段中可以看到, @NamedStoredProcedureQuery
的定义非常简洁,你需要指定查询的名称、数据库中的存储过程名称以及输入和输出参数。在本例中,笔者用输入参数 x
和 y
调用存储过程 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的这些功能及其他新功能,你可以免费下载。
OneAPM 为您提供端到端的 Java 应用性能解决方案,我们支持所有常见的 Java 框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java 监控从来没有如此简单。想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客
相关推荐
在IBM Websphere 8.5环境中配置JPA 2.1的支持,主要是因为该版本默认仅支持JPA 2.0,而某些项目可能需要使用JPA 2.1提供的新特性和功能。以下是一步步指导如何进行配置: 1. **修改Hibernate配置文件**: 在项目的...
- **二级缓存改进**:为了提高性能,JPA 2.1 加强了二级缓存的功能,使得持久化上下文能够在应用程序之间共享缓存数据。 - **可插拔性**:增强了 JPA 的可插拔性,允许开发人员更轻松地扩展或替换某些组件。 - **...
1. **创建Web项目**:使用Eclipse或其他IDE创建一个新的Web项目。 2. **配置Struts2**: - 将Struts2的核心配置文件`struts.xml`复制到项目的`src`目录下。 - 配置文件示例: ```xml <!DOCTYPE struts PUBLIC ...
#### 三、JPA 存储库 ##### 3.1 引入 - **Spring 的名称空间**: - 使用 Spring 的名称空间来配置 JPA 存储库。 - 示例: ```xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa=...
JPA是一个Java标准,旨在简化Java应用中实体对象与数据库表之间的映射关系,而Hibernate则是这一规范的具体实现之一。 - **Hibernate Core**: Hibernate的核心实现,负责处理对象与关系数据之间的转换。 - **...
JPA2.0作为JPA的一个重大版本更新,在原有基础上增加了许多新功能和改进,使得数据持久化变得更加简单高效。 - **新特性**: - **支持JSR-303验证注解**:允许在实体类上直接使用JSR-303的验证注解,如`@NotNull`...
Hibernate4.1在性能和功能上都有所提升,包括支持JPA 2.1规范、优化的缓存机制以及对新数据库特性的支持。 在这个SSH整合中,Struts2负责处理HTTP请求和响应,Spring管理整个应用的bean,包括Struts2的Action和...
Spring Boot 是一个基于 Spring 框架的简化版本,它旨在简化新Spring应用的初始搭建以及开发过程。Spring Boot 的核心设计目标是“约定优于配置”,通过这种方式,开发者只需要少量的配置就能创建一个独立运行(运行...
SSH是Java开发中常见的三个框架的缩写,分别代表Struts、Spring和Hibernate。这个压缩包包含的是这三个框架的特定版本:Struts2.3.20、Spring4.1.1和Hibernate4.3.7。这些版本在当时是相对稳定的,并广泛应用于企业...
总结,Hibernate 4.0.0 Beta1版本为开发者提供了更多强大且灵活的工具,无论是对JPA 2.1的支持,还是内部的性能优化,都体现了其致力于提升开发效率和应用性能的决心。在实际项目中,熟练掌握Hibernate的各种特性和...
总的来说,Hibernate 4.2.6 Final版本是Hibernate发展历程中的一个重要里程碑,它集成了众多新功能和优化,为开发者提供了更强大、更灵活的数据库操作手段,是Java企业级应用的理想选择。无论是大型项目还是小型应用...
1. 支持JPA 2.1规范:提供了新的查询元素,如TemporalType,以及更丰富的实体生命周期回调。 2. 强化了第二级缓存:引入了对查询结果的缓存支持,提高了系统响应速度。 3. 优化了HQL和JPQL查询:增强了类型安全性和...
这个版本进一步提升了性能和稳定性,对JPA 2.1规范进行了兼容,增加了对延迟加载和实体图形的深度遍历的支持。它还引入了对Java 8特性的支持,比如日期和时间API的改进。在多线程和并发处理方面也做了优化,提供了更...
它还可能增强了对JPA 2.1规范中定义的新的事务隔离级别的支持。 最后,Hibernate的4.5.1版本可能对日志系统进行了调整,提供了更好的调试信息和日志级别控制,帮助开发者在遇到问题时更快地定位和解决问题。 总的...
Hibernate是一个对象关系映射(ORM)框架,它在Hibernate4.2版本中可能提升了性能,增加了新的特性,如对JPA2.1规范的支持、更完善的二级缓存机制、更好的数据库兼容性等。它允许开发者通过Java对象来操作数据库,...
Struts2.3.14、Spring3.1.1和Hibernate4.1.0是三个非常关键的Java开源框架,它们在企业级Web应用开发中有着广泛的应用。这个压缩包文件包含了这三个框架的jar包,是搭建基于SSH(Struts2、Spring、Hibernate)集成...
Hibernate 4.3版本增加了对Java Persistence API(JPA 2.1)的支持,提升了性能,提供了更丰富的查询语言(HQL)和 Criteria API,以及对新数据库特性的支持。此外,4.3版本还加强了与Spring框架的集成,使得在...
SSH2 是一个流行的Java开发框架组合,主要包括Struts2、Spring和Hibernate三个核心组件,用于构建企业级Web应用程序。在最新版本中,SSH2 提供了高效、灵活且可扩展的解决方案,帮助开发者轻松地管理和整合应用的...
3. 支持 JPA 2.1:Hibernate 4.1.8.Final 全面支持 JPA 2.1 规范,包括新的注解、事件模型和多租户功能。 4. 事务管理:提供了对 JTA 事务的更完善支持,使得在分布式环境下的事务处理更为便捷。 5. 错误和日志改进...
这个“struts2.1+spring2.5+hibernate3.2项目基本jar包”集合了这三个框架的核心库,为开发者提供了便捷的开发环境。 **Struts2** 是一个基于MVC设计模式的Web应用程序框架,主要用于控制应用程序的流程。Struts2.1...