Spring Data / Hibernate 使用 Postgres 保存实体,在冲突更新某些字段时使用插入

abs*_*hit 8 postgresql spring spring-data-jpa spring-boot

我在 Spring 中有一个域对象,我使用 JpaRepository.save 方法保存它并使用 Postgres 的序列生成器自动生成 id。

@SequenceGenerator(initialValue = 1, name = "device_metric_gen", sequenceName = "device_metric_seq")
public class DeviceMetric extends BaseTimeModel {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "device_metric_gen")
    @Column(nullable = false, updatable = false)
    private Long id;
///// extra fields
Run Code Online (Sandbox Code Playgroud)

我的用例需要做一个upsert而不是正常的save操作(我知道如果 id 存在就会更新)。如果存在三列的组合(假设复合唯一),我想更新现有行,否则创建新行。这与类似:

INSERT INTO customers (name, email)
VALUES
   (
      'Microsoft',
      'hotline@microsoft.com'
   ) 
ON CONFLICT (name) 
DO
      UPDATE
     SET email = EXCLUDED.email || ';' || customers.email;
Run Code Online (Sandbox Code Playgroud)

我能想到的在 Spring-data 中实现相同的一种方法是:

  1. 在服务层编写自定义保存操作
  2. 是否获取三列以及是否存在一行
  3. 在当前对象中设置相同的 id 并执行 repository.save
  4. 如果不存在行,则执行正常的 repository.save

上述方法的问题在于,每次insert都执行 aselect和 thensave进行两次数据库调用,而 postgresinsert on conflict功能只需一次 db 调用即可实现相同的效果。关于如何在 Spring Data 中实现这一点的任何指示?

一种方法是编写本机查询insert into values (all fields here)。有问题的对象有大约 25 个字段,所以我正在寻找另一种更好的方法来实现相同的目标。

小智 0

我建议使用命名查询来根据候选键获取行。如果存在一行,则更新它,否则创建一个新行。这两个操作都可以使用 save 方法来完成。

@NamedQuery(name="getCustomerByNameAndEmail", query="select a from Customers a where a.name = :name and a.email = :email");
Run Code Online (Sandbox Code Playgroud)

您还可以在实体上使用 @UniqueColumns() 注释,以确保这些列在分组在一起时始终保持唯一性。

Optional<Customers> customer = customerRepo.getCustomersByNameAndEmail(name, email);
Run Code Online (Sandbox Code Playgroud)

在您的存储库中实现上述方法。它所要做的就是调用查询并将名称和电子邮件作为参数传递。如果不存在任何行,请确保返回Optional.empty()。

Customers c;
if (customer.isPresent()) {
    c = customer.get();
    c.setEmail("newemail@gmail.com");
    c.setPhone("9420420420");
    customerRepo.save(c);
} else {
    c = new Customer(0, "name", "email", "5451515478");
    customerRepo.save(c);
}
Run Code Online (Sandbox Code Playgroud)

将 ID 传递为 0,JPA 将插入一个新行,其中包含根据序列生成器生成的 ID。

尽管我从不建议使用数字作为 ID,但如果可能的话,使用随机生成的 UUID 作为主键,它将保证唯一性并避免序列生成器可能出现的任何意外行为。