Spring Data JPA中實現更新插入三種方法

banq發表於2024-04-23

在應用程式開發中,執行更新或插入操作(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());
}

相關文章