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 上找到。