Hibernate 中 INSERT 查询的 ON CONFLICT 子句

1、概览

本文将带你了解 Hibernate 6.5 中引入的用于 INSERT 查询的 ON CONFLICT 子句。

我们使用 ON CONFLICT 子句来处理使用 HQLCriteria 查询插入数据时违反表约束的情况。ON CONFLICT 子句也可以用于处理 upsert 查询。

2、ON CONFLICT 子句

使用 ON CONFLICT 子句进行 insert 的语法如下:

"INSERT" "INTO"? targetEntity targetFields (queryExpression | valuesList) conflictClause?

conflictClause 的写法为:

"on conflict" conflictTarget? conflictAction

conflictAction(冲突操作)可以是 DO NOTHING 或者 DO UPDATE

现在,来看一个例子。假设实体类 Student 的属性有 studentIdname

@Entity
public class Student {
    @Id
    private long studentId;
    private String name;
}

studentId 属性是 Student 实体的唯一键。我们可以在 INSERT VALUES 查询中手动插入 @Id 值,或者使用 @GeneratedValue 注解来指定 ID 的生成策略。

可以根据唯一约束的名称或属性列表来处理冲突。要使用唯一约束名称作为冲突目标,需要数据库的原生支持,或者操作是单行 insert 插入。

冲突处理的操作包括 忽略冲突更新冲突 的对象/行:

int updated = session.createMutationQuery("""
  insert into Student (studentId, name)
  values (1, 'John')
  on conflict(studentId) do update
  set name = excluded.name
  """).executeUpdate();

如上,如果插入一条与现有记录具有相同 studentId 的记录发生冲突,那么现有记录将被更新。特殊别名 excludedON CONFLICT 子句的 update set 子句中可用,指的是由于唯一约束冲突而插入失败的值。

Hibernate 会将带有 ON CONFLICT 子句的查询转换为 Merge Query(合并查询):

MERGE INTO Student s1_0
USING (VALUES (1, 'John')) excluded(studentId, NAME)
ON ( s1_0.studentId = excluded.studentId)
WHEN matched THEN
  UPDATE SET NAME = excluded.NAME
WHEN NOT matched THEN
  INSERT (studentId,
          NAME)
  VALUES (excluded.studentId,
          excluded.NAME) 

使用 excluded 功能将 ON CONFLICT 子句转换为 upsert 查询。该查询排除了别名为 p1_0 的原始记录,并插入了新的记录。如果 studentId(冲突属性)匹配,则更新 studentId 以外的属性。如果不匹配,则执行插入操作。

我们使用 DO NOTHING 来忽略冲突,确保在发生冲突时不会发生任何事情,避免潜在错误:

int updated = session.createMutationQuery("""
  insert into Student (studentId, name)
  values (1, 'John')
  on conflict(studentId) do nothing
  """).executeUpdate();

如上,如果表中已经包含了一条 studentId1 的记录,Hibernate 会忽略查询并避免异常。

3、示例

3.1、DO UPDATE

我们添加了一些测试用例,以更好地理解 ON CONFLICT 子句。我们在插入非冲突数据时使用了 do update(冲突操作更新):

long rowCountBefore = getRowCount();
int updated = session.createMutationQuery("""
  insert into Student (studentId, name) values (2, 'Sean')
  on conflict(studentId) do update
  set name = excluded.name
  """).executeUpdate();
long rowCountAfter = getRowCount();
assertEquals(updated, 1);
assertEquals(rowCountBefore, rowCountAfter);

插入的数据不冲突,并已插入数据库。

当不存在冲突时,Hibernate 会忽略 ON CONFLICT 子句,查询执行后会返回更新值 1。执行语句后,行数量发生变化,表明查询在表中插入了一行。

现在,来看一个测试用例,在这个测试用例中,我们插入了冲突数据,并通过 冲突操作 进行了更新:

long rowCountBefore = getRowCount();
int updated = session.createMutationQuery("""
  insert into Student (studentId, name) values (1, 'Sean')
  on conflict(studentId) do update
  set name = excluded.name
  """).executeUpdate();
long rowCountAfter = getRowCount();
assertEquals(updated, 1);
assertEquals(rowCountBefore, rowCountAfter);

在这个测试案例中,查询插入了一条 studentId1 的记录,而表中已经有一条 studentId1 的记录。 如果执行查询,通常会抛出 ConstraintViolationException 异常,因为 studentId 是唯一约束。我们使用 ON CONFLICT 子句来处理这个异常。指定的冲突操作不会产生异常,而是会更新指定的数据。

set name = excluded.name 这一行更新了 name 字段。关键字 excluded 自带冲突操作,什么也不做。

我们可以使用关键字 excluded 更新冲突字段以外的所有字段。

3.2、DO NOTHING

现在,来看看当我们使用 ON CONFLICT 子句插入非冲突数据,并将冲突操作设置为 do nothing(不做任何操作)时,会发生什么情况:

long rowCountBefore = getRowCount();
int updated = session.createMutationQuery("""
  insert into Student (studentId, name) values (2, 'Sean')
  on conflict do nothing 
  """).executeUpdate();
long rowCountAfter = getRowCount();
assertEquals(updated, 1);
assertNotEquals(rowCountBefore, rowCountAfter);

你可以看到,在向数据库插入数据记录时没有发生冲突。查询返回的更新值为 1。查询执行后,表中的行数增加了 1

来看看插入冲突数据并使用 ON CONFLICT 子句和 do nothing(什么都不做)操作的情况:

long rowCountBefore = getRowCount();
int updated = session.createMutationQuery("""
  insert into Student (studentId, name) values (1, 'Sean')
  on conflict do nothing 
  """).executeUpdate();
long rowCountAfter = getRowCount();
assertEquals(updated, 0);
assertEquals(rowCountBefore, rowCountAfter);

如上,插入记录的 studentId1,因此在执行 insert 查询时会产生冲突。由于我们使用了 “do nothing”(什么都不做)操作,因此在发生冲突时不会执行任何操作。查询执行后会返回更新值 0,表示未更新任何记录。此外,查询执行前后的记录数量保持不变。

4、总结

本文介绍了 Hibernate 6.5 中引入的用于 INSERT 查询的 ON CONFLICT 子句,以处理使用 HQL 或 Criteria 查询插入数据时违反表约束的情况。


Ref:https://www.baeldung.com/hibernate-insert-query-on-conflict-clause