Spring Data JPA 实现 updateOrInsert(更新或保存)

1、简介

在应用开发中,执行 “更新或插入” 操作(也称为 “upsert”)的需要是很常见的。这个操作涉及将新记录存入数据库表中,如果记录不存在,则插入新记录;如果记录已经存在,则更新现有记录。

本文将带你了解使用 Spring Data JPA 执行 “更新或插入” 操作的不同方法。

2、实体类

定义 CreditCard 实体类用于演示:

@Entity
@Table(name="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;

   // Get / Set 方法省略
}

3、实现

本文介绍三种不同的方法来实现 “更新或插入”。

3.1、使用 Repository 方法

使用从 CrudRepository 接口继承的 save(entity) 方法在 Repository 中编写一个带事务的 default 方法。

public interface CreditCardRepository extends JpaRepository<CreditCard,Long> {
    @Transactional  // 记得加事务
    default CreditCard updateOrInsert(CreditCard entity) {
        return save(entity);
    }
}

如上,如果记录是新的,save(entity) 方法会插入该记录,否则就根据 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("3494323432112222");
    card.setExpiryDate("2024-06-21");
    card.setCustomerId(10L);
    return creditCardRepository.save(card);
}

使用上面保存的 CreditCard 进行更新。

创建一个用于插入的 CreditCard 对象:

private CreditCard buildCreditCard() {
    CreditCard card = new CreditCard();
    card.setCardNumber("9994323432112222");
    card.setExpiryDate("2024-06-21");
    card.setCustomerId(10L);

    return card;
}

编写单元测试:

@Test
void givenCreditCards_whenUpdateOrInsertUsingRepositoryExecuted_thenUpserted() {
    // insert test
    CreditCard newCreditCard = buildCreditCard();
    CreditCard existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber());
    assertNull(existingCardByCardNumber);

    creditCardLogic.updateOrInsertUsingRepository(newCreditCard);

    existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber());
    assertNotNull(existingCardByCardNumber);

    // update test
    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() 方法断言了插入和更新操作。

3.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() 方法中将传入的 CreditCard 作为新实体插入。

编写单元测试来验证自定义逻辑:

@Test
void givenCreditCards_whenUpdateOrInsertUsingCustomLogicExecuted_thenUpserted() {
    // insert test
    CreditCard newCreditCard = buildCreditCard();
    CreditCard existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber());
    assertNull(existingCardByCardNumber);

    creditCardLogic.updateOrInsertUsingCustomLogic(newCreditCard);

    existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber());
    assertNotNull(existingCardByCardNumber);

    // update test
    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.3、使用数据库内置功能

许多数据库都提供了处理插入冲突的内置功能。例如,PostgreSQL 提供了 ON CONFLICT DO UPDATE,MySQL 提供了 ON DUPLICATE KEY。利用这一功能,我们可以在向数据库插入记录时,在出现重复 KEY 时编写后续更新语句。

示例查询如下:

String updateOrInsert = """
    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 数据库提供的 merge 查询。

CreditCardLogic 类中添加 merge 逻辑:

@Transactional
public void updateOrInsertUsingBuiltInFeature(CreditCard creditCard) {
    Long id = creditCard.getId();
    if (creditCard.getId() == null) {
        BigInteger nextVal = (BigInteger) em.createNativeQuery("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 提供的原生查询执行 merge 查询。

编写单元测试来验证结果:

@Test
void givenCreditCards_whenUpdateOrInsertUsingBuiltInFeatureExecuted_thenUpserted() {
    // insert test
    CreditCard newCreditCard = buildCreditCard();
    CreditCard existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber());
    assertNull(existingCardByCardNumber);

    creditCardLogic.updateOrInsertUsingBuiltInFeature(newCreditCard);

    existingCardByCardNumber = creditCardRepository.findByCardNumber(newCreditCard.getCardNumber());
    assertNotNull(existingCardByCardNumber);

    // update test
    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());
}

4、总结

本文介绍了在 Spring Data JPA 中实现 “更新或插入” 操作的不同方法。


Ref:https://www.baeldung.com/spring-data-jpa-update-or-insert