当前位置: 首页 > 科技观察

影响性能的10个常见Hibernate错误

时间:2023-03-20 00:54:58 科技观察

然后阅读这篇文章!我在许多应用程序中修复了性能问题,其中大多数是由相同的错误引起的。修复后,性能更流畅,大多数问题都很简单。因此,如果您想改进应用程序,这也可能是小菜一碟。这里列出了导致Hibernate性能问题的10个最常见错误以及如何修复它们。错误1:使用EagerFetchingFetchType。EAGER的启示已经讨论了好几年,并且有很多文章详细解释它。我自己也写了一个。但不幸的是,它仍然是性能问题的两个最常见原因之一。FetchType定义Hibernate何时初始化关联。您可以使用@OneToMany、@ManyToOne、@ManyToMany和@OneToOne注释的fetch属性来指定它。@EntitypublicclassAuthor{@ManyToMany(mappedBy="authors",fetch=FetchType.LAZY)privateListbooks=newArrayList();...}当Hibernate加载一个实体时,它也会加载立即关联的fetch。例如,当Hibernate加载Author实体时,它还会提取相关的Book实体。这需要对每个作者进行额外的查询,因此通常需要数十个甚至数百个额外的查询。这种方法效率很低,因为无论你是否要使用关联,Hibernate都会这样做。***请改用FetchType.LAZY。它延迟关系的初始化,直到在业务代码中使用它。这样可以避免大量不必要的查询,提高应用程序的性能。幸运的是,JPA规范将FetchType.LAZY定义为所有对多关联的默认值。因此,您只需要确保不更改此默认值即可。但不幸的是,一对一关系并非如此。错误2:忽略一对一关联的默认FetchType接下来,为了防止急切获取,您需要做的就是更改所有一对一关联的默认FetchType。不幸的是,默认情况下这些关系是即时获取的。在某些用例中,这不是一个大问题,因为您只是加载了一条额外的数据库记录。但是,如果您加载多个实体,每个实体指定其中的几个关联,它可以快速累加。因此,***确保所有一对一关联都将FetchType设置为LAZY。@EntitypublicclassReview{@ManyToOne(fetch=FetchType.LAZY)@JoinColumn(name="fk_book")privateBookbook;...}错误3:对所有关联使用FetchType.LAZY时不要初始化所需的关联以避免错误1和对于错误2,您会在代码中发现多个n+1选择问题。当Hibernate执行1个查询以选择n个实体,然后必须为每个实体执行额外的查询以初始化延迟获取关联时,就会出现此问题。Hibernate透明地获取惰性关系,因此很难在您的代码中发现此类问题。您只需调用关联的getter方法,我想我们都不希望Hibernate执行任何额外的查找。Listauthors=em.createQuery("SELECTaFROMAuthora",Author.class).getResultList();for(Authora:authors){log.info(a.getFirstName()+""+a.getLastName()+"wrote"+a.getBooks().size()+"books.");}如果使用开发配置激活Hibernate的统计组件,监控SQL语句的执行数量,n+1选择问题会更容易解决发现。15:06:48,362INFO[org.hibernate.engine.internal.StatisticalLoggingSessionEventListener]-SessionMetrics{28925nanosecondsspentacquiring1JDBCconnections;24726nanosecondsspentreleasing1JDBCconnections;1115946nanosecondsspentpreparing13JDBCstatements;8974211nanosecondsspentexecuting13JDBCstatements;0nanosecondsspentexecuting0JDBCbatches;0nanosecondsspentperforming0L2Cputs;0nanosecondsspentperforming0L2Chits;0nanosecondsspentperforming0L2Cmisses;20715894nanosecondsspentexecuting1flushes(flushingatotalof13entitiesand13collections);88175nanosecondsspentexecuting1partial-flushes(flushingatotalof0entitiesand0collections)}正如你看到的JPQL查询并对12个选定的作者实体中的每一个调用getBooks方法导致了13个查询。这比大多数开发人员看到如此简单的代码时所想的要多。如果让Hibernate初始化所需的关联,则可以轻松避免这种情况。有几种不同的方法可以做到这一点。最简单的方法是在FROM子句中添加JOINFETCH语句。Authora=em.createQuery("SELECTaFROMAuthoraJOINFETCHa.booksWHEREa.id=1",Author.class).getSingleResult();错误4:选择比需要更多的记录当我告诉您选择太多记录会减慢应用程序时,我保证您不会对速度感到惊讶。但是我还是经常发现这个问题,在咨询电话中分析应用的时候。原因之一可能是JPQL不支持您在SQL查询中使用OFFSET和LIMIT关键字。这似乎并不限制查询中检索到的记录数。但是,您可以做到。您只需要在Query接口上设置这些信息,而不用在JPQL语句中。我在下面的代码片段中执行此操作。我首先按id对选定的Author实体进行排序,然后告诉Hibernate检索前5个实体。Listauthors=em.createQuery("SELECTaFROMAuthoraORDERBYa.idASC",Author.class).setMaxResults(5).setFirstResult(0).getResultList();错误5:不使用绑定参数绑定参数是查询中的简单占位符,并提供许多与性能无关的好处:它们非常易于使用。Hibernate自动执行所需的转换。Hibernate会自动对String进行转义,以防止SQL注入漏洞。而且它还可以帮助您实现高性能的应用程序。大多数应用程序执行大量相同的查询,只是在WHERE子句中使用了一组不同的参数值。绑定参数允许Hibernate和数据库识别和优化这些查询。您可以在JPQL语句中使用命名绑定参数。每个命名参数都以“:”开头,后跟其名称。在查询中定义绑定参数后,需要调用Query接口的setParameter方法设置绑定参数值。TypedQueryq=em.createQuery("SELECTaFROMAuthoraWHEREa.id=:id",Author.class);q.setParameter("id",1L);Authora=q.getSingleResult();误区六:执行业务代码对于Java开发人员来说,很自然的把所有的逻辑都放在业务层来实现。我们可以使用我们最熟悉的语言、库和工具。但有时,最好实现在数据库中操作大量数据的逻辑。您可以通过调用JPQL或SQL查询中的函数或使用存储过程来实现。让我们快速了解一下如何在JPQL查询中调用函数。如果您想更深入地研究这个主题,可以阅读我关于存储过程的文章。您可以在JPQL查询中使用标准函数,就像在SQL查询中调用它们一样。您只需通过名称引用函数,然后是左括号、可选参数列表和右括号。Queryq=em.createQuery("SELECTa,size(a.books)FROMAuthoraGROUPBYa.id");Listresults=q.getResultList();并且,通过JPA的函数功能,您还可以调用数据库特定的或自定义的数据库函数。TypedQueryq=em.createQuery("SELECTbFROMBookbWHEREb.id=function('calculate',1,2)",Book.class);Bookb=q.getSingleResult();错误七:无缘无故调用flush方法是另一个常见的错误。在持久化新实体或更新现有实体后调用EntityManager的flush方法时,开发人员经常会遇到此错误。这会强制Hibernate对所有托管实体执行脏检查,并为所有挂起的插入、更新或删除操作创建和执行SQL语句。这会减慢应用程序的速度,因为它会阻止Hibernate使用某些内部优化。Hibernate将所有托管实体存储在持久化上下文中,并尝试尽可能地延迟写操作的执行。这使得Hibernate可以将对同一个实体的多个更新操作组合成一个SQLUPDATE语句,通过JDBC批处理绑定多个相同的SQL语句,避免执行重复的SQL语句返回你在当前Session中所做的SQL语句所使用的实体。根据经验,您应该避免调用任何flush方法。JPQL批量操作是罕见的例外之一,对此我在错误9中进行了解释。错误8:对一切都使用HibernateHibernate的对象关系映射和各种性能优化使得大多数CRUD用例的实现非常简单和高效。这使得Hibernate成为许多项目的绝佳选择。但这并不意味着Hibernate是适用于所有项目的良好解决方案。我在之前的一篇文章和视频中详细讨论过这个问题。JPA和Hibernate为创建、读取或更新某些数据库记录的大多数标准CRUD用例提供了良好的支持。对于这些用例,对象关系映射可以极大地提高生产力,Hibernate的内部优化提供了非常优越的性能。但是,当您需要执行非常复杂的查询、实施分析或报告用例或对大量记录执行写入操作时,结果会有所不同。这些情况都不适用于JPA和Hibernate的查询能力和基于生命周期的实体管理。如果这些用例只是您应用程序的一小部分,那么您仍然可以使用Hibernate。但总的来说,你应该看看其他更接近SQL的框架,如jOOQ或Querydsl,并避免任何对象关系映射。错误9:逐个更新或删除大量实体当您查看Java代码时,感觉逐个更新或删除实体是可以接受的。这就是我们对待物体的方式,对吧?这可能是处理Java对象的标准方式,但是如果您需要更新大量数据库记录,那么这不是一个好方法。在SQL中,您只需要定义一次影响多条记录的UPDATE或DELETE语句。数据库将非常有效地处理这些操作。不幸的是,使用JPA和Hibernate并不容易。每个实体都有自己的生命周期,如果要更新或删除多个实体,首先需要从数据库中加载它们。然后对每个实体执行操作,Hibernate将为每个实体生成所需的SQLUPDATE或DELETE语句。因此,Hibernate不会只用1条语句来更新1000条数据库记录,而是至少会执行1001条语句。显然,执行1001条语句比只执行1条语句需要更多的时间。幸运的是,您可以使用JPQL、本机SQL或Criteria查询对JPA和Hibernate执行相同的操作。但它有一些您应该注意的副作用。在数据库中执行更新或删除操作时不使用实体。这提供了更好的性能,但它也忽略了实体生命周期,并且Hibernate无法更新任何缓存。这个我在《How to use native queries to perform bulk updates》一文中有详细的解释。简而言之,在执行批量更新之前,您不应使用任何生命周期侦听器并在EntityManager上调用flush和clear方法。flush方法将强制Hibernate在clear方法从当前持久性上下文中分离所有实体之前将所有未决更改写入数据库。em.flush();em.clear();Queryquery=em.createQuery("UPDATEBookbSETb.price=b.price*1.1");query.executeUpdate();错误10:使用实体进行只读操作JPA和Hibernate支持一些不同的投影。如果您想优化应用程序的性能,那么您应该使用投影。最明显的原因是您应该只选择用例中需要的数据。但这不是唯一的原因。正如我最近的测试所示,DTO投影比实体快得多,即使您读取相同的数据库列也是如此。在SELECT子句中使用构造函数表达式而不是实体只是一个小改动。但在我的测试中,DTO投影比实体快40%。当然,这两个比较的数值取决于你的用例,你不应该通过这种简单有效的方式来提高性能。了解如何查找和修复Hibernate性能问题如您所见,一些小事情会降低您的应用程序的速度。但幸运的是,我们可以轻松避免这些问题,构建一个高性能的持久层。