1、简介 {#1简介}
在应用开发中,执行 "更新或插入" 操作(也称为 "upsert")的需要是很常见的。这个操作涉及将新记录存入数据库表中,如果记录不存在,则插入新记录;如果记录已经存在,则更新现有记录。
本文将带你了解使用 Spring Data JPA 执行 "更新或插入" 操作的不同方法。
2、实体类 {#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实现}
本文介绍三种不同的方法来实现 "更新或插入"。
3.1、使用 Repository 方法 {#31使用-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、使用自定义逻辑 {#32使用自定义逻辑}
在 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、使用数据库内置功能 {#33使用数据库内置功能}
许多数据库都提供了处理插入冲突的内置功能。例如,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、总结 {#4总结}
本文介绍了在 Spring Data JPA 中实现 "更新或插入" 操作的不同方法。
Ref:https://www.baeldung.com/spring-data-jpa-update-or-insert