Spirng Data JPA save方法遇到的坑

SpringDataJPA的save方法可能导致缓存问题,不在事物中立即更新数据库,而是在事务提交时同步。这可能导致异常处理时无法删除已创建的资源。此外,save方法默认会先查询再更新,影响性能,并会更新所有字段,包括空值。解决办法包括使用flush(),saveAndFlush(),@Modifying清除缓存,以及使用@DynamicUpdate和自定义SQL更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

      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注解,只需要在实体类上加上该注解,即可实现只更新有变化的字段。

 

### Spring Data JPA 中 `save` 方法的使用 #### 保存新实体 当向数据库中插入一条新的记录时,可以通过调用 `save` 方法来实现。如果传入的对象是一个全新的实例,则该对象会被标记为持久化状态并插入到数据库中。 ```java Book newBook = new Book(); newBook.setBookName("Effective Java"); newBook.setPrice(49.99f); newBook.setPubdate(new Date()); bookRepository.save(newBook); // 插入新书本条目[^1] ``` #### 更新现有实体 对于已经存在的实体,在对其进行任何属性更新之后只需简单地调用 `save` 即可。由于 Hibernate 的脏检查机制,即使不重新调用 `save` 或者 `merge` ,只要是在同一个事务内做出的变化都会被自动追踪并在提交时写回数据库[^4]。 ```java Optional<Book> optionalBook = bookRepository.findById(bookId); if (optionalBook.isPresent()) { Book existingBook = optionalBook.get(); existingBook.setPrice(existingBook.getPrice() * 0.85); // 应用折扣 bookRepository.save(existingBook); // 更新价格信息 } ``` 需要注意的是,一旦某个实体进入了管理(persistent)状态,所有的变更都将受到 EntityManager 的监控,并会在适当的时候同步至底层的数据存储层。因此大多数情况下并不需要显式地去调用 flush 来强制刷新缓存中的数据变化。 #### 处理级联操作 如果有多个相互关联的对象构成了一棵树形结构的话,那么还可以利用 cascade 属性来自定义何时以及如何传播某些特定的操作给子节点们。比如下面的例子展示了设置一对多关系时指定删除父项的同时也移除其下所有子项的方式: ```java @Entity public class Author { @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true) private Set<Book> books; ... } // 删除作者及其作品 authorRepository.deleteById(authorId); // 这也会清除掉对应的书籍列表[^3] ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值