澳门新葡萄京娱乐场 6

澳门新葡萄京娱乐场扼杀性能的 10 个常见 Hibernate 错误

本文由码农网 –
小峰原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

本文最新版已更新至:http://thinkinside.tk/2012/12/30/JPA.html

JPA定义了Java
ORM及实体操作API的标准。本文摘录了JPA的一些关键信息以备查阅。

如果有hibernate的基础,通过本文也可以快速掌握JPA的基本概念及使用。

你有没有想过如果你能解决Hibernate问题,那么你的应用程序可以更快?

你有没有想过如果你能解决Hibernate问题,那么你的应用程序可以更快?

Table of Contents

  • 1
    JPA概述
  • 2
    实体生命周期
  • 3
    实体关系映射(ORM)

    • 3.1
      基本映射
    • 3.2
      ID生成策略
    • 3.3
      关联关系
    • 3.4
      继承关系
  • 4
    事件及监听
  • 5 Query Language
    查询语言

    • 5.1
      使用参数
    • 5.2
      命名查询
    • 5.3
      排序
    • 5.4
      聚合查询
    • 5.5
      更新和删除
    • 5.6
      更多
  • 6
    事务管理

那么请阅读这篇文章!

那么请阅读这篇文章!

1 JPA概述

JPA(Java Persistence
API,Java持久化API),定义了对象-关系映射(ORM)以及实体对象持久化的标准接口。

JPA是JSR-220(EJB3.0)规范的一部分,在JSR-220中规定实体对象(EntityBean)由JPA进行支持。

所以JPA不局限于EJB3.0,而是作为POJO持久化的标准规范,可以脱离容器独立运行,开发和测试更加方便。

JPA在应用中的位置如下图所示:

澳门新葡萄京娱乐场 1

 

JPA维护一个Persistence
Context(持久化上下文),在持久化上下文中维护实体的生命周期。主要包含三个方面的内容:

  1. ORM元数据。JPA支持annotion或xml两种形式描述对象-关系映射。
  2. 实体操作API。实现对实体对象的CRUD操作。
  3. 查询语言。约定了面向对象的查询语言JPQL(Java Persistence Query
    Language)。

JPA的主要API都定义在javax.persistence包中。如果你熟悉Hibernate,可以很容易做出对应:

 
org.hibernate javax.persistence 说明
cfg.Configuration Persistence 读取配置信息
SessionFactory EntityManagerFactory 用于创建会话/实体管理器的工厂类
Session EntityManager 提供实体操作API,管理事务,创建查询
Transaction EntityTransaction 管理事务
Query Query 执行查询

澳门新葡萄京娱乐场 2

我在很多应用程序中修复过性能问题,其中大部分都是由同样的错误引起的。修复之后,性能变得更溜,而且其中的大部分问题都很简单。所以,如果你想改进应用程序,那么可能也是小菜一碟。

我在很多应用程序中修复过性能问题,其中大部分都是由同样的错误引起的。修复之后,性能变得更溜,而且其中的大部分问题都很简单。所以,如果你想改进应用程序,那么可能也是小菜一碟。

2 实体生命周期

实体生命周期是JPA中非常重要的概念,描述了实体对象从创建到受控、从删除到游离的状态变换。对实体的操作主要就是改变实体的状态。

JPA中实体的生命周期如下图:

澳门新葡萄京娱乐场 3

  1. New,新创建的实体对象,没有主键(identity)值
  2. Managed,对象处于Persistence
    Context(持久化上下文)中,被EntityManager管理
  3. Detached,对象已经游离到Persistence Context之外,进入Application
    Domain
  4. Removed, 实体对象被删除

EntityManager提供一系列的方法管理实体对象的生命周期,包括:

  1. persist, 将新创建的或已删除的实体转变为Managed状态,数据存入数据库。
  2. remove,删除受控实体
  3. merge,将游离实体转变为Managed状态,数据存入数据库。

如果使用了事务管理,则事务的commit/rollback也会改变实体的状态。

这里列出了导致Hibernate性能问题的10个最常见的错误,以及如何修复它们。

这里列出了导致Hibernate性能问题的10个最常见的错误,以及如何修复它们。

3 实体关系映射(ORM)

澳门新葡萄京娱乐场 4

澳门新葡萄京娱乐场 5

3.1 基本映射

 
对象端 数据库端 annotion 可选annotion
Class Table @Entity @Table(name="tablename")
property column @Column(name = "columnname")
property primary key @Id @GeneratedValue 详见ID生成策略
property NONE @Transient  

错误1:使用Eager Fetching

FetchType.EAGER的启示已经讨论了好几年了,而且有很多文章对它进行了详细的解释。我自己也写了一篇。但不幸的是,它仍然是性能问题最常见的两个原因之一。

FetchType定义了Hibernate何时初始化关联。你可以使用@OneToMany,@ManyToOne,@ManyToMany和@OneToOneannotation注释的fetch属性进行指定。

@Entity
public class Author{

    @ManyToMany(mappedBy="authors", fetch=FetchType.LAZY)
    private List<Book> books = new ArrayList<Book>();

    ...

}

当Hibernate加载一个实体的时候,它也会即时加载获取的关联。例如,当Hibernate加载Author实体时,它也提取相关的Book实体。这需要对每个Author进行额外的查询,因此经常需要几十甚至数百个额外的查询。

这种方法是非常低效的,因为Hibernate不管你是不是要使用关联都会这样做。最好改用FetchType.LAZY代替。它会延迟关系的初始化,直到在业务代码中使用它。这可以避免大量不必要的查询,并提高应用程序的性能。

幸运的是,JPA规范将FetchType.LAZY定义为所有对多关联的默认值。所以,你只需要确保你不改变这个默认值即可。但不幸的是,一对一关系并非如此。

错误1:使用Eager Fetching

3.2 ID生成策略

ID对应数据库表的主键,是保证唯一性的重要属性。JPA提供了以下几种ID生成策略

  1. GeneratorType.AUTO ,由JPA自动生成
  2. GenerationType.IDENTITY,使用数据库的自增长字段,需要数据库的支持(如SQL
    Server、MySQL、DB2、Derby等)
  3. GenerationType.SEQUENCE,使用数据库的序列号,需要数据库的支持(如Oracle)
  4. GenerationType.TABLE,使用指定的数据库表记录ID的增长
    需要定义一个TableGenerator,在@GeneratedValue中引用。例如:

    @TableGenerator( name=”myGenerator”,
    table=”GENERATORTABLE“, pkColumnName =
    “ENTITYNAME“, pkColumnValue=”MyEntity”, valueColumnName =
    “PKVALUE“, allocationSize=1 )

    @GeneratedValue(strategy =
    GenerationType.TABLE,generator=”myGenerator”)

错误2:忽略一对一关联的默认FetchType

接下来,为了防止立即抓取(eager
fetching),你需要做的是对所有的一对一关联更改默认的FetchType。不幸的是,这些关系在默认情况下会被即时抓取。在一些用例中,那并非一个大问题,因为你只是加载了一个额外的数据库记录。但是,如果你加载多个实体,并且每个实体都指定了几个这样的关联,那么很快就会积少成多,水滴石穿。

所以,最好确保所有的一对一关联设置FetchType为LAZY。

@Entity
public class Review {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "fk_book")
    private Book book;

    ...

}

FetchType.EAGER的启示已经讨论了好几年了,而且有很多文章对它进行了详细的解释。我自己也写了一篇。但不幸的是,它仍然是性能问题最常见的两个原因之一。

3.3 关联关系

JPA定义了one-to-one、one-to-many、many-to-one、many-to-many 4种关系。

对于数据库来说,通常在一个表中记录对另一个表的外键关联;对应到实体对象,持有关联数据的一方称为owning-side,另一方称为inverse-side。

为了编程的方便,我们经常会希望在inverse-side也能引用到owning-side的对象,此时就构建了双向关联关系。
在双向关联中,需要在inverse-side定义mappedBy属性,以指明在owning-side是哪一个属性持有的关联数据。

对关联关系映射的要点如下:

 
关系类型 Owning-Side Inverse-Side
one-to-one @OneToOne @OneToOne(mappedBy="othersideName")
one-to-many / many-to-one @ManyToOne @OneToMany(mappedBy="xxx")
many-to-many @ManyToMany @ManyToMany(mappedBy ="xxx")

其中
many-to-many关系的owning-side可以使用@JoinTable声明自定义关联表,比如Book和Author之间的关联表:

@JoinTable(name = “BOOKAUTHOR“, joinColumns = {
@JoinColumn(name = “BOOKID“, referencedColumnName = “id”) },
inverseJoinColumns = { @JoinColumn(name = “AUTHORID“,
referencedColumnName = “id”) })

关联关系还可以定制延迟加载和级联操作的行为(owning-side和inverse-side可以分别设置):

通过设置fetch=FetchType.LAZY 或
fetch=FetchType.EAGER来决定关联对象是延迟加载或立即加载。

通过设置cascade={options}可以设置级联操作的行为,其中options可以是以下组合:

  • CascadeType.MERGE 级联更新
  • CascadeType.PERSIST 级联保存
  • CascadeType.REFRESH 级联刷新
  • CascadeType.REMOVE 级联删除
  • CascadeType.ALL 级联上述4种操作

错误3:不要初始化所需的关联

当你对所有关联使用FetchType.LAZY以避免错误1和错误2时,你会在代码中发现若干n+1选择问题。当Hibernate执行1个查询来选择n个实体,然后必须为每个实体执行一个额外的查询来初始化一个延迟的获取关联时,就会发生这个问题。

Hibernate透明地获取惰性关系,因此在代码中很难找到这种问题。你只要调用关联的getter方法,我想我们大家都不希望Hibernate执行任何额外的查询吧。

List<Author> authors = em.createQuery("SELECT a FROM Author a", Author.class).getResultList();
for (Author a : authors) {
    log.info(a.getFirstName() + " " + a.getLastName() + " wrote "
            + a.getBooks().size() + " books.");
}

如果你使用开发配置激活Hibernate的统计组件并监视已执行的SQL语句的数量,n+1选择问题就会更容易被发现。

15:06:48,362 INFO [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
  28925 nanoseconds spent acquiring 1 JDBC connections;
  24726 nanoseconds spent releasing 1 JDBC connections;
  1115946 nanoseconds spent preparing 13 JDBC statements;
  8974211 nanoseconds spent executing 13 JDBC statements;
  0 nanoseconds spent executing 0 JDBC batches;
  0 nanoseconds spent performing 0 L2C puts;
  0 nanoseconds spent performing 0 L2C hits;
  0 nanoseconds spent performing 0 L2C misses;
  20715894 nanoseconds spent executing 1 flushes (flushing a total of 13 entities and 13 collections);
  88175 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}

正如你所看到的JPQL查询和对12个选定的Author实体的每一个调用getBooks方法,导致了13个查询。这比大多数开发人员所以为的还要多,在他们看到如此简单的代码片段的时候。

如果你让Hibernate初始化所需的关联,那么你可以很容易地避免这种情况。有若干不同的方式可以做到这一点。最简单的方法是添加JOIN
FETCH语句到FROM子句中。

Author a = em.createQuery(
                "SELECT a FROM Author a JOIN FETCH a.books WHERE a.id = 1",
                Author.class).getSingleResult();

FetchType定义了Hibernate何时初始化关联。你可以使用@OneToMany,@ManyToOne,@ManyToMany和@OneToOneannotation注释的fetch属性进行指定。

3.4 继承关系

JPA通过在父类增加@Inheritance(strategy=InheritanceType.xxx)来声明继承关系。A支持3种继承策略:

  1. 单表继承(InheritanceType.SINGLETABLE),所有继承树上的类共用一张表,在父类指定(@DiscriminatorColumn)声明并在每个类指定@DiscriminatorValue来区分类型。
  2. 类表继承(InheritanceType.JOINED),父子类共同的部分公用一张表,其余部分保存到各自的表,通过join进行关联。
  3. 具体表继承(InheritanceType.TABLEPERCLASS),每个具体类映射到自己的表。

其中1和2能够支持多态,但是1需要允许字段为NULL,2需要多个JOIN关系;3最适合关系数据库,对多态支持不好。具体应用时根据需要取舍。

错误4:选择比所需的更多记录

当我告诉你选择太多的记录会减慢应用程序的速度时,我敢保证你一定不会感到惊讶。但是我仍然经常会发现这个问题,当我在咨询电话中分析应用程序的时候。

其中一个原因可能是JPQL不支持你在SQL查询中使用OFFSET和LIMIT关键字。这看起来似乎不能限制查询中检索到的记录数量。但是,你可以做到这一点。你只需要在Query接口上,而不是在JPQL语句中设置此信息。

我在下面的代码片段中做到这一点。我首先通过id排序选定的Author实体,然后告诉Hibernate检索前5个实体。

List<Author> authors = em.createQuery("SELECT a FROM Author a ORDER BY a.id ASC", Author.class)
                                    .setMaxResults(5)
                                    .setFirstResult(0)
                                    .getResultList();
@Entity public class Author{      @ManyToMany(mappedBy="authors", fetch=FetchType.LAZY)     private List<Book> books = new ArrayList<Book>();      ...  } 

4 事件及监听

澳门新葡萄京娱乐场 6

通过在实体的方法上标注@PrePersist,@PostPersist等声明即可在事件发生时触发这些方法。

错误5:不使用绑定参数

绑定参数是查询中的简单占位符,并提供了许多与性能无关的好处:

  • 它们非常易于使用。
  • Hibernate自动执行所需的转换。
  • Hibernate会自动转义Strings,防止SQL注入漏洞。

而且也可以帮助你实现一个高性能的应用程序。

大多数应用程序执行大量相同的查询,只在WHERE子句中使用了一组不同的参数值。绑定参数允许Hibernate和数据库识别与优化这些查询。

你可以在JPQL语句中使用命名的绑定参数。每个命名参数都以“:”开头,后面跟它的名字。在查询中定义了绑定参数后,你需要调用Query接口上的setParameter方法来设置绑定参数值。

TypedQuery<Author> q = em.createQuery(
                "SELECT a FROM Author a WHERE a.id = :id", Author.class);
q.setParameter("id", 1L);
Author a = q.getSingleResult();

当Hibernate加载一个实体的时候,它也会即时加载获取的关联。例如,当Hibernate加载Author实体时,它也提取相关的Book实体。这需要对每个Author进行额外的查询,因此经常需要几十甚至数百个额外的查询。

5 Query Language 查询语言

JPA提供两种查询方式,一种是根据主键查询,使用EntityManager的find方法:

T find(Class entityClass, Object primaryKey)

另一种就是使用JPQL查询语言。JPQL是完全面向对象的,具备继承、多态和关联等特性,和hibernate
HQL很相似。

使用EntityManager的createQuery方法:

Query createQuery(String qlString)

错误6:执行业务代码中的所有逻辑

对于Java开发人员来说,在业务层实现所有的逻辑是自然而然的。我们可以使用我们最熟悉的语言、库和工具。

但有时候,在数据库中实现操作大量数据的逻辑会更好。你可以通过在JPQL或SQL查询中调用函数或者使用存储过程来完成。

让我们快速看看如何在JPQL查询中调用函数。如果你想深入探讨这个话题,你可以阅读我关于存储过程的文章。

你可以在JPQL查询中使用标准函数,就像在SQL查询中调用它们一样。你只需引用该函数的名称,后跟一个左括号,一个可选的参数列表和一个右括号。

Query q = em.createQuery("SELECT a, size(a.books) FROM Author a GROUP BY a.id");
List<Object[]> results = q.getResultList();

并且,通过JPA的函数function,你也可以调用数据库特定的或自定义的数据库函数。

TypedQuery<Book> q = em.createQuery(
             "SELECT b FROM Book b WHERE b.id = function('calculate', 1, 2)",
             Book.class);
Book b = q.getSingleResult();

这种方法是非常低效的,因为Hibernate不管你是不是要使用关联都会这样做。最好改用FetchType.LAZY代替。它会延迟关系的初始化,直到在业务代码中使用它。这可以避免大量不必要的查询,并提高应用程序的性能。

5.1 使用参数

可以在JPQL语句中使用参数。JPQL支持命名参数和位置参数两种参数,但是在一条JPQL语句中所有的参数只能使用同一种类型。

举例如下:

  • 命令参数

Query query = em.createQuery(“select p from Person p where
p.personid=:Id”); query.setParameter(“Id”,new Integer(1));

  • 位置参数

Query query = em.createQuery(“select p from Person p where
p.personid=?1”); query.setParameter(1,new Integer(1));

错误7:无理由地调用flush方法

这是另一个比较普遍的错误。开发人员在持久化一个新实体或更新现有实体后,调用EntityManager的flush方法时经常会出现这个错误。这迫使Hibernate对所有被管理的实体执行脏检查,并为所有未决的插入、更新或删除操作创建和执行SQL语句。这会减慢应用程序,因为它阻止了Hibernate使用一些内部优化。

Hibernate将所有被管理的实体存储在持久性上下文中,并试图尽可能延迟写操作的执行。这允许Hibernate将同一实体上的多个更新操作合并为一个SQL
UPDATE语句,通过JDBC批处理绑定多个相同的SQL语句,并避免执行重复的SQL语句,这些SQL语句返回你已在当前Session中使用的实体。

作为一个经验法则,你应该避免任何对flush方法的调用。JPQL批量操作是罕见的例外之一,对此我将在错误9中解释。

幸运的是,JPA规范将FetchType.LAZY定义为所有对多关联的默认值。所以,你只需要确保你不改变这个默认值即可。但不幸的是,一对一关系并非如此。

5.2 命名查询

如果某个JPQL语句需要在多个地方使用,还可以使用@NamedQuery 或者
@NamedQueries在实体对象上预定义命名查询。

在需要调用的地方只要引用该查询的名字即可。

例如:

@NamedQuery(name=”getPerson”, query= “FROM Person WHERE personid=?1”)

@NamedQueries({ @NamedQuery(name=”getPerson1″, query= “FROM Person WHERE
personid=?1″), @NamedQuery(name=”getPersonList”, query= “FROM Person
WHERE age>?1”) })

Query query = em.createNamedQuery(“getPerson”);

错误8:使用Hibernate应付一切

Hibernate的对象关系映射和各种性能优化使大多数CRUD用例的实现非常简单和高效。这使得Hibernate成为许多项目的一个很好的选择。但这并不意味着Hibernate对于所有的项目都是一个很好的解决方案。

我在我之前的一个帖子和视频中详细讨论过这个问题。JPA和Hibernate为大多数创建、读取或更新一些数据库记录的标准CRUD用例提供了很好的支持。对于这些用例,对象关系映射可以大大提升生产力,Hibernate的内部优化提供了一个很优越的性能。

但是,当你需要执行非常复杂的查询、实施分析或报告用例或对大量记录执行写操作时,结果就不同了。所有这些情况都不适合JPA和Hibernate的查询能力以及基于实体管理的生命周期。

如果这些用例只占应用程序的一小部分,那么你仍然可以使用Hibernate。但总的来说,你应该看看其他的框架,比如jOOQ或者Querydsl,它们更接近于SQL,并且可以避免任何对象关系映射。

错误2:忽略一对一关联的默认FetchType

5.3 排序

JPQL也支持排序,类似于SQL中的语法。例如: Query query =
em.createQuery(“select p from Person p order by p.age, p.birthday
desc”);

错误9:逐个更新或删除巨大的实体列表

在你看着你的Java代码时,感觉逐个地更新或删除实体也可以接受。这就是我们对待对象的方式,对吧?

这可能是处理Java对象的标准方法,但如果你需要更新大量的数据库记录,那么,这就不是一个好方法了。在SQL中,你只需一次定义一个影响多个记录的UPDATE或DELETE语句。数据库将会非常高效地处理这些操作。

不幸的是,用JPA和Hibernate操作起来则没有那么容易。每个实体都有自己的生命周期,而你如果要更新或删除多个实体的话,则首先需要从数据库加载它们。然后在每个实体上执行操作,Hibernate将为每个实体生成所需的SQL
UPDATE或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();
Query query = em.createQuery("UPDATE Book b SET b.price = b.price*1.1");
query.executeUpdate();

接下来,为了防止立即抓取(eager
fetching),你需要做的是对所有的一对一关联更改默认的FetchType。不幸的是,这些关系在默认情况下会被即时抓取。在一些用例中,那并非一个大问题,因为你只是加载了一个额外的数据库记录。但是,如果你加载多个实体,并且每个实体都指定了几个这样的关联,那么很快就会积少成多,水滴石穿。

5.4 聚合查询

JPQL支持AVG、SUM、COUNT、MAX、MIN五个聚合函数。例如:

Query query = em.createQuery(“select max(p.age) from Person p”); Object
result = query.getSingleResult(); String maxAge = result.toString();

错误10:使用实体进行只读操作

JPA和Hibernate支持一些不同的projections。如果你想优化你的应用程序的性能,那么你应该使用projections。最明显的原因是你应该只选择用例中需要的数据。

但这不是唯一的原因。正如我在最近的测试中显示的那样,即使你读取了相同的数据库列,DTO
projections也比实体快得多。

在SELECT子句中使用构造函数表达式而不是实体只是一个小小的改变。但在我的测试中,DTO
projections比实体快40%。当然,两者比较的数值取决于你的用例,而且你也不应该通过这样一个简单而有效的方式来提高性能。

所以,最好确保所有的一对一关联设置FetchType为LAZY。

5.5 更新和删除

JPQL不仅用于查询,还可以用于批量更新和删除。

如:

Query query = em.createQuery(“update Order as o set
o.amount=o.amount+10”); //update 的记录数 int result =
query.executeUpdate();

Query query = em.createQuery(“delete from OrderItem item where
item.order in(from Order as o where o.amount<100)”);
query.executeUpdate();

query = em.createQuery(“delete from Order as o where o.amount<100”);
query.executeUpdate();//delete的记录数

了解如何查找和修复Hibernate性能问题

正如你所看到的,一些小小的问题都可能会减慢你的应用程序。但幸运的是,我们可以轻松避免这些问题并构建高性能持久层。

上面这些仅是我在“Hibernate Performance Tuning Online
Training”中展示的一部分内容。在Hibernate性能调优在线培训中你还将学习如何查找性能问题,以及学习大量久经考验的Hibernate性能调优技术。

@Entity public class Review {      @ManyToOne(fetch = FetchType.LAZY)     @JoinColumn(name = "fk_book")     private Book book;      ...  } 

5.6 更多

与SQL类似,JPQL还涉及到更多的语法,可以参考:

错误3:不要初始化所需的关联

6 事务管理

JPA支持本地事务管理(RESOURCELOCAL)和容器事务管理(JTA),容器事务管理只能用在EJB/Web容器环境中。

事务管理的类型可以在persistence.xml文件中的“transaction-type”元素配置。

JPA中通过EntityManager的getTransaction()方法获取事务的实例(EntityTransaction),之后可以调用事务的begin()、commit()、rollback()方法。

Date: 2012-12-30 16:46:29 CST

Author: Holbrook

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0

当你对所有关联使用FetchType.LAZY以避免错误1和错误2时,你会在代码中发现若干n+1选择问题。当Hibernate执行1个查询来选择n个实体,然后必须为每个实体执行一个额外的查询来初始化一个延迟的获取关联时,就会发生这个问题。

Hibernate透明地获取惰性关系,因此在代码中很难找到这种问题。你只要调用关联的getter方法,我想我们大家都不希望Hibernate执行任何额外的查询吧。

List<Author> authors = em.createQuery("SELECT a FROM Author a", Author.class).getResultList(); for (Author a : authors) {     log.info(a.getFirstName() + " " + a.getLastName() + " wrote "             + a.getBooks().size() + " books."); } 

如果你使用开发配置激活Hibernate的统计组件并监视已执行的SQL语句的数量,n+1选择问题就会更容易被发现。

15:06:48,362 INFO [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {   28925 nanoseconds spent acquiring 1 JDBC connections;   24726 nanoseconds spent releasing 1 JDBC connections;   1115946 nanoseconds spent preparing 13 JDBC statements;   8974211 nanoseconds spent executing 13 JDBC statements;   0 nanoseconds spent executing 0 JDBC batches;   0 nanoseconds spent performing 0 L2C puts;   0 nanoseconds spent performing 0 L2C hits;   0 nanoseconds spent performing 0 L2C misses;   20715894 nanoseconds spent executing 1 flushes (flushing a total of 13 entities and 13 collections);   88175 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections) } 

正如你所看到的JPQL查询和对12个选定的Author实体的每一个调用getBooks方法,导致了13个查询。这比大多数开发人员所以为的还要多,在他们看到如此简单的代码片段的时候。

如果你让Hibernate初始化所需的关联,那么你可以很容易地避免这种情况。有若干不同的方式可以做到这一点。最简单的方法是添加JOIN
FETCH语句到FROM子句中。

Author a = em.createQuery(                 "SELECT a FROM Author a JOIN FETCH a.books WHERE a.id = 1",                 Author.class).getSingleResult(); 

错误4:选择比所需的更多记录

当我告诉你选择太多的记录会减慢应用程序的速度时,我敢保证你一定不会感到惊讶。但是我仍然经常会发现这个问题,当我在咨询电话中分析应用程序的时候。

其中一个原因可能是JPQL不支持你在SQL查询中使用OFFSET和LIMIT关键字。这看起来似乎不能限制查询中检索到的记录数量。但是,你可以做到这一点。你只需要在Query接口上,而不是在JPQL语句中设置此信息。

我在下面的代码片段中做到这一点。我首先通过id排序选定的Author实体,然后告诉Hibernate检索前5个实体。

List<Author> authors = em.createQuery("SELECT a FROM Author a ORDER BY a.id ASC", Author.class)                                     .setMaxResults(5)                                     .setFirstResult(0)                                     .getResultList(); 

错误5:不使用绑定参数

绑定参数是查询中的简单占位符,并提供了许多与性能无关的好处:

  • 它们非常易于使用。
  • Hibernate自动执行所需的转换。
  • Hibernate会自动转义Strings,防止SQL注入漏洞。

而且也可以帮助你实现一个高性能的应用程序。

大多数应用程序执行大量相同的查询,只在WHERE子句中使用了一组不同的参数值。绑定参数允许Hibernate和数据库识别与优化这些查询。

你可以在JPQL语句中使用命名的绑定参数。每个命名参数都以“:”开头,后面跟它的名字。在查询中定义了绑定参数后,你需要调用Query接口上的setParameter方法来设置绑定参数值。

TypedQuery<Author> q = em.createQuery(                 "SELECT a FROM Author a WHERE a.id = :id", Author.class); q.setParameter("id", 1L); Author a = q.getSingleResult(); 

错误6:执行业务代码中的所有逻辑

对于Java开发人员来说,在业务层实现所有的逻辑是自然而然的。我们可以使用我们最熟悉的语言、库和工具。

但有时候,在数据库中实现操作大量数据的逻辑会更好。你可以通过在JPQL或SQL查询中调用函数或者使用存储过程来完成。

让我们快速看看如何在JPQL查询中调用函数。如果你想深入探讨这个话题,你可以阅读我关于存储过程的文章。

你可以在JPQL查询中使用标准函数,就像在SQL查询中调用它们一样。你只需引用该函数的名称,后跟一个左括号,一个可选的参数列表和一个右括号。

Query q = em.createQuery("SELECT a, size(a.books) FROM Author a GROUP BY a.id"); List<Object[]> results = q.getResultList(); 

并且,通过JPA的函数function,你也可以调用数据库特定的或自定义的数据库函数。

TypedQuery<Book> q = em.createQuery(              "SELECT b FROM Book b WHERE b.id = function('calculate', 1, 2)",              Book.class); Book b = q.getSingleResult(); 

错误7:无理由地调用flush方法

这是另一个比较普遍的错误。开发人员在持久化一个新实体或更新现有实体后,调用EntityManager的flush方法时经常会出现这个错误。这迫使Hibernate对所有被管理的实体执行脏检查,并为所有未决的插入、更新或删除操作创建和执行SQL语句。这会减慢应用程序,因为它阻止了Hibernate使用一些内部优化。

Hibernate将所有被管理的实体存储在持久性上下文中,并试图尽可能延迟写操作的执行。这允许Hibernate将同一实体上的多个更新操作合并为一个SQL
UPDATE语句,通过JDBC批处理绑定多个相同的SQL语句,并避免执行重复的SQL语句,这些SQL语句返回你已在当前Session中使用的实体。

作为一个经验法则,你应该避免任何对flush方法的调用。JPQL批量操作是罕见的例外之一,对此我将在错误9中解释。

错误8:使用Hibernate应付一切

Hibernate的对象关系映射和各种性能优化使大多数CRUD用例的实现非常简单和高效。这使得Hibernate成为许多项目的一个很好的选择。但这并不意味着Hibernate对于所有的项目都是一个很好的解决方案。

我在我之前的一个帖子和视频中详细讨论过这个问题。JPA和Hibernate为大多数创建、读取或更新一些数据库记录的标准CRUD用例提供了很好的支持。对于这些用例,对象关系映射可以大大提升生产力,Hibernate的内部优化提供了一个很优越的性能。

但是,当你需要执行非常复杂的查询、实施分析或报告用例或对大量记录执行写操作时,结果就不同了。所有这些情况都不适合JPA和Hibernate的查询能力以及基于实体管理的生命周期。

如果这些用例只占应用程序的一小部分,那么你仍然可以使用Hibernate。但总的来说,你应该看看其他的框架,比如jOOQ或者Querydsl,它们更接近于SQL,并且可以避免任何对象关系映射。

错误9:逐个更新或删除巨大的实体列表

在你看着你的Java代码时,感觉逐个地更新或删除实体也可以接受。这就是我们对待对象的方式,对吧?

这可能是处理Java对象的标准方法,但如果你需要更新大量的数据库记录,那么,这就不是一个好方法了。在SQL中,你只需一次定义一个影响多个记录的UPDATE或DELETE语句。数据库将会非常高效地处理这些操作。

不幸的是,用JPA和Hibernate操作起来则没有那么容易。每个实体都有自己的生命周期,而你如果要更新或删除多个实体的话,则首先需要从数据库加载它们。然后在每个实体上执行操作,Hibernate将为每个实体生成所需的SQL
UPDATE或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(); Query query = em.createQuery("UPDATE Book b SET b.price = b.price*1.1"); query.executeUpdate(); 

错误10:使用实体进行只读操作

JPA和Hibernate支持一些不同的projections。如果你想优化你的应用程序的性能,那么你应该使用projections。最明显的原因是你应该只选择用例中需要的数据。

但这不是唯一的原因。正如我在最近的测试中显示的那样,即使你读取了相同的数据库列,DTO
projections也比实体快得多。

在SELECT子句中使用构造函数表达式而不是实体只是一个小小的改变。但在我的测试中,DTO
projections比实体快40%。当然,两者比较的数值取决于你的用例,而且你也不应该通过这样一个简单而有效的方式来提高性能。

了解如何查找和修复Hibernate性能问题

正如你所看到的,一些小小的问题都可能会减慢你的应用程序。但幸运的是,我们可以轻松避免这些问题并构建高性能持久层。

【编辑推荐】

发表评论

电子邮件地址不会被公开。 必填项已用*标注