1. 概述
Spring JPA简化了数据库交互,使通信过程透明化。但默认实现有时需要根据应用需求调整。本文将探讨如何实现禁止默认更新的解决方案,分析多种方法的优缺点。
2. 默认行为
JpaRepository<T, ID>
中的save(T)
方法默认执行upsert操作。若数据库中已存在该实体,则会触发更新:
@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);
}
}
根据ID判断:首次插入时调用persist()
,否则调用merge()
更新实体。
3. 服务层检查
最直接的解决方案是在服务层显式检查实体ID并选择行为。虽然侵入性较强,但业务逻辑往往需要这种显式控制:
@Service
public class SimpleBookService {
private SimpleBookRepository repository;
@Autowired
public SimpleBookService(SimpleBookRepository repository) {
this.repository = repository;
}
public SimpleBook save(SimpleBook book) {
if (book.getId() == null) {
return repository.save(book);
}
return book;
}
public Optional<SimpleBook> findById(Long id) {
return repository.findById(id);
}
}
✅ 优点:
- 逻辑清晰明确
- 可灵活定制每种情况的处理方式
- 不受JPA或数据库实现限制
4. 仓储层检查
将检查逻辑移至仓储层,但需新增方法而非覆盖save()
:
public interface RepositoryCheckBookRepository extends JpaRepository<RepositoryCheckBook, Long> {
default <S extends RepositoryCheckBook> S persist(S entity) {
if (entity.getId() == null) {
return save(entity);
}
return entity;
}
}
⚠️ 注意:此方案仅适用于数据库自动生成ID的场景(因有ID即视为已持久化)。
✅ 优点:
- 更精准控制行为
- 可轻松修改为通知客户端而非静默忽略
5. 使用EntityManager
通过自定义实现直接使用EntityManager
。需先创建接口:
public interface PersistableEntityManagerBookRepository<S> {
S persistBook(S entity);
}
再实现类(注意命名规范):
public class PersistableEntityManagerBookRepositoryImpl<S> implements PersistableEntityManagerBookRepository<S> {
@PersistenceContext
private EntityManager entityManager;
@Override
@Transactional
public S persist(S entity) {
entityManager.persist(entity);
return entity;
}
}
最后组合接口:
public interface EntityManagerBookRepository extends JpaRepository<EntityManagerBook, Long>,
PersistableEntityManagerBookRepository<EntityManagerBook> {
}
❌ 若实体已有ID,将抛出PersistentObjectException
导致的InvalidDataAccessApiUsageException
。
6. 使用原生查询
通过@Query
注解修改默认行为。因JPQL不支持插入,需使用原生SQL:
public interface CustomQueryBookRepository extends JpaRepository<CustomQueryBook, Long> {
@Modifying
@Transactional
@Query(value = "INSERT INTO custom_query_book (id, title) VALUES (:#{#book.id}, :#{#book.title})",
nativeQuery = true)
void persist(@Param("book") CustomQueryBook book);
}
❌ 主要问题:
- 必须手动提供ID(与数据库自动生成冲突)
- 修改查询仅支持
void
或int
返回值 - 易引发
DataIntegrityViolationException
异常
⚠️ 此方案行为反直觉,应尽量避免使用。
7. 实现Persistable<ID>
接口
通过实现Persistable<ID>
接口自定义实体状态判断逻辑:
public interface Persistable<ID> {
@Nullable
ID getId();
boolean isNew();
}
强制实体始终返回isNew()=true
:
@Entity
public class PersistableBook implements Persistable<Long> {
// fields, getters, and setters
@Override
public boolean isNew() {
return true;
}
}
❌ 踩坑:此方案违反持久化契约(将所有实体视为新实体),可能导致意外异常。
8. 不可更新字段
最佳方案:通过@Column
注解定义不可更新字段。这是最干净的解决方案,可精确控制可更新字段:
@Entity
public class UnapdatableBook {
@Id
@GeneratedValue
private Long id;
@Column(updatable = false)
private String title;
private String author;
// constructors, getters, and setters
}
测试验证:
@Test
void givenDatasourceWhenUpdateBookTheBookUpdatedIgnored() {
UnapdatableBook book = new UnapdatableBook(TITLE, AUTHOR);
UnapdatableBook persistedBook = repository.save(book);
Long id = persistedBook.getId();
persistedBook.setTitle(NEW_TITLE);
persistedBook.setAuthor(NEW_AUTHOR);
repository.save(persistedBook);
Optional<UnapdatableBook> actualBook = repository.findById(id);
assertTrue(actualBook.isPresent());
assertThat(actualBook.get().getId()).isEqualTo(id);
assertThat(actualBook.get().getTitle()).isEqualTo(TITLE);
assertThat(actualBook.get().getAuthor()).isEqualTo(NEW_AUTHOR);
}
✅ 结果:
title
字段未被更新author
字段成功更新
9. 结论
Spring JPA不仅提供便捷的数据库交互工具,还具有高度灵活性和可配置性。选择合适方案需深入理解可用功能:
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
服务层检查 | 业务逻辑复杂 | 灵活可控 | 侵入性强 |
仓储层检查 | ID自动生成 | 精准控制 | 依赖ID生成策略 |
EntityManager | 需底层操作 | 功能强大 | 实现复杂 |
原生查询 | 特殊SQL需求 | 直接控制 | 易出错 |
Persistable接口 | 全局控制 | 简单实现 | 违反契约 |
不可更新字段 | 精细控制 | 最干净方案 | 仅限字段级 |
所有示例代码可在GitHub获取。