在 Spring Data JPA 中實作僅持久化實體
1. 概述
Spring JPA 簡化了與資料庫的交互並使通訊透明。然而,預設的 Spring 實作有時需要根據應用程式需求進行調整。
在本教程中,我們將學習如何實現預設情況下不允許更新的解決方案。我們將考慮幾種方法並討論每種方法的優缺點。
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,如果這是第一次插入,它將保留該實體。否則,它將呼叫merge(S)
方法來更新它。
3. 服務檢查
此問題最明顯的解決方案是明確檢查實體是否包含 ID 並選擇適當的行為。這是更具侵入性的解決方案,但同時,這種行為通常由領域邏輯決定。
因此,儘管這種方法需要我們編寫一條 if 語句和幾行程式碼,但它是乾淨且明確的。此外,我們可以更自由地決定在每種情況下做什麼,並且不受 JPA 或資料庫實現的限制:
@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);
}
}
4. 儲存庫檢查
此方法與前一種方法類似,但將檢查直接移至儲存庫中。然而,如果我們不想從頭開始提供save(T)
方法的實現,我們需要實作另一個:
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
此方法還需要自訂實現,但我們將直接使用EntityManger
。它也可能為我們提供更多功能。但是,我們必須先建立一個自訂實現,因為我們無法將 bean 注入到介面中。讓我們從一個介面開始:
public interface PersistableEntityManagerBookRepository<S> {
S persistBook(S entity);
}
之後,我們可以為其提供一個實作。我們將使用@PersistenceContext
,其行為類似於@Autowired
,但更具體:
public class PersistableEntityManagerBookRepositoryImpl<S> implements PersistableEntityManagerBookRepository<S> {
@PersistenceContext
private EntityManager entityManager;
@Override
@Transactional
public S persist(S entity) {
entityManager.persist(entity);
return entity;
}
}
遵循正確的命名約定很重要。實作應該與介面具有相同的名稱,但以Impl
結尾。為了將所有的東西結合在一起,我們需要建立另一個介面來擴展我們的自訂介面和JpaRepository<T, ID>
:
public interface EntityManagerBookRepository extends JpaRepository<EntityManagerBook, Long>,
PersistableEntityManagerBookRepository<EntityManagerBook> {
}
如果實體有 ID, persist(T)
方法會拋出由 PersistentObjectException 引起的InvalidDataAccessApiUsageException
PersistentObjectException.
6. 使用本機查詢
更改JpaRepository<T>
預設行為的另一種方法是使用@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,
這可能不方便。
整體而言,此方法會因 ID 衝突而導致DataIntegrityViolationException
。這可能會產生開銷。此外,該方法的行為並不簡單,因此應盡可能避免使用這種方法。
7. Persistable<ID>
接口
我們可以透過實作Persistable<ID>
介面來實現類似的結果:
public interface Persistable<ID> {
@Nullable
ID getId();
boolean isNew();
}
簡而言之,此介面允許新增自訂邏輯,同時識別實體是新實體還是已存在實體。這與我們在預設save(S)
實作中看到的isNew()
方法相同。
我們可以實作這個介面並始終告訴 JPA 該實體是新的:
@Entity
public class PersistableBook implements Persistable<Long> {
// fields, getters, and setters
@Override
public boolean isNew() {
return true;
}
}
這將強制save(S)
始終選擇persist(S)
,
在違反 ID 約束的情況下拋出異常。這個解決方案通常會起作用,但它可能會產生問題,因為我們違反了持久性契約,考慮到所有實體都是新的。
8. 不可更新的字段
最好的方法是將欄位定義為不可更新。這是處理問題的最簡潔的方法,並且允許我們僅識別那些我們想要更新的欄位。我們可以使用@Column
註解來定義這樣的欄位:
@Entity
public class UnapdatableBook {
@Id
@GeneratedValue
private Long id;
@Column(updatable = false)
private String title;
private String author;
// constructors, getters, and setters
}
JPA 在更新時會默默地忽略這些欄位。同時,它仍然允許我們更新其他欄位:
@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);
}
我們沒有更改書名,但成功更新了書的作者。
9. 結論
Spring JPA不僅為我們提供了方便的與資料庫互動的工具,而且還具有高度的靈活性和可配置性。我們可以使用許多不同的方法來改變預設行為並滿足我們應用程式的需求。
針對特定情況選擇正確的方法需要對可用功能有深入的了解。
與往常一樣,本教程中使用的所有程式碼都可以在 GitHub 上找到。