在應用程式開發中,執行更新或插入操作(Update-Or-Insert也稱為“upsert”)的需求非常常見。此操作涉及將新記錄放入資料庫表(如果不存在)或更新現有記錄(如果存在)。
在本教程中,我們將學習使用Spring Data JPA執行更新或插入操作的不同方法。
出於演示目的,我們將使用CreditCard實體:
@Entity @Table(name=<font>"credit_card") public class CreditCard { @Id @GeneratedValue(strategy= GenerationType.SEQUENCE, generator = "credit_card_id_seq") @SequenceGenerator(name = "credit_card_id_seq", sequenceName = "credit_card_id_seq", allocationSize = 1) private Long id; private String cardNumber; private String expiryDate; private Long customerId; // getters and setters<i> }
|
我們將使用三種不同的方法來實現更新或插入。
1.使用儲存庫方法
在這種方法中,我們將使用save(entity)方法在儲存庫中編寫事務預設方法,該方法繼承自CrudRepository介面。 save (entity) 方法會插入新記錄,或者根據 id 更新現有實體:
public interface CreditCardRepository extends JpaRepository<CreditCard,Long> { @Transactional default CreditCard updateOrInsert(CreditCard entity) { return save(entity); } }
|
我們將CreditCard傳遞給CreditCardLogic類中的updateOrInsertUsingReposiotry()方法,該方法根據實體id插入或更新實體:@Service public class CreditCardLogic { @Autowired private CreditCardRepository creditCardRepository; public void updateOrInsertUsingRepository(CreditCard creditCard) { creditCardRepository.updateOrInsert(creditCard); } }
|
這種方法的一個重要注意事項是實體是否要更新由id決定。如果我們需要根據另一列查詢現有記錄,例如使用cardNumber而不是id,那麼這種方法將不起作用。在這種情況下,我們可以使用後面幾節中討論的方法。我們可以編寫單元測試來驗證我們的邏輯。首先,我們將一些測試資料儲存到credit_card表中:
private CreditCard createAndReturnCreditCards() { CreditCard card = new CreditCard(); card.setCardNumber(<font>"3494323432112222"); card.setExpiryDate("2024-06-21"); card.setCustomerId(10L); return creditCardRepository.save(card); }
|
我們將使用上面儲存的信用卡進行更新。讓我們構建一個用於插入的CreditCard物件:private CreditCard buildCreditCard() { CreditCard card = new CreditCard(); card.setCardNumber(<font>"9994323432112222"); card.setExpiryDate("2024-06-21"); card.setCustomerId(10L); return card; }
|
我們準備好編寫我們的單元測試:@Test void givenCreditCards_whenUpdateOrInsertUsingRepositoryExecuted_thenUpserted() { <font>// insert test<i> CreditCard newCreditCard = buildCreditCard(); CreditCard existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber()); assertNull(existingCardByCardNumber); creditCardLogic.updateOrInsertUsingRepository(newCreditCard); existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber()); assertNotNull(existingCardByCardNumber); // update test<i> CreditCard cardForUpdate = existingCard; String beforeExpiryDate = cardForUpdate.getExpiryDate(); cardForUpdate.setExpiryDate("2029-08-29"); existingCardByCardNumber = creditCardRepository.findByCardNumber(cardForUpdate.getCardNumber()); assertNotNull(existingCardByCardNumber); creditCardLogic.updateOrInsertUsingRepository(cardForUpdate); assertNotEquals("2029-08-29", beforeExpiryDate); CreditCard updatedCard = creditCardRepository.findById(cardForUpdate.getId()).get(); assertEquals("2029-08-29", updatedCard.getExpiryDate()); }
|
在上面的測試中,我們為updateOrInsertUsingRepository()方法斷言插入和更新操作。2.使用自定義邏輯
在這種方法中,我們在CreditCardLogic類中編寫自定義邏輯,該類首先檢查給定的行是否已存在於表中,然後根據輸出決定插入或更新記錄:
public void updateOrInsertUsingCustomLogic(CreditCard creditCard) { CreditCard existingCard = creditCardRepository.findByCardNumber(creditCard.getCardNumber()); if (existingCard != null) { existingCard.setExpiryDate(creditCard.getExpiryDate()); creditCardRepository.save(creditCard); } else { creditCardRepository.save(creditCard); } }
|
根據上述邏輯,如果資料庫中已存在cardNumber ,則我們根據傳遞的CreditCard物件更新該現有實體。否則,我們將傳遞的信用卡作為新實體插入updateOrInsertUsingCustomLogic()方法中。我們可以編寫單元測試來驗證我們的自定義邏輯:
@Test void givenCreditCards_whenUpdateOrInsertUsingCustomLogicExecuted_thenUpserted() { <font>// insert test<i> CreditCard newCreditCard = buildCreditCard(); CreditCard existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber()); assertNull(existingCardByCardNumber); creditCardLogic.updateOrInsertUsingCustomLogic(newCreditCard); existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber()); assertNotNull(existingCardByCardNumber); // update test<i> CreditCard cardForUpdate = existingCard; String beforeExpiryDate = cardForUpdate.getExpiryDate(); cardForUpdate.setExpiryDate("2029-08-29"); creditCardLogic.updateOrInsertUsingCustomLogic(cardForUpdate); assertNotEquals("2029-08-29", beforeExpiryDate); CreditCard updatedCard = creditCardRepository.findById(cardForUpdate.getId()).get(); assertEquals("2029-08-29", updatedCard.getExpiryDate()); }
|
3.使用資料庫內建功能
許多資料庫提供內建功能來處理插入衝突。例如,PostgreSQL 提供“ON CONFLICT DO UPDATE”,MySQL 提供“ON DUPLICATE KEY”。利用這個特性,我們可以在向資料庫插入一條記錄時,當出現重複鍵時,編寫後續的更新語句。
一個示例查詢如下:
String updateOrInsert = <font>""" INSERT INTO credit_card (card_number, expiry_date, customer_id) VALUES( :card_number, :expiry_date, :customer_id ) ON CONFLICT ( card_number ) DO UPDATE SET card_number = :card_number, expiry_date = :expiry_date, customer_id = :customer_id """;
|
為了進行測試,我們使用 H2 資料庫,它不提供“ON CONFLICT”功能,但我們可以使用H2 資料庫提供的合併查詢。讓我們在CreditCardLogic類中新增合併邏輯:@Transactional public void updateOrInsertUsingBuiltInFeature(CreditCard creditCard) { Long id = creditCard.getId(); if (creditCard.getId() == null) { BigInteger nextVal = (BigInteger) em.createNativeQuery(<font>"SELECT nextval('credit_card_id_seq')").getSingleResult(); id = nextVal.longValue(); } String upsertQuery = """ MERGE INTO credit_card (id, card_number, expiry_date, customer_id) KEY(card_number) VALUES (?, ?, ?, ?) """; Query query = em.createNativeQuery(upsertQuery); query.setParameter(1, id); query.setParameter(2, creditCard.getCardNumber()); query.setParameter(3, creditCard.getExpiryDate()); query.setParameter(4, creditCard.getCustomerId()); query.executeUpdate(); }
|
在上面的邏輯中,我們使用entityManager提供的本機查詢執行合併查詢。現在,讓我們編寫單元測試來驗證結果:
@Test void givenCreditCards_whenUpdateOrInsertUsingBuiltInFeatureExecuted_thenUpserted() { <font>// insert test<i> CreditCard newCreditCard = buildCreditCard(); CreditCard existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber()); assertNull(existingCardByCardNumber); creditCardLogic.updateOrInsertUsingBuiltInFeature(newCreditCard); existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber()); assertNotNull(existingCardByCardNumber); // update test<i> CreditCard cardForUpdate = existingCard; String beforeExpiryDate = cardForUpdate.getExpiryDate(); cardForUpdate.setExpiryDate("2029-08-29"); creditCardLogic.updateOrInsertUsingBuiltInFeature(cardForUpdate); assertNotEquals("2029-08-29", beforeExpiryDate); CreditCard updatedCard = creditCardRepository.findById(cardForUpdate.getId()).get(); assertEquals("2029-08-29", updatedCard.getExpiryDate()); }
|