概述
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架,底层使用了 Hibernate 的 JPA 技术实现,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展。
Spring Data JPA给开发人员带来了简便的同时,也埋了一些“坑”,如果没弄明白其内部机制,有时可能就会莫名的踩坑,今天就说下JPA的save方法在使用时要特别注意的地方。
一、缓存问题
系统有一个修改人员的接口,修改人的定位卡,会调用开放平台新增定位卡,并把卡和人进行绑定。业务逻辑大致如下:
1、UserService修改人员信息入库
2、调用LableService绑定定位卡
3、调用定位卡平台新增定位卡
4、如出现异常则删除定位卡平台定位卡
代码如下:
UserServiceImpl
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private LableService lableService;
@Override
@Transactional(rollbackFor = Exception.class)
public void edit() {
try {
//模拟修改
User user = userDao.findById(1L).orElse(null);
user.setName("姓名超出数据库长度");
user.setSn("202306230002");
user = userDao.save(user);
log.info("====更新用户成功");
lableService.bindSn(user);
log.info("====定位卡绑定用户成功");
} catch (Exception e) {
log.error("====更新用户异常:", e);
log.info("====调用开放平台删除定位卡");
throw e;
}
}
}
LableServiceImpl
@Slf4j
@Service
public class LableServiceImpl implements LableService {
@Autowired
private LableDao lableDao;
@Override
@Transactional(rollbackFor = Exception.class)
public void bindSn(User user) {
String sn = user.getSn();
Lable lable = new Lable();
lable.setBindId(user.getId());
lable.setSn(sn);
lableDao.save(lable);
log.info("====调用开放平台新增定位卡");
}
}
这里模拟更新用户信息异常的情况。
执行打印日志如下:
结果并不是想象中的异常被catch并执行
log.error("====更新用户异常:", e);
log.info("====调用开放平台删除定位卡");
虽然事务回滚了,但是开放平台的卡片未被删除。
原因:
Spring Data JPA 有一级缓存,当在一个事务内通过save()方法去update一个从数据库中查询出来的实体时,Spring Data JPA并不会马上执行Update SQL语句,把修改同步到数据库,而是等到事务提交时才会将缓存中的实体信息同步到数据库中。
解决方案:
1、在执行更新操作后,调用flush()方法
2、save()方法修改为saveAndFlush()
3、如果使用@Query注解更新,则使用@Modifying(clearAutomatically = true)。将clearAutomatically
属性设置为true
表示,执行完自定义的update
语句后会将一级缓存给清空。
二、默认先select,再update
save方法在更新DB时一个最大的特点是:默认每次都会先去查一遍DB,对查出来的DB数据与要保存的数据进行比较,看是否有变化,若有变化,才会将数据持久化至DB(执行数据的update),否则,就不会进行数据的持久化。如果需要处理大量的数据量时,意味着除了update外,还要多出大量的DB查询交互,影响性能。
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
解决方案:
1、不使用save方式,改为使用@Query方式,即直接写update hql/sql语句来更新数据。
2、可以使用EntityManager,通过拼接 SQL 语句的方式,批量插入或者更新多条记录。
三、默认更新所有的字段
原本只更改了一个字段值,但最后更新DB时,JPA依旧会对该条记录的所有字段进行更新。对于DB来说,只更新一个字段,肯定是比更新十个字段的性能消耗小很多,另外可能会把空值更新到DB。由于save方法的入参是一个实体对象,如果传入的实体对象的某些属性值为空(null),则最后JPA在更新时会把对应的字段也尝试更新为空(null),而实际上你可能只想更新所更改过的字段,对于空值字段你希望自动忽略不更新。
解决方案:
1、使用@Query方式,直接写update hql/sql语句来更新数据。
2、使用@DynamicUpdate注解,只需要在实体类上加上该注解,即可实现只更新有变化的字段。