jpa insert 对象_JPA最佳实践

前言

JPA要用好,要遵循最佳实践,否则不如不用。

比起半自动化的MyBatis,全自动化的JPA开发效率更高,但由于底层封装较多,较为复杂,JPA在解放开发人员生产力的同时,也存在一些坑,有些坑在线上环境可能会给您造成血的教训。。。

好在JPA也提供了一些方式,可以绕开这些坑,因此总结JPA的最佳实践十分有必要。


  整体使用原则  

e9a19bea7a497f928edba0493a7edf07.png

简单的增删改查操作:优先考虑用JPA自带的Repository API

说明:

如果是根据主键id查询,则直接使用JPA内置的findById方法,如:

User user = userDAO.findById(id).get();

如果是要根据主键id之外属性的固定查询,则一般在对应的Repository类里定义findByXxx方法即可,如:

List findByAddress(String address);

灵活复杂的where、嵌套子查询、批量更新操作:推荐使用querydsl实现

说明:

在querydsl出现之前,相比MyBatis,JPA在如下2方面存在明显的不足:

  • 灵活的where

  • 嵌套子查询

而querydsl的出现给JPA加上了一对翅膀,很好的弥补了JPA在如上2方面的劣势,让JPA的能力又上了一个新台阶。

出于方便集中审核sql的需要:建议所有自定义的db操作集中放在同一个包下

说明:

很多人在使用JPA时,除了在repository类里会自定义一些db操作外,也会在上层的service、manager里自定义一些db操作(如JPAQuery),这造成了db操作分散在多处,难以集中管理,非常不方便定期审核这些db操作。

如果该db操作并未使用Repository API(如JPAQuery),则建议将之单独抽离到一个类里,打上@Component注解,跟Repository类放在同一个包下,以方便开发人员review以及DBA做sql审核。


 保存操作最佳实践

这里的保存操作,是指实体的新增更新这2种操作。

ee25f0367a3e7b83c1a672ebbb3e9f9c.png

persist方法的最佳实践

适用场景:

persist只会新增,不会更新,且不会select,只会insert,即与DB只有一次交互,适用于并发要求较高的场景。

使用限制:

  • 实际DB里可以将主键id设置为自增的,但对应实体的主键id属性不可配置@GeneratedValue(strategy = GenerationType.IDENTITY)

  • 必须要在事务内执行该方法,否则会报错

  • 不会返回所保存实体的主键id,因此如果希望拿到保存后的id,则该方法不适用

纯新增操作的最佳实践

1)如果并发或吞吐量较小,则优先考虑使用save方法。因为persist只能新增,不能更新,而save两者皆可,也更方便。

2)如果并发较大,且满足前面提到的persist的使用限制条件,则建议优先考虑使用persist方法进行新增操作。

3)如果并发较大,但不满足上面persist的使用限制条件,则建议在@Query注解里定义hql或native sql来实现

4)如果吞吐量较大,则建议自己扩展实现批量插入(比如自己拼接insert sql,一次发送给DB),目前JPA暂时未提供批量插入的API。虽然其save方法可接受一批数据,但实际执行时时逐条插入的,是个伪批量。

只保存想要保存的字段

建议对实体类都配置@DynamicUpdate、@DynamicInsert注解,以保证JPA执行时,只会保存代码里显式set的value,因为JPA的save默认会更新/插入所有字段,假如某字段没有被显式set,则该字段最终会被JPA尝试以null保存到DB,这会带来一些不可预估的负面影响,比如误覆盖原值、触发DB字段的非null约束报错等

save方法的使用限制

JPA内置的save方法在执行时,都会先逐笔查询一次db,再发给DB进行数据持久化。即使是一批数据进行save时,依旧是逐条发给DB,因此在并发量或吞吐量较大时,性能较低,此时建议直接使用如下3种方式之一来新增或更新数据:

  • 若是更新,则优先考虑querydsl,因为querydsl也提供了update的支持

  • 若为新增,则考虑在@Query注解里定义hql/native sql或自己扩展实现批量新增

新增操作需确定主键id为空

尤其是采用复制方式(如BeanUtils.copyProperties())来生成新的对象实例,插入到DB时,要特别小心,千万要记得清空掉原主键id,即:将新生成的对象实例的主键id显式置为null。否则在执行JPA的save方法时,会被当做对原对象实例的更新(update),而不是新增(insert),导致最终直接更新原纪录了,而非插入一条新纪录。

save后不要对实体有任何的set操作

因为事务方法内,save后的set会连带持久化到DB,也就是说,在对某个实例对象调用save方法后,千万不要对该实例对象的属性做任何的改变,否则这些你觉得仅仅是在内存的变化,其实最后是会被更新到DB的。

如果确实存在save后再改变该实例对象的属性的需要,建议重新new一个实体来操作,不要基于原来的实例对象操作。

异常重试save时先清空主键id

当save方法执行发生异常时(请注意这个前提条件),若要重试时,一定要显式清空实体的主键id后再重试。

因为如果重试成功,则JPA返回给应用的该依旧是重试之前预获取的主键id,而不是重试成功时的主键id。此时该实例数据的主键id在DB中其实已发生了变化,但内存中的实例的主键id却没有变化,依旧是重试之前的值。也就是说,同样一条数据,在内存中的主键id与其在DB中的主键id不一致!

因此在执行save方法发生异常,进行重试时,一定要清空该实体上的主键id,即显式的设为null,这样再执行save的重试时,就能获取到与DB实际一致的主键id,保证了数据的一致性。

自动返回主键id时需配置IDENTITY策略

若需要在save某个实例后自动返回该实例的主键id,需要在该实体的主键id属性上配置注解@GeneratedValue(strategy = GenerationType.IDENTITY)

唯一键冲突异常捕获最佳实践

DB唯一键冲突异常若需要捕获,建议只捕获粗粒度的Exception异常,然后检查异常实例的getCause()里是否包含SQLIntegrityConstraintViolationException,这样扩展性会好一些,因为不同DB抛出的具体异常是不同的(比如TiDB与MySQL抛出的唯一键冲突异常完全不同)

何时适合采用querydsl进行更新操作

通过querydsl方式来更新数据时,不会像save那样先去select再update,而是只会update,即只与DB交互一次,因此相比save,querydsl的更新无疑性能更好。

若存在如下场景之一,则建议使用querydsl方式来更新:

  • update操作较为频繁,尤其是在高并发高吞吐场景时,建议优先考虑使用querydsl的update

  • update的条件不固定,即update的where条件较为灵活时


 删除操作最佳实践

6f5e12d6f7d3d02b4e57d60abd6b22db.png

并发量或吞吐量较大时不要使用JPA内置的delete方法

因为JPA内置的delete方法每次执行时,都会先查询一次db,且逐条发给DB进行删除,在并发或吞吐量较大时,性能低下。建议使用如下2种方式之一:

  • 使用querydsl提供的delete API进行删除

  • 使用@Query注解定义的hql或native sql来删除

批量删除最佳实践

1)批量删除请不要使用delete(Iterable enties)方法,虽然该方法能一次接受一批记录进行删除,但最终却是逐条发给DB删除,也就是说有多少条数据,就会与DB交互多少次。

2)批量删除切勿使用deleteAll方法,因为该方法也会默认先去查询一次DB,然后逐条发给DB删除,在数据量较大时,可能会占用较大内存,直接OOM。

3)数据量较小时,可以使用deleteInBatch进行批量删除,因为该方法只会与DB交互一次,前面2个都是伪批量删除,而这个deleteInBatch才是真批量删除。

4)使用deleteInBatch进行批量删除时,要能确定所删除的数据条数小于3000,否则很有可能会引发StackOverFlow异常

5)若所删除的记录数大于3000或者不确定所删除的记录数,建议使用如下2种方式之一:

  • 使用querydsl提供的delete API进行删除

  • 使用@Query注解定义的hql或native sql来删除

事务方法内先删除后保存场景的最佳实践

在同个事务方法中,若存在先删除,后保存的场景,要注意避免代码中的操作顺序(先delete再save)与实际sql执行顺序(先save再delete)不一致的情况发生,建议采用以下三种方式之一来规避这个问题:

  • 若坚持使用JPA自带的delete方法,则一定要在数据删除之后、数据插入操作之前,先执行一次flush以刷新delete操作,让数据删除操作的事务先提交

  • 数据删除操作不采用JPA的delete方法,改为采用@Query注解定义的hql/native sql 或者 querydsl的delete API 来删除数据

  • 如果实际业务场景允许同一个方法中delete成功而insert失败的话,可考虑以非事务方式执行这2个操作。

结语

以上为个人在使用JPA的过程中总结出来的一些最佳实践,若有遗漏或不足,还请各位补充和指正。449ea4d3aa03f4b5ba352645a61a53f4.png

若各位有JPA踩坑经历或最佳实践总结,也欢迎一起交流探讨。ef0c4e88fe2ae7d5e8c65e788896b5f8.png

个人邮箱:tom_kang@qq.com

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值