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;

   // getters and setters
}

3. 实现

我们将通过三种不同的方式实现更新或插入操作。

3.1. 使用仓库方法

在这个方法中,我们将在仓库中编写一个事务性的默认方法,使用来自 CrudRepository 接口继承的 save(entity) 方法。save(entity) 方法会在记录新时插入,根据 id 更新现有实体:

public interface CreditCardRepository extends JpaRepository<CreditCard,Long> {
    @Transactional
    default CreditCard updateOrInsert(CreditCard entity) {
        return save(entity);
    }
}

CreditCardLogic 类的 updateOrInsertUsingReposiotry() 方法中,我们将 creditCard 传递给该方法,根据实体的 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”。利用这个特性,当尝试向数据库插入记录时,我们可以编写后续的更新语句。*

示例查询可能如下所示:

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 中执行更新或插入操作的不同方法。我们实现了这些方法,并使用单元测试进行了验证。虽然每个数据库都提供了处理 upsert 的一些内置功能,但在基于 id 列的 Spring Data JPA 上层逻辑中实现自定义的 upsert 并不复杂。

如往常一样,示例代码可在 GitHub 上找到。