Hibernate5 与 Spring Boot2 最佳性能实践(1)
作者:互联网
1. 通过字节码增强实现属性延迟加载
默认情况下,实体的属性是立即加载的,即一次加载所有属性。你确定这是你想要的吗?
"描述:"即使目前没有这样的需求,了解可以延迟加载属性也很重要。通过 Hibernate 字节码插装或者 subentities 也可以实现。该特性对于存储了大量 `CLOB`、`BLOB`、`VARBINARY` 类型数据时非常有用。
> 译注:字节码增强(Bytecode enhancement)与字节码插装(Bytecode instrumentation)的区别。字节码增强分在线、离线两种模式。在线模式指在运行时执行,持久化类在加载时得到增强;离线模式指在编译后的步骤中进行增强。字节码插装,指在“运行时”向 Java 类加入字节码。实际上不是在运行时,而是在 Java 类的“加载”过程中完成。
技术要点
在 Maven `pom.xml` 中激活 Hibernate 字节码插装(像下面这样使用 Maven 字节码增强插件)
为需要延迟加载的列标记 `@Basic(fetch = FetchType.LAZY)`
在 View 中禁用 Open Session
[示例代码][1]
[1]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootAttributeLazyLoadingBasic
2. 通过 Log4J 2 查看绑定参数
开发中,如果不能监测调用的SQL语句绑定的参数,很有可能造成潜在的性能损失(例如 N+1 问题)。
> 译注:“N+1 问题”即执行一次查询 N 条主数据后,由于关联引起的 N 次从数据查询,因此会带来了性能问题。一般来说,通过延迟加载可以部分缓解 N+1 带来的性能问题。
"更新:"如果项目中"已经"配置了 Log4J 2,可以采用以下方案。如果没有配置,建议使用 `TRACE`(感谢 Peter Wippermann 的建议)或 `log4jdbc`(感谢 Sergei Poznanski 的建议以及 [SO][2] 的答案)。这两种方案不需要取消默认 Spring Boot 日志功能。使用 `TRACE` 的例子参见[这里][3],`log4jdbc` 的示例参见[这里][4]。
[2]:https://stackoverflow.com/questions/45346905/how-to-log-sql-queries-their-parameters-and-results-with-log4jdbc-in-spring-boo/45346996#45346996
[3]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootLogTraceViewBindingParameters
[4]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootLog4JdbcViewBindingParameters
"基于 Log4J 2 方案:"最好的办法还是监视SQL语句绑定的参数,可以通过 Log4J 2 logger 设置。
技术要点
在 Maven `pom.xml` 中移除默认 Spring Boot 日志依赖(参考上面的更新说明)
在 Maven `pom.xml` 中加入 Log4j 2 依赖
在 `log4j2.xml` 中添加以下配置:
```xml
<Logger name="org.hibernate.type.descriptor.sql" level="trace"/>
```
示例输出
[示例代码][5]
[5]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootLog4j2ViewBindingParameters
3.如何通过 datasource-proxy 监视查询细节
如果无法保证批处理正常工作,很容易会遇到严重的性能损失。即使已经配置了批处理并且认为会在后台运行,还是有一些情况会造成批处理被禁用。为了确保这一点,可以使用 `hibernate.generate_statistics` 显示详细信息(包括批处理细节),也可以使用 datasource-proxy。
"描述:"通过 [datasource-proxy][6] 查看查询细节(包括查询类型、绑定参数、批处理大小等)。
[6]:https://github.com/ttddyy/datasource-proxy
技术要点
在 Maven `pom.xml` 中加入 `datasource-proxy` 依赖
为 `DataSource` bean 创建 Post Processor 进行拦截
用 `ProxyFactory` 和 `MethodInterceptor` 实现包装 `DataSource` bean
示例输出
[示例代码] [here][7]
[7]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDataSourceProxy
4. 通过 saveAll(Iterable<S> entities) 在 MySQL(或其他 RDBMS)中执行批量插入
默认情况下,100次插入会生成100个 `INSERT` 语句,带来100个数据库行程开销。
"描述:"批处理机制对 `INSERT`、`UPDATE` 和 `DELETE` 进行分组,能够显著降低数据库行程数。批处理插入可以调用 `SimpleJpaRepository#saveAll(Iterable<S> entities)` 方法,下面是在 MySQL 中的应用步骤。
技术要点
在 `application.properties` 中设置 `spring.jpa.properties.hibernate.jdbc.batch_size`
在 `application.properties`中设置 `spring.jpa.properties.hibernate.generate_statistics`:检查批处理是否正常工作
在 `application.properties` JDBC URL 中设置 `rewriteBatchedStatements=true`:针对 MySQL 优化
在 `application.properties` JDBC URL 中设置 `cachePrepStmts=true`:启用缓存。启用 prepStmtCacheSize、prepStmtCacheSqlLimit 等参数前必须设置此参数
In `application.properties` JDBC URL 中设置 `useServerPrepStmts=true`:切换到服务端生成预处理语句,可能会带来显著性能提升
在实体类中使用 [assigned generator][8]:MySQL `IDENTITY` 会禁用批处理
在实体类中为 `Long` 属性添加 `@Version` 注解:不仅可以避免批处理生成额外的 `SELECT`,还能减少多个请求事务中丢失 update。使用 `merge()` 替代 `persist()` 时会生成额外的 `SELECT`。`saveAll()` 实际调用 `save()`,如果实体对象ID非空会被看作已有对象。这时调用 `merge()` 触发 Hibernate 生成 `SELECT` 检查数据库中是否存在相同标识
注意:传入 `saveAll()` 的对象数量不要“覆盖“持久化上下文。通常情况下,`EntityManager` 会定期执行 flush 和 clear,但是 `saveAll()` 执行过程中不会。因此,如果 `saveAll()` 传入了大量数据,所有数据都会命中持久化上下文(1级缓存),并一直保持直到执行 flush 操作。这里的配置适用于规模较小的数据,对于大数据的情况请参考例5
[8]:https://vladmihalcea.com/how-to-combine-the-hibernate-assigned-generator-with-a-sequence-or-an-identity-column/
示例输出
[示例代码][9]
[9]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootBatchInsertsJpaRepository
5. 通过 EntityManager 在 MySQL(或其他 RDBMS)中执行批量插入
批处理可以提高性能,但是在执行 flush 前需要关注持久化上下文中的数据量。在内存中存储大量数据会导致性能下降,例4中的方法只适合数据量相对较少的情况。
"描述:"通过 `EntityManager` 在 MySQL(或其他 RDBMS)中执行批量插入。这种方法可以更好地控制持久化上下文(1级缓存) `flush()` 和 `clear()` 操作。Spring Boot 中 `saveAll(Iterable<S>entities)` 做不到这点。其它好处,可以调用 `persist()` 而不是 `merge()` 方法,Spring Boot `saveAll(Iterable< S>entities)` 与 `save(S entity)` 默认调用前者。
技术要点
在 `application.properties` 中设置 `spring.jpa.properties.hibernate.jdbc.batch_size`
在 `application.properties` 中设置 `spring.jpa.properties.hibernate.generate_statistics`:检查批处理是否正常工作
在 `application.properties` JDBC URL 中设置 `rewriteBatchedStatements=true`:针对 MySQL 优化
在 `application.properties` JDBC URL 中设置 `withcachePrepStmts=true`:启用缓存。启用 prepStmtCacheSize、prepStmtCacheSqlLimit 等参数前必须设置此参数
在 `application.properties` JDBC URL 中设置 `withuseServerPrepStmts=true`:切换到服务端生成预处理语句,可能会带来显著性能提升
在实体类中使用 [assigned generator][8]:MySQL `IDENTITY` 会禁用批处理
在 DAO 中定期对持久化上下文执行 flush 和 clear,避免“覆盖“持久化上下文
示例输出
[示例代码][10]
[10]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootBatchInsertsEntityManager
你可能也会对下面内容感兴趣
[6. 如何在 MySQL 中通过 JpaContext/EntityManager 执行批量插入][11]"
[7. 在 MySQL 中实现 Session 级批处理(Hibernate 5.2 或更高版本)][12]"
[11]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootBatchInsertsEntityManagerViaJpaContext
[12]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootBatchInsertsViaSession
8. 通过 Spring Data/EntityManager/Session 直接获取结果
从数据库获取数据的方式决定了应用的执行效率,要优化查询必须了解每种获取数据方法的特点。在了解实体类'主键'的情况下,*直接获取*是最简单且实用的办法。
"描述:"下面是使用 Spring Data、`EntityManager` 和 Hibernate `Session` 直接获取数据的示例:
技术要点
通过 Spring Data 直接获取数据,调用 `findById()`
通过 `EntityManager#find()` 直接获取数据
通过 Hibernate `Session#get()` 直接获取数据
[示例代码][13]
[13]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDirectFetching
9. 通过 Spring Data Projection 实现 DTO
获取超出需要的数据是导致性能下降的常见问题之一。不仅如此,得到实体后不做修改也是一样。
"描述:"通过 Spring Data Projection(DTO)从数据库只获取必须的数据。也可以查看例子25至32。
技术要点
编写接口(projection),包含数据库所需数据表指定列的 getter 方法
编写返回 `List<projection>` 的查询
可能的话,要限制返回的行数(例如,通过 `LIMIT`)。这个例子中,使用了 Spring Data repository 的内置 query builder 机制
示例输出(选择前2列,只获取 "name" 和 "age")
[示例代码][14]
[14]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoViaProjections
10. 如何在 MySQL 中存储 UTC 时区
在数据库中存储不同格式或指定格式的日期、时间和时间戳会带来日期转换问题。
"描述:" 这个例子展示了如何在 MySQL 中以 UTC 时区存储日期、时间和时间戳。对其他 RDBMS(例如 PostgreSQL),只要移除 `useLegacyDatetimeCode=false` 对应调整 JDBC URL 即可。
技术要点
`spring.jpa.properties.hibernate.jdbc.time_zone=UTC`
`spring.datasource.url=jdbc:mysql://localhost:3306/db_screenshot?useLegacyDatetimeCode=false`
[示例代码] [here][15]
> 译注:运行时修改示例 url 为 jdbc:mysql://localhost:3306/db_screenshot?createDatabaseIfNotExist=true&useLegacyDatetimeCode=false,设置参数 spring.jpa.hibernate.ddl-auto=create
[15]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootUTCTimezone
11. 通过 Proxy 得到父实体
执行的 SQL 越多,性能损失越大。尽可能减少执行的 SQL 数量非常重要,通过 Reference 是最易于使用的优化方法。
"描述:"`Proxy` 在子实体可以通过指向父实体的一个持久化引用表示时非常有用。这种情况下,执行`SELECT` 语句从数据库获得父实体会带来性能损失且没有意义。Hibernate 能够对未初始化的 `Proxy` 设置基础外键值。
技术要点
底层依赖 `EntityManager#getReference()`
在 Spring 中调用 `JpaRepository#getOne()`
在这个示例中,使用了 Hibernate `load()` 方法
示例中有 `Tournament` 和 `TennisPlayer` 两个实例,一个 tournament 包含多个 player(`@OneToMany`)
通过 `Proxy` 获取 tournament 对象(不会触发 `SELECT`),接着创建一个 TennisPlayer 对象,把 `Proxy` 设为 player 的 tournament,最后保存 player(触发 `INSERT` 操作,在 tennis player 中插入 `tennis_player`)
示例输出
命令行只输出一条 `INSERT`,没有 `SELECT` 语句。
[示例代码][16]
[16]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootPopulatingChildViaProxy
12. N+1 问题
“N+1问题”可能造成严重的性能损失。减少损失的首要任务是定位问题。
N+1 本质上是一个延迟加载问题(预先加载也不例外)。缺乏对实际执行SQL进行监测,很可能会造成 N+1 问题,最好的解决办法是 JOIN+DTO(例36至例42)。
技术要点
定义 `Category` 和 `Product` 两类实体,关系为 `@OneToMany`
延迟加载 `Product`,不主动加载 `Category`(只生成1条查询)
循环读取 `Product` 集合, 对每个产品获取 `Category`(生成N条查询)
示例输出
[示例代码][17]
[17]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootSimulateNPlus1
13. 通过 HINT_PASS_DISTINCT_THROUGH 优化 Distinct SELECT
把 `SELECT DISTINCT` 传递给 RDBMS 会[影响性能][18]。
[18]:http://in.relation.to/2016/08/04/introducing-distinct-pass-through-query-hint/
"描述:" Hibernate 5.2.2 开始,可以通过 `HINT_PASS_DISTINCT_THROUGH` 优化 `SELECT DISTINCT`。不会把 `DISTINCT` 关键字传给 RDBMS,而是由 Hibernate 删除重复数据。
技术要点
使用 `@QueryHints(value = @QueryHint(name = HINT_PASS_DISTINCT_THROUGH, value = "false"))`
示例输出
[示例代码][19]
[19]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootHintPassDistinctThrough
14. 启用脏数据跟踪
Java 反射执行速度慢,通常被看作性能损失。
"描述:"Hibernate 5 之前,脏数据检查机制基于 Java Reflection API。自 Hibernate 5 开始,转而采用了**字节码增强**技术。后者的性能更好,实体数量较多时效果尤其明显。
技术要点
在 `pom.xml` 中增加插件配置(例如,使用 Maven bytecode enhancement 插件)
示例输出
字节码增强效果可以在 `User.class` 上[看到][20]
[示例代码][21]
[20]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/blob/master/HibernateSpringBootEnableDirtyTracking/Bytecode%20Enhancement%20User.class/User.java
[21]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootEnableDirtyTracking
15. 在实体和查询上使用 Java 8 Optional
把 Java 8 `Optional` 作为处理 `null` 的“银弹”可能弊大于利,最好的方式还是按照设计的意图使用。
"描述:"下面的示例展示了如何在实体和查询中正确使用 Java 8 `Optional`。
技术要点
使用 Spring Data 内建查询方法返回 `Optional`(例如 `findById()`)
自己编写查询方法返回 `Optional`
在实体 getter 方法中使用 `Optional`
可以使用 `data-mysql.sql` 脚本验证不同场景
[示例代码][22]
[22]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootEnableDirtyTracking
16. 如何正确建立 @OneToMany 双向关系
实现 `@OneToMany` 双向关系有几个陷阱,相信你也希望一开始就能实现正确。
"描述:"下面的示例应用展示了如何正确实现 `@OneToMany` 双向关联。
技术要点
"总是"建立父子级联
对父亲标记 `mappedBy`
对父亲使用 `orphanRemoval`,移除没有引用的子对象
在父节点上使用 helper 方法实现关联同步
"总是"使用延迟加载
使用业务主键或实体标识符,参考[这篇介绍][23]覆写 `equals()` 和 `hashCode()` 方法。
[示例代码][24]
[23]:https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/
[24]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootOneToManyBidirectional
17. JPQL/HQL 查询数据
在不具备直接查询的情况下,可以考虑通过 JPQL/HQL 查询数据。
"描述:"下面的示例展示了如何通过 `JpaRepository`、`EntityManager` 和 `Session` 进行查询。
技术要点
对 `JpaRepository` 使用 `@Query` 注解或者创建 Spring Data Query
对 `EntityManager` 与 `Session` 使用 `createQuery()` 方法
[示例代码][25]
[25]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootQueryFetching
18. 避免在 MySQL 与 Hibernate 5 中使用 AUTO Generator 类型
在 MySQL 开发过程中,尽量避免使用 `TABLE` 生成器,最好[永远不要使用][26]。
[26]:https://vladmihalcea.com/why-you-should-never-use-the-table-identifier-generator-with-jpa-and-hibernate/
"描述:" 在使用 MySQL 和 Hibernate 5 开发时,`GenerationType.AUTO` 类型的生成器会调用 `TABLE` 生成器,造成严重的性能损失。可以通过 `GenerationType.IDENTITY` 调用 `IDENTITY` 生成器或者使用 *native* 生成器。
技术要点
使用 `GenerationType.IDENTITY` 取代 `GenerationType.AUTO`
使用[示例代码][27],调用 *native* 生成器
[27]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootAutoGeneratorType
示例输出
[示例代码][28]
[28]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootAutoGeneratorType
19. 多余的 save() 调用
大家都喜欢使用 `save()`。由于 Hibernate 采用了脏数据检查机制避免多余调用,`save()` 对于托管实体并不适用。
"描述:" 下面的示例展示了对于托管实体调用 `save()` 方法是多余的。
技术要点
Hibernate 会为每个托管实体调用 `UPDATE` 语句,不需要显示调用 `save()` 方法
多余的调用意味着性能损失(参见[这篇文章][29])
[示例代码][30]
[29]https://vladmihalcea.com/jpa-persist-and-merge/
[30]https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootRedundantSave
20. PostgreSQL (BIG)SERIAL 与批量插入
在 PostgreSQL 中,使用 `GenerationType.IDENTITY` 会禁用批量插入。
"描述:" `(BIG)SERIAL` 与 MySQL 的 `AUTO_INCREMENT` 功能“接近”。在这个示例中,我们通过 `GenerationType.SEQUENCE` 开启批量插入,同时通过 `hi/lo` 算法进行了优化。
技术要点
使用 `GenerationType.SEQUENCE` 取代 `GenerationType.IDENTITY`
通过 `hi/lo` 算法在一个数据库行程中完成多个标识符读取(还可以使用 Hibernate `pooled` 和 `pooled-lo` 标识符生成器,它们是 `hi/lo` 的改进版)
示例输出
[示例代码][31]
[31]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootBatchingAndSerial
> 译注:示例中 `createDatabaseIfNotExist=true` 参数对 PostgreSQL 无效,需要手动创建 `db_users` 数据库。
21. JPA 继承之 Single Table
JPA 支持 `SINGLE_TABLE`、`JOINED` 和 `TABLE_PER_CLASS` 继承策略,有着各自优缺点。以 `SINGLE_TABLE` 为例,读写速度快但不支持对子类中的列设置 `NOT NULL`。
"描述:"下面的示例展示了 JPA Single Table 继承策略(`SINGLE_TABLE`)。
技术要点
这是 JPA 默认的继承策略(`@Inheritance(strategy=InheritanceType.SINGLE_TABLE)`)
所有继承结构中的类都会被映射到数据库中的单个表
示例输出(下面是四个实体得到的单个表)
[示例代码][32]
[32]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootSingleTableInheritance
22. 如何对 SQL 语句统计和断言
如果不对 SQL 语句进行统计和断言,很容易对后台执行的 SQL 语句失去控制,进而造成性能损失。
"描述:"下面的示例展示了如何对后台 SQL 语句进行统计和断言。统计 SQL 非常有用,能够确保不会生成多余的 SQL(例如,可以对预期的语句数量断言检测 N+1 问题)。
技术要点
在 Maven `pom.xml` 中添加 `datasource-proxy` 依赖和 Vlad Mihalcea 的 `db-util`
新建 `ProxyDataSourceBuilderwithcountQuery()`
`SQLStatementCountValidator.reset()` 重置计数
通过 `assertInsert{Update/Delete/Select}Count(long expectedNumberOfSql` 对 `INSERT`、`UPDATE`、`DELETE` 和 `SELECT` 进行断言
示例输出(期望的 SQL 语句数量与实际生成的数量不一致时抛出异常)
[示例代码][33]
[33]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootCountSQLStatements
23. 如何使用 JPA 回调
为实体绑定事件处理时,记得使用 JPA 内建回调,不要重新发明轮子。
"描述:"下面的示例展示了如何启用 JPA 回调(`Pre/PostPersist`、`Pre/PostUpdate`、`Pre/PostRemove` 和 `PostLoad`)。
技术要点
在实体中编写回调方法并挑选合适的注解
Bean Class 中带注解的回调方法返回类型必须为 `void` 且不带参数
示例输出
[示例代码][34]
[34]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootJpaCallbacks
24. @OneToOne 与 @MapsId
双向 `@OneToOne` 效率不及单向 `@OneToOne`,后者与父表共享主键。
"描述:" 下面的示例展示了为何建议使用 `@OneToOne` 和 `@MapsId` 取代 `@OneToOne`。
技术要点
在子实体上使用 `@MapsId`
对于 `@OneToOne` 关联,基本上会与父表共享主键。
[示例代码][35]
[35]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootOneToOneMapsId
25. 通过 SqlResultSetMapping 设置 DTO
超出需要获取数据是不好的习惯。另一种常见的错误,没有打算修改实体对象却获取并存储到持久化上下文中,同样会导致性能问题。例25至例32展示了如何使用不同方法提取 DTO。
"描述:"下面的示例展示了如何通过 `SqlResultSetMapping` 和 `EntityManager` 使用 DTO 提取需要的数据。
技术要点
使用 `SqlResultSetMapping` 和 `EntityManager`
使用 Spring Data Projection 时,请检查例9中的注意事项
[示例代码][36]
[36]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoSqlResultSetMapping
标签:Hibernate5,Hibernate,SpringBoot,示例,Spring,Boot2,github,https,com 来源: https://blog.51cto.com/u_15127686/2832723