其他分享
首页 > 其他分享> > Hibernate5 与 Spring Boot2 最佳性能实践(1)

Hibernate5 与 Spring Boot2 最佳性能实践(1)

作者:互联网

1. 通过字节码增强实现属性延迟加载


默认情况下,实体的属性是立即加载的,即一次加载所有属性。你确定这是你想要的吗?


"描述:"即使目前没有这样的需求,了解可以延迟加载属性也很重要。通过 Hibernate 字节码插装或者 subentities 也可以实现。该特性对于存储了大量 `CLOB`、`BLOB`、`VARBINARY` 类型数据时非常有用。


> 译注:字节码增强(Bytecode enhancement)与字节码插装(Bytecode instrumentation)的区别。字节码增强分在线、离线两种模式。在线模式指在运行时执行,持久化类在加载时得到增强;离线模式指在编译后的步骤中进行增强。字节码插装,指在“运行时”向 Java 类加入字节码。实际上不是在运行时,而是在 Java 类的“加载”过程中完成。


技术要点



[示例代码][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 设置。


技术要点



```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


技术要点


 

示例输出


图片


[示例代码] [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 中的应用步骤。


技术要点



[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)` 默认调用前者。


技术要点



示例输出


图片


[示例代码][10]


[10]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootBatchInsertsEntityManager


你可能也会对下面内容感兴趣



[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` 直接获取数据的示例:


技术要点



[示例代码][13]


[13]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDirectFetching


9. 通过 Spring Data Projection 实现 DTO


获取超出需要的数据是导致性能下降的常见问题之一。不仅如此,得到实体后不做修改也是一样。


"描述:"通过 Spring Data Projection(DTO)从数据库只获取必须的数据。也可以查看例子25至32。


技术要点



示例输出(选择前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 即可。


技术要点



[示例代码] [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` 设置基础外键值。


技术要点



示例输出


命令行只输出一条 `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)。


技术要点



示例输出


图片


[示例代码][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 删除重复数据。


技术要点



示例输出


图片


[示例代码][19]


[19]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootHintPassDistinctThrough


14. 启用脏数据跟踪


Java 反射执行速度慢,通常被看作性能损失。


"描述:"Hibernate 5 之前,脏数据检查机制基于 Java Reflection API。自 Hibernate 5 开始,转而采用了**字节码增强**技术。后者的性能更好,实体数量较多时效果尤其明显。


技术要点



示例输出




[示例代码][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`。


技术要点



[示例代码][22]


[22]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootEnableDirtyTracking


16. 如何正确建立 @OneToMany 双向关系


实现 `@OneToMany` 双向关系有几个陷阱,相信你也希望一开始就能实现正确。


"描述:"下面的示例应用展示了如何正确实现 `@OneToMany` 双向关联。


技术要点



[示例代码][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` 进行查询。


技术要点



[示例代码][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* 生成器。


技术要点



[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()` 方法是多余的。


技术要点



[示例代码][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` 算法进行了优化。


技术要点



示例输出


图片


[示例代码][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`)。


技术要点



示例输出(下面是四个实体得到的单个表)


图片


[示例代码][32]


[32]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootSingleTableInheritance


22. 如何对 SQL 语句统计和断言


如果不对 SQL 语句进行统计和断言,很容易对后台执行的 SQL 语句失去控制,进而造成性能损失。 


"描述:"下面的示例展示了如何对后台 SQL 语句进行统计和断言。统计 SQL 非常有用,能够确保不会生成多余的 SQL(例如,可以对预期的语句数量断言检测 N+1 问题)。


技术要点



示例输出(期望的 SQL 语句数量与实际生成的数量不一致时抛出异常)


图片


[示例代码][33]


[33]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootCountSQLStatements


23. 如何使用 JPA 回调


为实体绑定事件处理时,记得使用 JPA 内建回调,不要重新发明轮子。


"描述:"下面的示例展示了如何启用 JPA 回调(`Pre/PostPersist`、`Pre/PostUpdate`、`Pre/PostRemove` 和 `PostLoad`)。


技术要点



示例输出


图片


[示例代码][34]


[34]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootJpaCallbacks


24. @OneToOne 与 @MapsId


双向 `@OneToOne` 效率不及单向 `@OneToOne`,后者与父表共享主键。


"描述:" 下面的示例展示了为何建议使用 `@OneToOne` 和 `@MapsId` 取代 `@OneToOne`。


技术要点



[示例代码][35]


[35]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootOneToOneMapsId


25. 通过 SqlResultSetMapping 设置 DTO


超出需要获取数据是不好的习惯。另一种常见的错误,没有打算修改实体对象却获取并存储到持久化上下文中,同样会导致性能问题。例25至例32展示了如何使用不同方法提取 DTO。


"描述:"下面的示例展示了如何通过 `SqlResultSetMapping` 和 `EntityManager` 使用 DTO 提取需要的数据。


技术要点



[示例代码][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