1、概览 {#1概览}
本文将带你了解 Hibernate 6.5 中引入的用于 INSERT
查询的 ON CONFLICT
子句。
我们使用 ON CONFLICT
子句来处理使用 HQL 或 Criteria 查询插入数据时违反表约束的情况。ON CONFLICT
子句也可以用于处理 upsert
查询。
2、ON CONFLICT 子句 {#2on-conflict-子句}
使用 ON CONFLICT
子句进行 insert 的语法如下:
"INSERT" "INTO"? targetEntity targetFields (queryExpression | valuesList) conflictClause?
conflictClause
的写法为:
"on conflict" conflictTarget? conflictAction
conflictAction (冲突操作)可以是 DO NOTHING
或者 DO UPDATE
。
现在,来看一个例子。假设实体类 Student
的属性有 studentId
和 name
:
@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
的记录发生冲突,那么现有记录将被更新。特殊别名 excluded
在 ON 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();
如上,如果表中已经包含了一条 studentId
为 1 的记录,Hibernate 会忽略查询并避免异常。
3、示例 {#3示例}
3.1、DO UPDATE {#31do-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);
在这个测试案例中,查询插入了一条 studentId
为 1
的记录,而表中已经有一条 studentId
为 1
的记录。 如果执行查询,通常会抛出 ConstraintViolationException
异常,因为 studentId
是唯一约束。我们使用 ON CONFLICT
子句来处理这个异常。指定的冲突操作不会产生异常,而是会更新指定的数据。
set name = excluded.name
这一行更新了 name
字段。关键字 excluded
自带冲突操作,什么也不做。
我们可以使用关键字 excluded
更新冲突字段以外的所有字段。
3.2、DO NOTHING {#32do-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);
如上,插入记录的 studentId
为 1 ,因此在执行 insert
查询时会产生冲突。由于我们使用了 "do nothing"(什么都不做)操作,因此在发生冲突时不会执行任何操作。查询执行后会返回更新值 0
,表示未更新任何记录。此外,查询执行前后的记录数量保持不变。
4、总结 {#4总结}
本文介绍了 Hibernate 6.5 中引入的用于 INSERT
查询的 ON CONFLICT
子句,以处理使用 HQL 或 Criteria 查询插入数据时违反表约束的情况。
Ref:https://www.baeldung.com/hibernate-insert-query-on-conflict-clause