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(与数据库自动生成冲突)
  • 修改查询仅支持voidint返回值
  • 易引发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获取。


原始标题:Implementing Persistable-Only Entities in Spring Data JPA | Baeldung