vlambda博客
学习文章列表

读书笔记《hands-on-high-performance-with-spring-5》Hibernate性能调优和缓存

Hibernate Performance Tuning and Caching

在上一章中,我们学习了如何在我们的应用程序中使用 JDBC 访问数据库。我们学习了如何优化设计我们的数据库、事务管理和连接池,以从我们的应用程序中获得最佳性能。我们还学习了 如何通过在 JDBC 中使用准备好的语句来防止 SQL 注入。 我们看到了如何使用 JDBC 模板删除用于管理事务、异常和提交的传统样板代码。

在本章中,我们将介绍一些使用 对象关系映射 (ORM) 框架(例如 Hibernate)访问数据库的高级方法。我们将学习如何使用 ORM 以最佳方式改进数据库访问。使用 Spring Data,我们可以进一步删除实现 数据访问对象 (DAO) 接口的样板代码。

以下是我们将在本章中讨论的主题:

  • Introduction to Spring Hibernate and Spring Data
  • Spring Hibernate configuration
  • Common Hibernate traps
  • Hibernate performance tuning

Introduction to Spring Hibernate and Spring Data

正如我们在前几章中看到的,Java 数据库连接 (JDBC) 公开了一个隐藏数据库供应商特定通信的 API。但是,它存在以下限制:

  • JDBC development is very much verbose, even for trivial tasks
  • JDBC batching requires a specific API and is not transparent
  • JDBC does not provide built-in support for explicit locking and optimistic concurrency control
  • There is a need to handle transactions explicitly, with lots of duplicate code
  • Joined queries require additional processing to transform the ResultSet into domain models, or data transfer objects (DTO)

ORM 框架几乎涵盖了 JDBC 的所有限制。 ORM 框架在数据访问层提供对象映射、延迟加载、急切加载、管理资源、级联、错误处理和其他服务。 ORM 框架之一是 Hibernate。 Spring Data 是 Spring 框架实现的一个层,用于提供样板代码并简化对应用程序中使用的不同类型的持久性存储的访问。让我们在以下部分中查看 Spring Hibernate 和 Spring Data 的概述。

Spring Hibernate

Hibernate 是从 EJB 的复杂性和性能问题的挫折中演变而来的。 Hibernate 提供了一种抽象 SQL 的方法,并允许开发人员专注于持久化对象。 Hibernate 作为一个 ORM 框架,有助于将对象映射到关系数据库中的表。 Hibernate 在引入时有自己的标准,并且代码与其标准实现紧密耦合。因此,为了使持久性通用且与供应商无关,Java Community Process (JCP) 开发了一个标准化的 API 规范,称为 Java Persistence API( JPA)。所有的 ORM 框架都开始遵循这个标准,Hibernate 也是如此。

Spring 没有实现自己的 ORM;但是,它支持任何 ORM 框架,例如 Hibernate、iBatis、JDO 等。使用 ORM 解决方案,我们可以轻松地以 Plain Old Java Object (POJO) 对象的形式从关系数据库中持久化和访问数据。 Spring ORM 模块是 Spring JDBC DAO 模块的扩展。 Spring还提供了ORM模板,比如我们在中看到的基于JDBC的模板第 5 章了解 Spring 数据库交互

Spring Data

众所周知,在过去几年中,非结构化和非关系型数据库(称为 NoSQL)变得流行起来。使用 Spring JPA,与关系数据库的对话变得容易;那么,我们如何与非关系型数据库对话呢? Spring 开发了一个名为 Spring Data 的模块,以提供一种与各种数据存储通信的通用方法。

由于每个持久存储有不同的方式来连接和检索/更新数据,Spring Data 提供了一种通用的方法来访问来自每个不同存储的数据。

以下是 Spring Data 的特点:

  • Easy integration with multiple data stores, through various repositories. Spring Data provides generic interfaces for each data store in the form of repositories.
  • The ability to parse and form queries based on repository method names provided the convention is followed. This reduces the amount of code to be written to fetch data.
  • Basic auditing support, such as created by and updated by a user.
  • Fully integrated with the Spring core module.
  • Integrated with the Spring MVC to expose REpresentational State Transfer (REST) controllers through the Spring Data REST module.

以下是 Spring Data 存储库的一个小示例。我们不需要实现这个方法来编写查询并通过 ID 获取帐户;它将由 Spring Data 在内部完成:

public interface AccountRepository extends CrudRepository<Account, Long> {
   Account findByAccountId(Long accountId);
}

Spring Hibernate configuration

我们知道 Hibernate 是一个持久化框架,它提供对象和数据库表之间的关系映射,并且它具有丰富的特性来提高性能和优化资源的使用,例如缓存、急切和延迟加载、事件侦听器等。

Spring 框架提供了对集成许多持久性 ORM 框架的全面支持,Hibernate 也是如此。在这里,我们将看到 Spring 与 JPA,使用 Hibernate 作为持久性提供程序。此外,我们将看到 Spring Data 与使用 Hibernate 的 JPA 存储库。

Spring with JPA using Hibernate

众所周知,JPA 不是一个实现;它是持久性的规范。 Hibernate 框架遵循所有规范,并且还具有自己的附加功能。在应用程序中使用 JPA 规范使我们能够在需要时轻松切换持久性提供程序。

单独使用 Hibernate 需要 SessionFactory,而使用带有 JPA 的 Hibernate 需要 EntityManager。我们将使用 JPA,以下是基于 Spring Java 的 Hibernate JPA 配置:

@Configuration
@EnableTransactionManagement
@PropertySource({ "classpath:persistence-hibernate.properties" })
@ComponentScan({ "com.packt.springhighperformance.ch6.bankingapp" })
public class PersistenceJPAConfig {

  @Autowired
  private Environment env;

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean em = new 
    LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource());
    em.setPackagesToScan(new String[] { 
    "com.packt.springhighperformance
    .ch6.bankingapp.model" });

    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);
    em.setJpaProperties(additionalProperties());

    return em;
  }

  @Bean
  public BeanPostProcessor persistenceTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
  }

  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(this.env.get
    Property("jdbc.driverClassName"));
    dataSource.setUrl(this.env.getProperty("jdbc.url"));
    dataSource.setUsername(this.env.getProperty("jdbc.user"));
    dataSource.setPassword(this.env.getProperty("jdbc.password"));
    return dataSource;
  }

  @Bean
  public PlatformTransactionManager 
  transactionManager(EntityManagerFactory emf) {
      JpaTransactionManager transactionManager = new         
      JpaTransactionManager();
      transactionManager.setEntityManagerFactory(emf);
      return transactionManager;
  }

  @Bean
  public PersistenceExceptionTranslationPostProcessor 
    exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
  }

  private Properties additionalProperties() {
    Properties properties = new Properties();
    properties.setProperty("hibernate.hbm2ddl.auto", 
    this.env.getProperty("hibernate.hbm2ddl.auto"));
    properties.setProperty("hibernate.dialect", 
    this.env.getProperty("hibernate.dialect"));
    properties.setProperty("hibernate.generate_statistics", 
    this.env.getProperty("hibernate.generate_statistics"));
    properties.setProperty("hibernate.show_sql", 
    this.env.getProperty("hibernate.show_sql"));
    properties.setProperty("hibernate.cache.use_second_level_cache", 
    this.env.getProperty("hibernate.cache.use_second_level_cache"));
    properties.setProperty("hibernate.cache.use_query_cache", 
    this.env.getProperty("hibernate.cache.use_query_cache"));
    properties.setProperty("hibernate.cache.region.factory_class", 
    this.env.getProperty("hibernate.cache.region.factory_class"));
    
    return properties;
  }
}

在前面的配置中,我们使用 LocalContainerEntityManagerFactoryBean 类配置了 EntityManager。我们设置 DataSource 以提供有关在何处可以找到我们的数据库的信息。由于我们使用的是 JPA,这是由不同供应商遵循的规范,我们通过设置 HibernateJpaVendorAdapter 和设置供应商特定的附加属性来指定我们在应用程序中使用的供应商。

现在我们已经在我们的应用程序中配置了基于 JPA 的 ORM 框架,让我们看看在使用 ORM 时如何在我们的应用程序中创建 DAO。

以下是 AbstractJpaDAO 类,具有我们所有 DAO 所需的基本通用方法:

public abstract class AbstractJpaDAO<T extends Serializable> {

    private Class<T> clazz;

    @PersistenceContext
    private EntityManager entityManager;

    public final void setClazz(final Class<T> clazzToSet) {
        this.clazz = clazzToSet;
    }

    public T findOne(final Integer id) {
        return entityManager.find(clazz, id);
    }

    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        return entityManager.createQuery("from " + 
        clazz.getName()).getResultList();
    }

    public void create(final T entity) {
        entityManager.persist(entity);
    }

    public T update(final T entity) {
        return entityManager.merge(entity);
    }

    public void delete(final T entity) {
        entityManager.remove(entity);
    }

    public void deleteById(final Long entityId) {
        final T entity = findOne(entityId);
        delete(entity);
    }
}

下面是AccountDAO类,管理Account实体相关的方法:

@Repository
public class AccountDAO extends AbstractJpaDAO<Account> implements IAccountDAO {

  public AccountDAO() {
    super();
    setClazz(Account.class);
  }
}

前面的 DAO 实现示例非常基础,也是我们通常在应用程序中所做的。如果 DAO 抛出诸如 PersistenceException 之类的异常,而不是向用户显示异常,我们希望向最终用户显示正确的、人类可读的消息。为了在发生异常时提供可读的消息,Spring 提供了一个翻译器,我们需要在我们的配置类中定义如下:

@Bean
  public BeanPostProcessor persistenceTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
  }

当我们使用 @Repository 注释来注释我们的 DAO 时,BeanPostProcessor 命令起作用。 PersistenceExceptionTranslationPostProcessor bean 将充当 bean 的顾问,这些 bean 使用 @Repository 注释进行注释。请记住,我们在 第 3 章中了解了建议,调整面向方面的编程。当被告知时,它将重新抛出代码中捕获的特定于 Spring 的未经检查的数据访问异常。

因此,这是使用 Hibernate 的 Spring JPA 的基本配置。现在,让我们看看 Spring Data 配置。

Spring Data configuration

正如我们在介绍中了解到的,Spring Data 提供了一种连接不同数据存储的通用方法。 Spring Data 通过 Repository 接口提供基本的抽象。 Repository 接口是 Spring Data 的核心接口。 Spring Data 提供的基本仓库如下:

  • CrudRepository provides basic CRUD operations
  • PagingAndSortingRepository provides methods to do the pagination and sorting of records
  • JpaRepository provides JPA-related methods, such as flush and insert/update/delete in a batch, and so on

Repository,在 Spring Data 中,消除了 DAO 和模板的实现,例如 HibernateTemplateJdbcTemplate。 Spring Data 是如此抽象,以至于我们甚至不需要为基本的 CRUD 操作编写任何方法实现;我们只需要基于 Repository 定义接口并为方法定义适当的命名约定。 Spring Data 将负责创建基于方法名称的查询,并将其执行到数据库。

Spring Data 的 Java 配置与我们在 Spring JPA 中看到的相同,使用 Hibernate,除了添加了定义存储库。以下是向配置声明存储库的片段:

@Configuration
@EnableTransactionManagement
@PropertySource({ "classpath:persistence-hibernate.properties" })
@ComponentScan({ "com.packt.springhighperformance.ch6.bankingapp" })
    @EnableJpaRepositories(basePackages = "com.packt.springhighperformance.ch6.bankingapp.repository")
public class PersistenceJPAConfig {

}

在本章中,我们不会深入研究 Hibernate 和 Spring Data 特定的开发。但是,我们将深入探讨在没有以最佳方式使用 Hibernate 或 JPA 并在我们的应用程序中配置正确时所面临的问题,以及问题的解决方案,以及实现高性能的最佳实践。让我们看看我们在应用程序中使用 Hibernate 时遇到的常见问题。

Common Hibernate traps

JPA 和 Hibernate ORM 是大多数 Java 应用程序中用于与关系数据库交互的最流行的框架。由于它们使用面向对象领域和底层关系数据库之间的映射来抽象数据库交互并使得实现简单的 CRUD 操作变得非常容易,因此它们的受欢迎程度增加了。

在这种抽象下,Hibernate 使用了大量的优化并将所有数据库交互隐藏在其 API 后面。很多时候,甚至不知道Hibernate 什么时候会执行一条SQL 语句。由于这种抽象,很难发现效率低下和潜在的性能问题。让我们看看我们在应用程序中面临的常见 Hibernate 问题。

Hibernate n + 1 problem

使用 JPA 和 Hibernate 时,fetch 类型对应用程序的性能有很大的影响。我们 应该始终获取满足给定业务需求所需的尽可能多的数据。 为此,我们将关联实体 FetchType 设置为 LAZY .当我们将这些关联实体的获取类型设置为 LAZY 时,我们在应用程序中实现了一个嵌套选择,因为我们不知道这些关联是如何在 ORM 提供的抽象下获取的构架。嵌套选择只不过是两个查询,其中一个是外部或主查询(从表中获取结果),另一个是作为主查询的结果对每一行执行(以获取相应或相关的数据从其他表/秒)。

下面的例子展示了我们是如何无意中实现了嵌套选择的:

Account account = this.em.find(Account.class, accountNumber);
List<Transaction> lAccountTransactions = account.getTransaction();
for(Transaction transaction : lAccountTransactions){
  //.....
}

大多数情况下,开发人员倾向于像前面的示例那样编写代码,并且不会意识到像 Hibernate 这样的 ORM 框架是如何在内部获取数据的。在这里,像 Hibernate 这样的 ORM 框架执行一个查询以获取 account,并执行第二个查询以获取该 account 的事务。两个查询都很好,不会对性能产生太大影响。这两个查询用于实体中的一个关联。

假设我们在 Account 实体中有五个关联:TransactionsUserProfilePayee 等等。当我们尝试从 Account 实体中获取每个关联时,框架会为每个关联执行一个查询,从而产生 1 + 5 = 6 个查询。六个查询不会有太大影响,对吧?这些查询是针对一个用户的,那么如果我们的应用程序的并发用户数是 100 怎么办?然后我们将有 100 * (1 + 5) = 600 个查询。现在,这会影响性能。获取 Account 时的 1 + 5 次查询在 Hibernate 中称为 n + 1 问题。我们将在本章的Hibernate 性能调优部分看到一些避免这个问题的方法。

The open session in view anti-pattern

我们在上一节中看到,为了将获取延迟到需要关联实体时,我们将关联实体的获取类型设置为 LAZY。当我们尝试在我们的表示层访问这些关联实体时(如果它们没有在我们的业务(服务)层初始化),Hibernate 会抛出一个异常,称为 LazyInitializationException。当服务层方法完成执行时,Hibernate 提交事务并关闭会话。因此,在呈现视图时,活动会话无法获取关联实体。

为避免 LazyInitializationException,解决方案之一是在视图中打开会话。这意味着我们保持 Hibernate 会话在视图中打开,以便表示层可以获取所需的关联实体,然后关闭会话。

为了启用这个解决方案,我们需要在我们的应用程序中添加一个 Web 过滤器。如果我们单独使用Hibernate,我们需要添加filterOpenSessionInViewFilter;如果我们使用 JPA,那么我们需要添加 filter OpenEntityManagerInViewFilter。由于我们在本章中使用 JPA 和 Hibernate,以下是添加 filter 的片段:

<filter>
    <filter-name>OpenEntityManagerInViewFilter</filter-name>
    <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
   ....
</filter>
...
<filter-mapping>
    <filter-name>OpenEntityManagerInViewFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Open Session in View (OSIV) 模式 提供的避免异常的解决方案可能看起来并不可怕乍一看;但是,使用 OSIV 解决方案存在问题。让我们讨论一下 OSIV 解决方案的一些问题:

  1. The service layer opens the transaction when its method is invoked and closes it when the method execution completes. Afterward, there is no explicit open transaction. Every additional query executed from the view layer will be executed in auto-commit mode. Auto-commit mode could be dangerous from a security and database point of view. Due to the auto-commit mode, the database needs to flush all of the transaction logs to disk immediately, causing high I/O operations.
  2. It will violate the single responsibility of SOLID principles, or separation of concern because database statements are executed by both the service layer and the presentation layer.
  3. It will lead to the n + 1 problem that we saw in the preceding Hibernate n + 1 problem section, though Hibernate offers some solutions that can cope with this scenario: @BatchSize and FetchMode.SUBSELECT, however, would apply to all of the business requirements, whether we want to or not.
  4. The database connection is held until the presentation layer completes rendering. This increases the overall database connection time and impacts transaction throughput.
  5. If an exception occurs in the fetching session or executing queries in the database, it will occur while rendering the view, so it will not be feasible to render a clean error page to the user.

Unknown Id.generator exception

大多数时候,我们希望对表的主键使用数据库排序。为此,我们知道我们需要在实体的 @GeneratedValue 注释中添加 generator 属性。 @GeneratedValue 注释允许我们为我们的主键定义一个策略。

以下是我们在实体中添加的代码片段,用于为主键设置数据库排序:

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "accountSequence")
private Integer id;

这里,我们认为accountSequence是提供给generator的数据库序列名;但是,当应用程序运行时,它会给出异常。为了解决这个异常,我们用 @SequenceGenerator 注释我们的实体,并将名称命名为 accountSequence,以及 Hibernate 需要使用的数据库序列名称。下面展示了如何设置@SequenceGenerator注解:

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "accountSequence")
@SequenceGenerator(name = "accountSequence", sequenceName = "account_seq", initialValue = 100000)
private Long accountId;

我们看到了实施过程中面临的共同问题。现在,让我们看看如何调整 Hibernate 以实现高性能。

Hibernate performance tuning

在上一节中,我们看到了常见的 Hibernate 陷阱或问题。这些问题并不一定意味着 Hibernate 中的故障;有时,它们是由于对框架的错误使用,在某些情况下,是由于 ORM 框架本身的限制。在接下来的部分中,我们将看到如何提高 Hibernate 的性能。

Approaches to avoid the n + 1 problem

我们已经在 Hibernate n + 1 问题 部分看到了 n + 1 问题。太多的查询会降低我们应用程序的整体性能。所以,为了避免这些额外的延迟加载查询,让我们看看有哪些选项可用。

Fetch join using JPQL

通常,我们调用DAO的findById方法来获取外部或父实体,然后调用关联的getter方法。这样做会导致 n + 1 个查询,因为框架将为每个关联执行额外的查询。相反,我们可以使用 EntityManagercreateQuery 方法编写 JPQL 查询。在这个查询中,我们可以加入我们的关联实体,我们希望通过使用 JOIN FETCH 来获取外部实体。以下是如何获取 JOIN FETCH 实体的示例:

Query query = getEntityManager().createQuery("SELECT a FROM Account AS a JOIN FETCH a.transactions WHERE a.accountId=:accountId", Account.class);
query.setParameter("accountId", accountId);
return (Account)query.getSingleResult();

以下是指出只执行一个查询的日志:

2018-03-14 22:19:29 DEBUG ConcurrentStatisticsImpl:394 - HHH000117: HQL: SELECT a FROM Account AS a JOIN FETCH a.transactions WHERE a.accountId=:accountId, time: 72ms, rows: 3
Transactions:::3
2018-03-14 22:19:29 INFO StatisticalLoggingSessionEventListener:258 - Session Metrics {
    26342110 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    520204 nanoseconds spent preparing 1 JDBC statements;
    4487788 nanoseconds spent executing 1 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;
    13503978 nanoseconds spent executing 1 flushes (flushing a total of 
    4 entities and 1 collections);
    56615 nanoseconds spent executing 1 partial-flushes (flushing a 
    total of 0 entities and 0 collections)
}

JOIN FETCH 告诉 entityManager 在同一个查询中加载选定的实体以及关联的实体。

这种方法的优点是 Hibernate 在一个查询中获取所有内容。从性能的角度来看,这个选项很好,因为所有内容都是在单个查询而不是多个查询中获取的。这减少了每个单独查询到数据库的往返次数。

这种方法的缺点是我们需要编写额外的代码来执行查询。在我们获取一些关联或关系之前,这不是问题。但是,如果实体有很多关联,我们需要为每个不同的用例获取不同的关联,情况会变得更糟。因此,为了满足每个不同的用例,我们需要编写具有所需关联的不同查询。每个用例有太多不同的查询会很混乱,也很难维护。

如果需要不同连接提取 组合的查询数量很少,此选项将是一个好方法。

Join fetch in Criteria API

这种方式与JPQL中的JOIN FETCH相同;但是,这一次,我们使用的是 Hibernate 的 Criteria API。以下是如何在 Criteria API 中使用 JOIN FETCH 的示例:

CriteriaBuilder criteriaBuilder = 
    getEntityManager().getCriteriaBuilder();
    CriteriaQuery<?> query = 
    criteriaBuilder.createQuery(Account.class);
    Root root = query.from(Account.class);
    root.fetch("transactions", JoinType.INNER);
    query.select(root);
    query.where(criteriaBuilder.equal(root.get("accountId"), 
    accountId));

    return (Account)this.getEntityManager().createQuery(query)
   .getSingleResult();

此选项与 JPQL 具有相同的优点和缺点。大多数时候,当我们使用 Criteria API 编写查询时,它是特定于用例的。因此,在这些情况下,此选项可能不是一个大问题,它是减少执行查询量的好方法。

Named entity graph

那么命名实体图是 JPA 2.1 中引入的一个新特性。在这种方法中,我们可以定义需要从数据库中查询的实体图。我们可以使用 @NamedEntityGraph 注释在实体类上定义实体图。

以下是如何在实体类上使用 @NamedEntityGraph 定义图的示例:

@Entity
@NamedEntityGraph(name="graph.transactions", attributeNodes= @NamedAttributeNode("transactions"))
public class Account implements Serializable {

  private static final long serialVersionUID = 1232821417960547743L;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  @Column(name = "account_id", updatable = false, nullable = false)
  private Long accountId;
  private String name;
  
  @OneToMany(mappedBy = "account", fetch=FetchType.LAZY)
  private List<Transaction> transactions = new ArrayList<Transaction>
  ();
.....
}

实体图定义独立于查询,并定义要从数据库中获取哪些属性。实体图可以用作 load fetch 图。 如果使用加载图,实体图定义中未指定的所有属性将继续遵循其默认的FetchType。 如果使用获取图,则仅指定的属性由实体图定义将被视为FetchType.EAGER,所有其他属性将被视为LAZY。以下是如何将命名实体图用作 fetchgraph 的示例:

EntityGraph<?> entityGraph = getEntityManager().createEntityGraph("graph.transactions");
Query query = getEntityManager().createQuery("SELECT a FROM Account AS a WHERE a.accountId=:accountId", Account.class);

query.setHint("javax.persistence.fetchgraph", entityGraph);
query.setParameter("accountId", accountId);
return (Account)query.getSingleResult();

我们不会在本书中详细介绍命名实体图。这是解决 Hibernate 的 n + 1 问题的最佳方法之一。这是 JOIN FETCH 的改进版本。 JOIN FETCH 之上的一个优点是它将被重用于不同的用例。这种方法的唯一缺点是我们必须为要在单个查询中获取的每个关联组合注释命名实体图。因此,如果我们要设置太多不同的组合,这可能会变得非常混乱。

Dynamic entity graph

动态实体图类似于命名实体图,不同之处在于我们可以通过 Java API 动态定义它。以下是如何使用 Java API 定义实体图的示例:

EntityGraph<?> entityGraph = getEntityManager().createEntityGraph(Account.class);
entityGraph.addSubgraph("transactions");
Map<String, Object> hints = new HashMap<String, Object>();
hints.put("javax.persistence.fetchgraph", entityGraph);

return this.getEntityManager().find(Account.class, accountId, hints);

因此,如果我们有很多特定于用例的实体图,那么这种方法将比命名实体图更有优势,在命名实体图上为每个用例添加注释会使代码不可读。我们可以在业务逻辑中保留所有特定于用例的实体图。使用这种方法,缺点是我们需要编写更多的代码,并且为了使代码可重用,我们需要为每个相关的业务逻辑编写更多的方法。

Finding performance issues with Hibernate statistics

大多数时候,我们在生产系统上面临缓慢的响应,而我们的本地或测试系统工作得很好。大多数这些情况是因为数据库查询速度慢。在本地实例中,我们不知道我们在生产中拥有的确切请求量和数据量。那么,我们如何在不将日志添加到我们的应用程序代码中的情况下找出导致问题的查询呢?答案是 Hibernate generate_statistics 配置。

我们需要将 Hibernate 属性 generate_statistics 设置为 true,因为该属性默认为 false。此属性会影响整体性能,因为它会记录所有数据库活动。因此,仅当您要分析慢查询时才启用此属性。此属性将生成汇总的多行日志,显示在数据库交互上花费了多少总时间。

如果我们想记录每个查询的执行,我们需要在日志配置中将org.hibernate.stat启用到DEBUG级别;同样,如果我们想要记录 SQL 查询(带时间),我们需要将 org.hibernate.SQL 启用到 DEBUG 级别。

以下是打印日志的示例:

读书笔记《hands-on-high-performance-with-spring-5》Hibernate性能调优和缓存
Hibernate generate_statistics logs

整体统计信息日志显示使用的 JDBC 连接数、语句、缓存和执行的刷新。我们总是需要先检查语句的数量,看看是否存在 n + 1 问题。

Using query-specific fetching

始终建议仅选择我们的用例所需的那些列。如果您使用 CriteriaQuery,请使用投影来选择所需的列。当表有太多列时,获取整个实体会降低应用程序的性能,因此数据库需要遍历存储页面的每个块来检索它们,而在我们的用例中我们甚至可能不需要所有这些列。此外,如果我们使用实体而不是 DTO 类,则持久性上下文必须管理实体并在需要时获取关联/子实体。这增加了开销。而不是获取整个实体,只获取所需的列:

SELECT a FROM Account a WHERE a.accountId= 123456;

获取特定列,如下所示:

SELECT a.accountId, a.name FROM Account a WHERE a.accountId = 123456;

使用特定于查询的获取的更好方法是使用 DTO 投影。我们的实体由持久化上下文管理。因此,如果我们想要更新它,将 ResultSet 获取到实体会更容易。我们为 setter 方法设置新值,Hibernate 将处理 SQL 语句来更新它。这种简单性伴随着性能的代价,因为 Hibernate 需要对所有托管实体进行脏检查,以确定是否需要保存对数据库的任何更改。 DTO 是 POJO 类,与我们的实体相同,但是,它不是由持久性管理的。

我们可以使用构造函数表达式来获取 JPQL 中的特定列,如下所示:

entityManager.createQuery("SELECT new com.packt.springhighperformance.ch6.bankingapp.dto.AccountDto(a.id, a.name) FROM Account a").getResultList();

同样,我们可以使用 CriteriaQueryJPAMetamodel 来做同样的事情,如下所示:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery q = cb.createQuery(AccountDTO.class);
Root root = q.from(Account.class);
q.select(cb.construct(AccountDTO.class, root.get(Account_.accountNumber), root.get(Account_.name)));

List authors = em.createQuery(q).getResultList();

Caching and its best practices

我们已经在 第 3 章中了解了 Spring 中的缓存是如何工作的, 调整面向方面的编程。在这里,我们将看到 Hibernate 中的缓存是如何工作的,以及 Hibernate 中有哪些不同类型的缓存可用。在 Hibernate 中,存在三种不同类型的缓存,如下所示:

  • First level cache
  • Second level cache
  • Query cache

让我们了解一下 Hibernate 中每种缓存机制是如何工作的。

First level cache

在一级缓存中,Hibernate 将实体缓存在会话对象中。 Hibernate 一级缓存默认开启, 我们不能关闭它。尽管如此,Hibernate 提供了一些方法,通过这些方法我们可以从缓存中删除特定对象,或者从会话对象中完全清除缓存。

由于 Hibernate 在会话对象中进行一级缓存,因此缓存的任何对象都不会对另一个会话可见。当会话关闭时,缓存被清除。我们不会详细介绍这种缓存机制,因为它默认可用,并且无法调整或禁用它。有一些方法可以知道这个级别的缓存,如下:

  • Use the session's evict() method to delete a single object from the Hibernate first level cache
  • Use the session's clear() method to clear the cache completely
  • Use the session's contains() method to check whether an object is present in the Hibernate cache

Second level cache

数据库抽象层(例如 ORM 框架)的一个好处是它们能够透明地缓存数据:

读书笔记《hands-on-high-performance-with-spring-5》Hibernate性能调优和缓存
Caching at the database and application level

应用程序缓存不是许多大型企业应用程序的选项。借助应用程序缓存,我们可以帮助减少从数据库缓存中获取所需数据的往返次数。应用程序级缓存存储整个对象,这些对象是根据哈希表键检索的。在这里,我们不打算讨论应用级缓存;我们将讨论二级缓存。

在 Hibernate 中,与一级缓存不同,二级缓存是 SessionFactory 范围的;因此,它由同一会话工厂中创建的所有会话共享。当启用第二级并查找实体时,以下内容适用:

  1. It will first be checked in the first level cache if the instance is available, and then returned.
  2. If the instance is not present in the first level, it will try to find it in the second level cache, and, if found, it is assembled and returned.
  3. If the instance is not found in the second level cache, it will make the trip to the database and fetch the data. The data is then assembled and returned.

Hibernate 本身不做任何缓存。它提供接口org.hibernate.cache.spi.RegionFactory,缓存提供者做这个接口的实现。在这里,我们将谈谈成熟且使用最广泛的缓存的Ehcache提供程序。为了启用二级缓存,我们需要在持久性属性中添加以下两行:

hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory

一旦启用了二级缓存,我们需要定义我们要缓存哪些实体;我们需要用 @org.hibernate.annotations.Cache 注释这些实体,如下所示:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Account implements Serializable {

}

Hibernate 使用单独的缓存区域来存储实体实例的状态。区域名称是完全限定的类名称。 Hibernate提供了不同的并发策略,我们可以根据自己的需求来使用。以下是不同的并发策略:

  • READ_ONLY: Used only for entities that are never modified; in the case of modification, an exception is thrown. It is used for some static reference data that doesn't change.
  • NONSTRICT_READ_WRITE: The cache is updated when the transaction affecting the cached data is committed. While the cache is updated, there is a chance to obtain stale data from the cache. This strategy is suitable for those requirements that can tolerate eventual consistency. This strategy is useful for data that is rarely updated.
  • READ_WRITE: To avoid obtaining stale data while the cache is updated, this strategy uses soft locks. When a cached entity is updated, the entity in the cache is locked and is released after the transaction is committed. All concurrent transactions will retrieve the corresponding data directly from the database.
  • TRANSACTIONAL: The transaction strategy is mainly used in distributed caches in the JTA environment.

如果没有定义过期和驱逐策略,c疼痛可能会无限增长并最终消耗所有内存。我们需要设置这些策略,这取决于缓存提供者。这里我们使用的是Ehcache,下面是在ehcache.xml中定义过期和驱逐策略的方法:

<ehcache>
    <cache name="com.packt.springhighperformance.ch6.bankingapp.model.Account" maxElementsInMemory="1000" timeToIdleSeconds="0" timeToLiveSeconds="10"/>
</ehcache>

我们中的许多人认为缓存存储了整个对象。但是,它不会存储整个对象,而是将它们存储在反汇编状态:

  • The primary key is not stored, because it is the cache key
  • Transient properties are not stored
  • Collection associations are not stored by default
  • All property values, except for associations, are stored in their original forms
  • Foreign keys for @ToOne associations are stored only with IDs

Query cache

可以通过添加以下 Hibernate 属性来启用查询缓存:

hibernate.cache.use_query_cache=true

启用查询缓存后,我们可以指定要缓存的查询,如下所示:

Query query = entityManager.createQuery("SELECT a FROM Account a WHERE a.accountId=:accountId", Account.class);
query.setParameter("accountId", 7L);
query.setHint(QueryHints.HINT_CACHEABLE, true);
Account account = (Account)query.getSingleResult();

如果我们再次执行查询缓存缓存的相同查询,则以 DEBUG 模式打印的日志如下:

2018-03-17 15:39:07 DEBUG StandardQueryCache:181 - Returning cached query results
2018-03-17 15:39:07 DEBUG SQL:92 - select account0_.account_id as account_1_0_0_, account0_.name as name2_0_0_ from Account account0_ where account0_.account_id=?

Performing updates and deletes in bulk

正如我们所知,当我们更新或删除任何实体时,像 Hibernate 这样的 ORM 框架会执行两个或多个查询。如果我们要更新或删除一些实体,这会很好,但考虑一下我们想要更新或删除 100 个实体的场景。 Hibernate 将执行 100 个 SELECT 查询来获取实体,并执行另外 100 个查询来更新或删除实体。

我们知道,为了让任何应用程序获得更好的性能,需要执行较少数量的数据库语句。如果我们使用 JPQL 或原生 SQL 执行相同的更新或删除,则可以在单个语句中完成。 Hibernate 作为 ORM 框架提供了很多好处,可以帮助我们专注于业务逻辑,而不是数据库操作。在 Hibernate 可能代价高昂的场景中,例如批量更新和删除,我们应该使用原生数据库查询来避免开销并获得更好的性能。

以下是我们可以执行原生查询UPDATE银行收件箱中所有用户的电子邮件作为读取的一种方式:

entityManager.createNativeQuery("UPDATE mails p SET read = 'Y' WHERE user_id=?").setParameter(0, 123456).executeUpdate();

我们可以使用 Hibernate 方法和本地查询来更新批量数据来衡量性能差异,只需记录 System.currentTimeMillis()。性能应该会显着提高,本机查询比 Hibernate 方法快 10 倍。

原生查询肯定会提升批量操作性能,但同时也存在一级缓存的问题,不会触发任何实体生命周期事件。众所周知,Hibernate 将我们在会话中使用的所有实体存储在一级缓存中。它有利于后写优化,并避免在同一会话中对同一实体执行重复的选择语句。但是,对于本机查询,Hibernate 不知道哪些实体被更新或删除,并相应地更新第一级缓存。如果我们在同一会话中执行本机查询之前获取实体,它将继续使用缓存中实体的过时版本。以下是使用本机查询的一级缓存问题示例:

private void performBulkUpdateIssue(){
    Account account = this.entityManager.find(Account.class, 7L);
    
    entityManager.createNativeQuery("UPDATE account a SET name = 
    name 
    || '-updated'").executeUpdate();
    _logger.warn("Issue with Account Name: "+account.getName());

    account = this.entityManager.find(Account.class, 7L);
    _logger.warn("Issue with Account Name: "+account.getName());
  }

此问题的解决方案是手动更新一级缓存,方法是在本机查询执行之前分离实体并在本机查询执行后将其附加回来。为此,请执行以下操作:

private void performBulkUpdateResolution(){
    //make sure you are passing right account id    
    Account account = this.entityManager.find(Account.class, 7L);

    //remove from persistence context
    entityManager.flush();
    entityManager.detach(account);
    entityManager.createNativeQuery("UPDATE account a SET name = 
    name 
    || '-changed'").executeUpdate();
    _logger.warn("Resolution Account Name: "+account.getName());
    
    account = this.entityManager.find(Account.class, 7L);
    _logger.warn("Resolution Account Name: "+account.getName());
  }

在执行本机查询之前调用 flush()detach() 方法。 flush() 方法告诉 Hibernate 将更改的实体从一级缓存写入数据库。这是为了确保我们不会丢失任何更新。

Hibernate programming practices

到现在为止,我们看到了 Hibernate 在没有得到最佳使用时的问题,以及如何使用 Hibernate 来获得更好的性能。以下是使用 JPA 和 Hibernate 时要遵循的最佳实践(就缓存而言,一般而言)以获得更好的性能。

Caching

以下是与 Hibernate 中不同缓存级别相关的一些编程技巧:

  • Make sure to use the same version of hibernate-ehcache as the version of Hibernate.
  • Since Hibernate caches all of the objects into the session first level cache, when running bulk queries or batch updates, it's necessary to clear the cache at certain intervals to avoid memory issues.
  • When caching an entity using the second level cache, collections inside of the entity are not cached by default. In order to cache the collections, annotate the collections within the entity with @Cacheable and @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE). Each collection is stored in a separate region in the second level cache, where the region name is the fully qualified name of the entity class, plus the name of the collection property. Define the expiration and eviction policy separately for each collection that's cached.
  • When DML statements are executed using JPQL, the cache for those entities will be updated/evicted by Hibernate; however, when using the native query, the entire second level cache will be evicted, unless the following detail is added to native query execution when using Hibernate with JPA:
Query nativeQuery = entityManager.createNativeQuery("update Account set name='xyz' where name='abc'");

nativeQuery.unwrap(org.hibernate.SQLQuery.class).addSynchronizedEntityClass(Account.class);

nativeQuery.executeUpdate();
  • In the case of query caching, there will be one cache entry for each combination of query and parameter values, so queries, where different combinations of parameter values are expected, are not good for caching.
  • In the case of query caching, a query that fetches the entity classes for which there are frequent changes in the database is not a good candidate for caching, because the cache will be invalidated when any entity involved in the query is changed.
  • All query cache results are stored in the org.hibernate.cache.internal.StandardQueryCache region. We can specify the expiration and eviction policies for this region. Also, if required, we can set a different region for a particular query to cache by using the query hint org.hibernate.cacheRegion.
  • Hibernate keeps last update timestamps in a region named org.hibernate.cache.spi.UpdateTimestampsCache for all query cached tables. Hibernate uses this to verify that cached query results are not stale. It is best to turn off automatic eviction and expiration for this cache region, because entries in this cache must not be evicted/expired, as long as there are cached query results in cache results regions.

Miscellaneous

以下是在您的应用程序中实现更好性能的一般 Hibernate 最佳实践:

  • Avoid enabling generate_statistics on the production system; rather, analyze issues by enabling generate_statistics on a staging or a replica of the production system.
  • Hibernate always updates all database columns, even though we update one or a few columns only. All of the columns in the UPDATE statement would take more time than a few columns. In order to achieve high performance and avoid using all of the columns in an UPDATE statement, only include the columns that are actually modified and use the @DynamicUpdate annotation on an entity. This annotation tells Hibernate to generate a new SQL statement for each update operation, with only modified columns.
  • Set the default FetchType as LAZY for all associations, and use query-specific fetching, using JOIN FETCH, named entity graphs, or dynamic entity graphs, to avoid the n + 1 issue and improve performance.
  • Always use bind parameters to avoid SQL injections and improve performance. When used with bind parameters, Hibernate and the database optimizes queries if the same query is executed multiple times.
  • Perform UPDATE or DELETE in a huge list in bulk, instead of performing them one-by-one. We already discussed this in the Performing updates and deletes in bulk section.
  • Never use entities for read-only operations; rather, use different projections provided by JPA and Hibernate. One that we already saw was the DTO projection. For read-only requirements, changing the entity to a constructor expression in SELECT is very easy, and high performance will be achieved.
  • With the introduction of the Stream API in Java 8.0, many people used its features to process huge data retrieved from a database. Stream is designed to work on huge data. But there are certain things that a database can do better than the Stream API. Don't use the Stream API for the following requirements:
    • Filter data: The database can filter data more efficiently than the Stream API, which we can do using the WHERE clause
    • Limiting data: The database provides more efficient results than the Stream API when we want to limit the number of data to be retrieved
    • Sort data: The database can sort more efficiently than the Stream API by using the ORDER BY clause
  • Use order instead of sorting, especially for huge associated entities of data. Sorting is Hibernate-specific, and not a JPA specification:
    • Hibernate sorts using the Java Comparator in memory. However, the same desired order of data can be retrieved from the database by using the @OrderBy annotation on associated entities.
    • If the column name is not specified, @OrderBy will be done on the primary key.
    • Multiple columns can be specified in @OrderBy, comma-separated.
    • The database handles @OrderBy more efficiently than implementing sorting in Java. The following is a code snippet, as an example:
@OneToMany(mappedBy = "account", fetch=FetchType.LAZY)
@OrderBy("created DESC")
private List<Transaction> transactions = new ArrayList<Transaction>();
  • Hibernate regularly performs dirty checks on all entities that are associated with the current PersistenceContext, to detect required database updates. For entities that never update, such as read-only database views or tables, performing dirty checks is an overhead. Annotate these entities with @Immutable, and Hibernate will ignore them in all dirty checks, improving performance.
  • Never define unidirectional one-to-many relationships; always define bidirectional relationships. If a unidirectional one-to-many relationship is defined, Hibernate will need an extra table to store the references of both of the tables, just like in many-to-many relationships. There would be many extra SQL statements executed in the case of a unidirectional approach, which would not be good for performance. For better performance, annotate @JoinColumn on the owning side of the entity, and use the mappedby attribute on the other side of the entity. This will reduce the number of SQL statements, improving performance. Adding and removing an entity from a relationship needs to be handled everywhere explicitly; hence, it is recommended to write helper methods in the parent entity, as follows:
@Entity
public class Account {
 
    @Id
    @GeneratedValue
    private Integer id;
  

    @OneToMany(mappedBy = "account")
    private List<Transaction> transactions = new ArrayList<>();
  

    public void addTransaction(Transaction transaction) {
        transactions.add(transaction);
        transaction.setPost(this);
    }
 
    public void removeTransaction(Transaction transaction) {
        transactions.remove(transaction);
        transaction.setPost(null);
    }
}

@Entity
public class Transaction {
 
    @Id
    @GeneratedValue
    private Integer id;
  
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    private Account account;
}

Summary

我们从使用 JPA 和 Spring Data 的 ORM 框架 Hibernate 的基本配置开始本章。我们专注于生产中面临的常见 ORM 问题。在本章中,我们学习了在使用 Hibernate 进行数据库操作和实现高性能时所面临的常见问题的最佳解决方案。我们学习了一些有用的技巧,以了解在开发基于 ORM 的框架时要遵循的最佳实践,以便从开发阶段实现高性能,而不是在生产系统中遇到问题时解决问题。

为了优化和高性能,下一章提供有关 Spring 消息传递优化的信息。如您所知,消息传递框架企业应用程序连接多个客户端并提供可靠性、异步通信和松散耦合。构建框架以提供各种好处;但是,如果我们不以最佳方式使用它们,我们就会面临问题。类似地,如果有效使用,某些与队列配置和可伸缩性相关的参数将最大化我们企业应用程序的 Spring 消息传递框架中的吞吐量。