美文网首页
Hibernate 最佳实践

Hibernate 最佳实践

作者: 单纯小码农 | 来源:发表于2018-12-11 10:46 被阅读0次

Hibernate 最佳实践

一、一级缓存

  1. merge 时记得使用返回值
  2. native DML 操作后要 clear
  3. 单元测试数据准备完毕要 flush 并 clear
  4. 不在事务中抓取数据库相关异常
  5. 只在 Service 和 Dao 中开启事务
  6. 使用 Readonly 事务
  7. 使用 Projection
  8. 必要时使用 update 而不是 merge
  9. 大表的自增 ID 用 Table Generator
  10. 不使用 OpenSessionInView 模式
  11. 批量保存时控制好事务粒度
  12. 不要随意更改@TableGenerator的allocationSize的值

二、关系映射

  1. 慎用关系映射
  2. 不使用多对多关系或者单个一对多关系
  3. 尽量使用 lazy 而不是 eager fetch 模式
  4. 一对多属性不要 join fetch
  5. 使用自然主键来生成 equals 和 hashCode
  6. equals方法中用 get 方法
  7. 设置 hibernate.default_batch_fetch_size
  8. jackson 序列化时使用 hibernate 扩展

三、查询

  1. JPQL 中 IN 查询参数个数的问题
  2. 使用 JPA Criteria 查询时不要直接使用数字常量

四、其他

  1. 检查 service 和 dao 方法生成的 SQL 语句
  2. 设置 hibernate.jdbc.batch_size
  3. 考虑增删改操作的顺序
  4. 用 logback 关闭 hibernate 查询的 warning 日志
  5. 生产环境启动时检查数据库schema是否正确
  6. 将注解加在 field 上
  7. 关闭二级缓存
  8. 慎用代理
  9. 慎用复合主键

五、推荐配置


一、一级缓存

1. merge时记得使用返回值

Hibernate 使用的一级缓存在 merge 时,会将传入的参数的状态合并到 Session 中已经存在的 Entity 中去,然后将此 Entity 返回。返回的 Entity 才是 managed instance,而传入的不是。什么是 managed instance,请参考 hibernate 官方参考手册或 Java Persistence API Specification

2. native DML 操作后要 clear

hibernate 一级缓存具有应用程序级的 Repeatable Read 功能,即之前缓存好的 Entity,下次再去数据库 select 出来时,如果缓存中已经有了,并不会刷新。在同一个事务中,如果将 hibernate 查询和更新与 native DML 执行穿插在一起,那么就需要及时的 flush 并 clear。一般建议不要将 native DML 与 hibernate 操作混合使用。这里 native DML 指的是 hibernate 的 native DML 和其他方式执行的增删改SQL。

3. 单元测试数据准备完毕要 flush 并 clear

原因同上

4. 不在事务中抓取数据库相关异常

使用 hibernate 进行增删改操作时,由于一级缓存的存在,hibernate 并不会立即将操作提交到数据库,而是在必要时才提交。如果接下来的操作会受到之前增删改操作的影响,则会将之前的操作提交到数据库,否则会延迟到事务提交时才执行。如果延迟到事务提交时执行,那么类似 “DataIntegrityViolationException” 这样的异常只会在事务提交时抛出,而不是调用 save 时抛出 。

5. 只在 Service 和 Dao 中开启事务

事务的边界最好是明确的而且是可控的。否则,会出现各种奇怪的问题。比如,在 hibernate 一级缓存的生命周期内,任何被修改过的 managed instance 最终都有可能被 flush 到数据库中。也就是说,即使你不显示的调用 save、merge、update、remove等方法,数据库的增删改操作也有可能发生。

6. 使用 Readonly 事务

hibernate 一级缓存在事务提交时会做一个 flush 操作,即对一级缓存中 managed 的所有 instance 做一个脏检查,然后将有变化的 instance 同步到数据库。如果只是查询数据而不想更新数据,那么可以将事务设置为 ReadOnly,这样脏检查就可以不做了。在数据量特别大时,脏检查有性能问题。记得之前有一个 IN 查询没有标记为 ReadOnly,而且我们为了规避 hibnerate 的 IN 查询的性能 bug,故意将 IN 查询拆分成了多个小的 IN 查询,结果导致了 O(n2)的脏检查,因为每一个查询都会导致脏检查的发生。

只读事务也可以让 MySQL 5.6 以后的 Innodb Engine 有更好的性能。

另外,我们还遇到过 hibernate 的一个 bug,在非 ReadOnly 事务中, hibernate.default_batch_fetch_size 不起作用,从而导致 n+1 的性能问题。在 hibernate 5.0.12 版本没有重现。

7. 使用 Projection

有些时候,我们需要先查询一个表再更新或保存到另外的表,这时,事务就不能是 ReadOnly 了。如果查询的数据量特别大,脏检查会有性能问题,那么可以使用 Projection。Projection 并不会涉及到脏检查的问题。

另外一个使用 Projection 的场景就是,在某些情况下(比如压缩表),如果查询结果中不包含 Blob 字段,可以提高性能。有的同学认为这种场景下直接在 Entity 的 Blob 字段上加 Lazy 就可以达到这个目标了。但是 @Lazy 只有在 maven 中配置了 hibernate 的编译期增强才能生效。另外,lazy 也会带来 Session 关闭的问题,因为你不知道什么时候这个 Blob 字段是可用的,什么时候不可用。

如果是普通表,则最好为大的 Blob 字段独立建一张表。

8. 必要时使用 update 而不是 merge

merge 比 update 会多一个查询。而且,如果多个 entity 一起 merge 的话,会导致无法 batch update。大批量数据update可以使用JdbcTemplate.batchUpdate来提高性能

9. 大表的自增 ID 用 Table Generator

使用 Table Generator 而不是 MySQL 自增 ID 的原因是,Table Generator 可以让 hibernate 启用 jdbc batch。如果 insert 语句是一批一批执行的,那么 mysql 驱动可以将一批 insert 语句改写成一个 insert 语句。当然,如果 Table Generator 的 ID 增长步长太小,性能上可能反而更差。

10. 不使用 OpenSessionInView 模式

OpenSessionInView 主要是针对jsp、freemarker 等传统前端技术的。对于使用了关系映射和传统前端技术的web 应用,OpenSessionInView 可以带来一定的便利性。但是,OpenSessionInView 也有自身的问题。一个问题是,OpenSessionInView 会导致连接数不够用,虽然可以配置成事务提交后立即释放连接,却经常会忘记这个配置。另外一个问题是,Session 的声明周期中,任何对 managed instance 的修改,都有可能导致不希望的数据更新操作。

11. 批量保存时控制好事务粒度

使用 hibernate 大批量保存或更新数据时,如果事务粒度过大,需要每一小批数据 flush 并 clear 一次,否则脏检查会越来越慢。所以,如果业务允许,就直接使用小批量事务吧,这样也就不需要记得 flush 和 clear 了。我们还碰到过一个事务里保存的数据量过大,导致一级缓存太大,出现out of memory的问题

二、关系映射

1. 慎用关系映射

  关系映射涉及到的陷阱有:
  1. lazy的关系映射会出现 Session 关闭错误
  2. eager的关系映射会将很多无关记录查询到内存,导致性能问题
  3. lazy 的关系映射会生成代理,代理会带来一些奇怪的问题
  4. 一对多映射的 join fetch 会导致重复记录,也会有分页问题
  5. lazy 的一对多映射会有 n+1 次查询的性能问题
  6. bag、list 等 collection 映射会先全部删除再重新添加
  7. mapping 配置非常复杂
  8. jackson 默认无法序列化 lazy 的属性

2. 不使用多对多关系或者单个一对多关系

单个一对多关系与多对多关系在 hibernate 中实现的方式是一样的。不使用多对多关系的一个主要原因是性能问题。最明显的性能问题是:当要从一对多里面的移除一个成员时,hibernate 会删除所有成员关系,然后再重新添加剩下的所有成员关系

3. 尽量使用 lazy 而不是 eager fetch 模式

如果关联关系用的比较多,而且都采用 eager 模式,那么每次查询都会带出很多用不到的对象,实在浪费性能。如果默认使用 lazy 模式,我们还可以在 DAO 查询时指定哪些关系需要取出,哪些不需要。当然,如果本身查询很少,关系层次也少,带出的数据也少,这种情况使用下 eager 模式也是可以的。

4. 一对多属性不要 join fetch

一对多属性进行 join 的最大问题就是会获取到重复的数据。如果不想获取重复数据,可能需要使用 distinct。而使用 distinct 既不好 count,也不好翻页。如果真的碰到要求分页,那么就用子查询吧。

5. 使用自然主键来生成 equals 和 hashCode

对于很多由 hibernate 或数据库来生成主键的 entity 来说,如果 equals 和 hashCode 是由主键而不是自然主键来生成的,那么就会导致 equals 和 hashCode 的不稳定性。而作为 HashSet 的 element 或 HashMap 的 key,这是会出奇怪问题的。

6. equals 方法中用 get 方法

这主要是因为代理的存在。如果 equals 方法中传入的参数是一个代理,直接获取字段是会返回空值的,使用 get 方法才会触发一次数据库查询从而获取真实字段。如果我们不使用代理(不用关联关系,不用 getReference,不用 load,不用 lazy),那么就可以忽略这一条。

7. 设置 hibernate.default_batch_fetch_size

关联关系使用 lazy 时,如果要读取关联关系,默认情况下,hibernate 会对每个 instance 都执行一次查询,从而带来 n+1 次查询的性能问题。如果将 hibernate.default_batch_fetch_size 设置成一个较大的数字,比如 50, 那么 hibernate 会做优化,可以减少这种情况下的查询次数。

8. jackson 序列化时使用 hibernate 扩展

对于 lazy 属性,jackson 默认是不能很好的做序列化的。以前很多同学都会用 @JsonIgnore 来忽略关联属性,某些场景下这并不适合。jackson 的 hibernate 扩展可以很好的支持到关联属性。

三、查询

1. JPQL 中 IN 查询参数个数的问题

  问题有3个:

    a. in 查询参数过多导致 CPU 100%
    b. in 查询参数过多且多变导致 OOM;hibernate 会缓存每个 JPQL 的解析结果,如果 in 查询参数过多而且多变,那么类似的 SQL 语句会被缓存好多次,而且每个都会占不少内存;
    c. in 查询参数多变导致 JPQL 缓存效率低下

  解决方案:

    限制 in 查询参数个数,而且将个数限制到某几个特例,比如 in 查询参数个数只能是 1,3,10,50,如果不足重复补最后一个参数。
    in查询用partion来分拆成多组in查询的时候,要对分组前的in查询参数去重,否则可能会有重复的参数分在不同的组里,最后查询结果有重复 

2. 使用 JPA Criteria 查询时不要直接使用数字常量

使用 JPA Criteria 进行查询时,hibernate 会将数字常量直接输出到 SQL 语句中,这会导致 JPQL 缓存效率低下,从而导致 Spring DATA JPA (其中有 synchronize) 的并发效率低下。
解决办法,使用自己写的 LiteralExpression 来包装数字常量。

四、其他

1. 检查 service 和 dao 方法生成的 SQL 语句

使用 hibernate 的同学,在写 service 和 dao 时,一定要查看其所生成的 SQL 语句,特别是要关注 hibernate 有没有生成一些多余的 SQL。

可以将 org.hibernate.SQL 设置成 DEBUG 来输出 hibernate SQL 日志。不建议使用 hibernate.show_sql。因为 hibernate.show_sql 将日志输出到了标准输出,而且不带时间戳,也不能运行期修改配置。

2. 设置 hibernate.jdbc.batch_size

写过 JDBC 的都知道,PrepareStatement 支持 batch update,这样 java 进程与数据库之间的交互就可以变少。jdbc batch 设置一个差不多的大小,应该就可以了,比如 50。

3. 考虑增删改操作的顺序

为了方便 hibernate 使用 jdbc batch,需要将类似的操作按顺序执行。比如,先对某一类对象做更新,然后对另一类对象做删除。穿插起来做,hibernate 就没法利用 jdbc batch 了。

4. 用 logback 关闭 hibernate 查询的 warning 日志

如果 org.hibernate.engine.jdbc.spi.SqlExceptionHelper 的日志级别在 ERROR 以下,那么,每次查询完毕,hibernate 都会再发起一个 showWarning 的查询,这是非常没有必要的。

5. 生产环境启动时检查数据库schema是否正确

我们现在有很多逻辑都依赖于数据库中存在一个唯一约束。如果生产环境中忘记加唯一约束了,那么会出现大问题。在启动的时候检查下唯一约束是很有必要的。

6. 将注解加在 field 上

将注解加在字段上时,hibernate 就认字段而不认 Bean Property 了。不认 Bean Property 有个好处,就是我们可以很方便的在 Entity 类中加入一些简单的业务逻辑方法,这个可以方便代码重用,也可以更清晰的显示表中有哪些字段。

7. 关闭二级缓存

hibernate 二级缓存的四个级别, READ_ONLY, NONSTRICT_READ_WRITE, READ_WRITE, TRANSACTIONAL。其中 READ_ONLY 受限于只读数据,NONSTRICT_READ_WRITE 可能会导致缓存与数据库数据不同步,READ_WRITE 要求不同同时修改同一个 Entity,而且在多进程环境下,还得需要一个独立的外部缓存,TRANSACTIONAL 要求缓存和数据库之间使用分布式事务。这些限制都不足以弥补它带来的好处,所以还是关闭更简单。如果真要做缓存,我们可以自己做应用层的缓存,而目前我们的应用场景来看,还不需要二级缓存。

8. 慎用代理

我们可以通过 EntityManager#getReference 和 Session#load 方式来获取一个 Entity 的代理。在关联关系中,用代理来表示关系可以省去一个查询,另外,ManyToOne 的 Lazy 也是通过代理来实现的。代理的主要问题是不够直观,在调用某个 get 或 set 方法时,如果会触发一次数据库查询,那将会是一件很令人惊讶的事情。另外,由于代理的存在,在 Entity 类的 equals 方法中,就必须使用 get 方法。

9. 慎用复合主键

复合主键有几个问题:
​ 1. 修改主键会锁表
​ 2. 如果主键中有 varchar,会出现奇怪的问题。比如,mysql 默认是不区分大小写的,当你用大写的复合组件去查询时,mysql 可能会给你返回一个小写的主键,这个时候 hibernate 内部会认为这种情况不应该发生,于是直接报错。
​ 3. hibernate 还不能很好的支持复合主键中使用 java8 的时间类型

五、推荐配置

spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.use-new-id-generator-mappings=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.properties.hibernate.cache.use_second_level_cache=false
spring.jpa.properties.hibernate.id.new_generator_mappings=true
spring.jpa.properties.hibernate.jdbc.batch_size=100
spring.jpa.properties.hibernate.default_batch_fetch_size=50
spring.jpa.properties.hibernate.batch_fetch_style=dynamic

相关文章

网友评论

      本文标题:Hibernate 最佳实践

      本文链接:https://www.haomeiwen.com/subject/dgrshqtx.html