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

[上一篇][1]介绍了 Hibernate 5 和 Spring Boot 2 一些性能方面的最佳实践,这一篇会继续介绍剩下的内容。让我们开始吧!


[26. 如何通过 SqlResultSetMapping & NamedNativeQuery 提取 DTO][2]

[27. 如何通过 javax.persistence.Tuple 和原生 SQL 提取 DTO][3]

[28. 如何通过 javax.persistence.Tuple 和 JPQL 提取 DTO][4]

[29. 如何通过 Constructor 表达式和 JPQL 提取 DTO][5]

[30. 如何通过 ResultTransformer 和原生 SQL 提取 DTO][6]

[31. 如何通过 ResultTransformer 和 JPQL 提取 DTO][7]


[1]:https://dzone.com/articles/50-best-performance-practices-for-hibernate-5-amp

[2]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoSqlResultSetMappingAndNamedNativeQuery

[3]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoTupleAndSql

[4]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoTupleAndJpql

[5]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoConstructorExpression

[6]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoResultTransformer

[7]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoResultTransformerJpql


32. 通过 Blaze-Persistence 实体视图实现 DTO


"描述:" 获取不必要的数据很容易造成性能损失,使用 DTO 允许只提取必须的数据。在这个示例中,使用了 Blaze-Persistence 实体视图。


技术要点


  • 在 Maven `pom.xml` 中,添加 Blaze-Persistence 依赖

  • 配置 Blaze-Persistence 中 `CriteriaBuilderFactory` 和 `EntityViewManager`

  • 按照 Blaze-Persistence 风格接口开发 *entity view*

  • 编写 Spring repository 继承 `EntityViewRepository`

  • 调用 repository 中的方法,比如 `findAll()`、`findOne()` 等

  • 使用 Spring Data Projection 请参考例9


[示例代码][8]


[8]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoBlazeEntityView


33. 没有 @OrderColumn 时使用 @ElementCollection 的执行效果


没有 `@OrderColumn` 时,对 `@ElementCollection` 进行插入和删除会造成性能损失。有 `@OrderColumn` 时,情况会好一些。


"描述:"在这个示例中,展示了没有 `@OrderColumn` 时使用 `@ElementCollection` 可能带来的性能损失。接下来的示例(例34)加上 `@OrderColumn` 能减少性能损失。


技术要点


  • `@ElementCollection` 未设置主键

  •  `@ElementCollection` 映射到独立的数据库表

  • 需要频繁插入、删除操作时,避免使用 `@ElementCollection`。因为执行插入或删除前,数据库会删除所有记录

  • 数据表记录越多,性能损失越大


示例输出


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


[示例代码][9]


[9]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootElementCollectionNoOrderColumn


34. 使用 OrderColumn 时 @ElementCollection 的执行效果


没有 `@OrderColumn` 时,对 `@ElementCollection` 进行插入和删除会造成性能损失。有 `@OrderColumn` 时,情况会好一些。


"描述:"在这个示例中,展示了没有 `@OrderColumn` 时使用 `@ElementCollection` 可能带来的性能损失。加上 `@OrderColumn` 后,能减少对集合尾部操作(例如,在集合尾部执行 add/remove 操作)的性能损失。主要原因是操作对象左边的元素都不会移动,性能开销主要集中在集合尾部。


技术要点


  • `@ElementCollection` 未设置主键

  • `@ElementCollection` 映射到独立的数据库表

  • 需要频繁插入、删除操作时,使用 `@ElementCollection` 同时加上 `@OrderColumn`

  • 在数据表开始的地方,执行插入或移除记录越多性能损失越大


示例输出


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


[示例代码][10]


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


35. 如何避免 Open Session In View 反模式带来的延迟加载实体问题(1 Session/1 HTTP Request-Response)


只要存在延迟加载实体,无论将来是否用到 Open-Session In View 都会获取,从而造成严重的性能问题。


"描述:"Spring Boot 会默认开启 Open Session In View 反模式。如果确实需要使用,最好能够减少由此带来的性能损失。一种优化方法,可以把 `Connection` 标记为只读,这样能够避免数据库服务器记录事务日志。另一种办法,对那些不希望被延迟加载的实体属性显式标记。


技术要点


  • 获取实体并显式设置延迟加载属性

  • 可以根据需要在 Service 或 Controller,但"一定要在事务之外设置"

  • 为什么这种方法有效?为什么能够设置托管实体属性却不触发刷新操作?答案可以从 `OpenSessionInViewFilter` 文档中找到:


>>>

“注意:该 filter 默认不会刷新 Hibernate Session,刷新模式默认设为 `FlushMode.NEVER`。这里假定 Service 层事务会处理刷新:在事务读写期间,活动事务管理器会临时把刷新模式更改为 `FlushMode.AUTO`,事务结束时重置刷新模式为 `FlushMode.NEVER`。如果需要在不使用事务的情况下使用 filter,可以考虑修改 `flushMode` 更改默认刷新模式。”

>>>


示例输出



示例代码][11]


[11]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootSuppressLazyInitInOpenSessionInView


36. 如何使用 Spring Projection(DTO) 和 Inner Join


SQL JOIN 和 DTO 有助于解决 N+1 问题,例36至42包含了很多相关示例。


> 译注:“N+1 问题”即执行一次查询 N 条主数据后,由于关联引起的 N 次从数据查询,因此会带来了性能问题。一般来说,通过延迟加载可以部分缓解 N+1 带来的性能问题。


"描述:"这个示例使用 JPQL 和原生 SQL(MySQL)验证 Spring Projection(DTO) 和 Inner Join 方案。


技术要点


  • 定义若干实体:例如 `Tournament` 和 `Player`,保持双向 `@OneToMany`

  • 用测试数据填充数据库:例如,`resources/data-mysql.sql` 中的数据

  • 开发从数据库中获取列信息 getter 接口(Projection):例如,`TournamentPlayerNameDto`、`PlayerRankNameDto`、`TournamentIdNameDto`

  • 使用 JPQL 或原生 SQL 编写 Inner Join 查询,例如

  • 查询所有球员的比赛:`localhost:8080/tournamentsOfPlayersNamesInnerJoinJpql`

  • 查询所有排名小于或等于 `rank` 球员的比赛:`localhost:8080/tournamentsIdNameByRankInnerJoinSql`


[示例代码][12]


[12]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoViaInnerJoins


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


[37. 如何使用 Spring Projections(DTO) 和 Left Join][13]

[38. 如何使用 Spring Projections(DTO) 和 Right Join][14]

[39. 如何使用 Spring Projections(DTO) 和 Full Join][15]

[40. 如何使用 Spring Projections(DTO) 和 Left Excluding Join][16]

[41. 如何使用 Spring Projections(DTO) 和 Right Excluding Join][17]

[42. 如何使用 Spring Projections(DTO) 和 Outer Excluding Join][18]


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

[14]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoViaRightJoins

[15]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoViaFullJoins

[16]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoViaLeftExcludingJoins

[17]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoViaRightExcludingJoins

[18]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoViaOuterExcludingJoins


43. 如何使用 Spring Post 提交


本例中描述的现象通常出现在生产环境,这种环境的负载很高。Spring post-commit hook 会在执行完成前让数据库连接保持打开状态。提交结束后,在 hook 函数中执行耗时任务。高负载时会导致连接池处于饥饿状态。显然,这种情况下需要更长时间才能连接成功。


"描述:"在这个示例展示了 Spring Post Commit Hook.


技术要点


  • 避免在 Post Commit 中执行耗时任务,数据库连接会一直保持打开状态直到代码执行完成。


[示例代码][19]


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


44. 如何在无关实体上使用 Spring Projection(DTO) 和 Join(Hibernate 5.1+)

Hibernate 5.1 引入了 Explicit Join 可以在无关实体上使用,语法和执行效果与 SQL JOIN 类似。


"描述:" 在这个示例展示了 Spring Projection 和无关实体连接。Hibernate 5.1 引入了 Explicit Join 可以在无关实体上使用,语法和执行效果与 SQL JOIN 类似。


技术要点


  • 定义若干实体:例如,`Patient` 和 `Clinic` 无关实体

  • 用测试数据填充数据库:例如,`resources/data-mysql.sql` 中的数据

  • 开发从数据库中获取列信息 getter 接口(Projection):例如,`PatientNameAndMedicalHistoryDto`

  • 使用 JPQL 或原生 SQL 编写 Join 查询,例如

  • 查询目前不在治疗中的所有病人的姓名和病史:`localhost:8080/allPatientsNameAndMedicalHistoryNoTreatmentInnerJoinJpql`


[示例代码][20]


[20]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDtoUnrelatedEntities


45. 为什么要避免在实体上使用 Lombok @EqualsAndHashCode


Lombok 时目前非常流行的一个开发库。但请注意,在实体上使用 Lombok `@EqualsAndHashCode` 可能会造成严重问题。


"描述:"实体必须像[下面][21]这样实现 `equals()` and `hashCode()`,而且必须在 *transient*、*attached*、*detached* 和 *removed* 状态转换中保持 equal。Lombok `@EqualsAndHashCode` 无法满足上述要求。


[21]:https://vladmihalcea.com/the-best-way-to-implement-equals-hashcode-and-tostring-with-jpa-and-hibernate/


技术要点


"避免"


  • 使用 Lombok `@EqualsAndHashCode` 默认行为:实体 `LombokDefaultProduct`,测试 `LombokDefaultEqualsAndHashCodeTest`

  • 只基于主键使用 Lombok `@EqualsAndHashCode`:实体 `LombokIdProduct`,测试 `LombokEqualsAndHashCodeWithIdOnlyTest`

  • 使用默认的 `equals()` 和 `hashCode()` :实体 `DefaultProduct`,测试 `DefaultEqualsAndHashCodeTest`

  • 只基于主键实现 `equals()` 和 `hashCode()` :实体 `IdProduct`,测试 `IdEqualsAndHashCodeTest`


"推荐"


  • 依赖 `@NaturalId`:实体 `NaturalIdProduct`,测试 `NaturalIdEqualsAndHashCodeTest`

  • 依赖 *Primary Key*:实体 `GoodProduct`,测试 `GoodEqualsAndHashCodeTest`


`equals()` 和 `hashCode()` 的优秀实现:


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


[示例代码][22]


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


46. 如何通过 JOIN FETCH 避免 LazyInitializationException


如果你从未遇到 `famousLazyInitializationException`,说明你从未用过 Hibernate :) 加入从 `LAZY` 转为 `EAGER` 解决了这个异常,这个例子就是为你准备的。


"描述:" 当遇到 `LazyInitializationException` 时,通常会把获取类型从 `LAZY` 改为 `EAGER`。这是一种很糟糕的[代码异味][23]。避免这种异常的最好办法是 `JOIN FETCH` + DTO(如果有需要)。这个示例展示了无 DTO 的 JOIN FETCH,通过单个 `SELECT` 查询获取实体。但是,基于上述 DTO 示例也可以很方便地改为使用 DTO。


[23]:https://vladmihalcea.com/eager-fetching-is-a-code-smell/


技术要点


  • 定义两个相关实体,例如 `Category` 和 `Product` 保持*一对多*双向延迟加载关系

  • 编写 JPQL `JOIN FETCH` 获取包括 product 在内的一个 category

  • 编写 JPQL `JOIN FETCH` 获取所有 product 包括 category


示例输出


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


[示例代码][24]


[24]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootJoinFetch


47. 如何合并实体集合


如何正确地合并集合不是一件轻松的事情!


"描述:"这个 Spring Boot 示例基于[这篇文章][25]。这是 Vlad 例子的一个函数式实现。'强烈推荐阅读此文'


[25]:https://vladmihalcea.com/merge-entity-collections-jpa-hibernate/


技术要点


  • 从数据库中移除不在目标集合中出现的行

  • 在数据库中更新所有出现在目标集合中行

  • 为数据库添加所有出现在目标集合中,但没有在数据库快照中找到的行


[示例代码][26]


[26]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootMergeCollections


48. 如何根据需要延迟获取数据库连接(Hibernate 5.2.10)


得到数据库连接后不立即使用可能导致性能下降。需要数据库连接的人可不会一直等待。


"描述:" 这个 Spring Boot 示例展示了如何利用 Hibernate 5.2.10 特性根据需要延迟获取连接。通常,调用 `@Transactional` 注解的方法后会立即得到连接。假如这个方法在执行第一条 SQL 语句后包含了一些耗时的任务,那么就会毫无意义地一直保持连接。但是,Hibernate 5.2.10 允许根据需要延迟获取连接。示例采用 HikariCP 作为 Spring Boot 默认连接池。


技术要点


  • 在 `application.properties` 中设置 `spring.datasource.hikari.auto-commit=false`

  • 在 `application.properties` 中设置 `spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true`


示例输出


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


[示例代码][27]


[27]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootDelayConnection


49. 如何通过 Hibernate hi/lo 算法获取主键


通常,我们不太关心如何生成或获取主键。如果真正关注的话,会考虑使用 `SEQUENCE` 这种便捷、灵活且高效的方法。然而,每个 sequence 都会执行一个数据库行程导致性能问题。hi/lo 是一种标识符序列生成优化算法。


"描述:" 这个 Spring Boot 示例使用 hi/lo 算法,在10个数据库行程中,批量执行1000次插入(每次10条记录)并获取1000个主键。


技术要点


  • 使用 `SEQUENCE` 生成器类型,例如 PostgreSQL

  • 在 `Player.java` 实体中配置 `hi/lo` 算法


示例输出


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


[示例代码][28]


[28]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootHiLo


50. 如何编写高效双向 @ManyToMany 关联


你是否需要一个“多对多”关系的数据表,而且不希望使用两个 `@OneToMany` 关联进行映射?如果你更喜欢 `@ManyToMany` 双向关联,可以变得更高效一点。


"描述:" 这个示例是一个关于如何编写高效双向 `@ManyToMany` 关联的验证


技术要点


  • 使用 `Tournament` 和 `Player` 两个实体。一个锦标赛可以有多个玩家,一个玩家可以参加多个锦标赛


示例输出


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


[示例代码][29]


[29]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootManyToManyBidirectional


51. 在 @ManyToMany 关系中优先使用 Set 而非 List


在 `@ManyToMany` 中使用 `List` 而不是 `Set` 可能会生成比预期更多的 SQL,例如删除操作。从而导致降低下降。


"描述:"这个 Spring Boot 示例,在双向 `@ManyToMany` 关系中分别使用了 `List` 与 `Set` 删除记录。结论是 `Set` 效果更好,该结论对于单项关系也成立。


技术要点


  • 使用 `Set` 比 `List` 效率更高


示例输出


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


[示例代码][30]


[30]:https://github.com/AnghelLeonard/Hibernate-SpringBoot/tree/master/HibernateSpringBootManyToManyBidirectionalListVsSet


如果喜欢本文,你可能也会对[这本书][31]感兴趣。


[31]:https://leanpub.com/java-persistence-performance-illustrated-guide


上一篇:Hibernate的四种查询方式(主键查询,HQL查询,Criteria查询,本地sql查询)和修改和添加


下一篇:32岁的程序员被裁,java分布式事务解决方案java