使用 Java (17)、Hibernate (6.4.1.Final) 和 H2 数据库 (2.2.224) 的 Maven 项目。
有两个(不相关的)实体(
MyFirstEntity
和 MySecondEntity
),第二个实体具有自引用(@OneToOne
与 @JoinTable
映射)。这些实体使用三个表:
MY_FIRST_ENTITY
:id
,title
(独特)
MY_SECOND_ENTITY
:id
,title
MY_SECOND_ENTITY_MAPPING
:a
(参考MY_SECOND_ENTITY
),b
(参考MY_SECOND_ENTITY
)
在这个问题的最后提供了一个Java文件、一个hibernate属性文件和一个maven pom文件,可用于重现问题。并且也可能有助于遵循文本问题描述。
我在将 Spring Boot 应用程序从版本 2 升级到版本 3 时偶然发现了这个问题。这里提供的示例是
DataJpaTest
测试类中发生的情况的一个非常精简的重现。
在事务中,
MyFirstEntity
的条目以标题“a”保留(此列有一个唯一的约束),并且会话被刷新。之后执行更新语句:UPDATE MY_SECOND_ENTITY SET title=null
(这应该是无操作,因为该表中没有条目)。然后事务回滚。
此后,创建另一个交易,并再次以标题“a”保留
MyFirstEntity
条目,这次交易被 commited 。提交会导致错误,因为违反了唯一约束:
唯一索引或主键违规:“PUBLIC.CONSTRAINT_INDEX_2 ON PUBLIC.MY_FIRST_ENTITY(TITLE NULLS FIRST) VALUES (/* 1 */ 'a')”
这让我感到惊讶,因为第一个事务被回滚了。 Hibernate似乎正在使用临时表来优化更新语句(多表批量操作)。如果删除更新语句,则一切正常,并且第二个事务中不会违反约束。还可以删除刷新调用或在插入语句之前移动更新语句来修复约束违规。
第一笔交易:
Hibernate:插入 MY_FIRST_ENTITY (title,id) 值 (?,?)
Hibernate:创建本地临时表HT_MY_SECOND_ENTITY(id uuid不为空,主键(id))
休眠:插入 HT_MY_SECOND_ENTITY(id) 选择 mse1_0.id from MY_SECOND_ENTITY mse1_0
Hibernate:更新 MY_SECOND_ENTITY 设置 title=null 其中 id in (从 HT_MY_SECOND_ENTITY temptable_ 选择 temptable_.id)
休眠:从 HT_MY_SECOND_ENTITY 删除
第二笔交易:
Hibernate:插入 MY_FIRST_ENTITY (title,id) 值 (?,?)
谁能解释为什么回滚在某些情况下不起作用?
这是预期的行为还是可以被视为错误?
任何帮助或提示表示赞赏。
import java.util.UUID;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.OneToOne;
public class DemoHibernateApplication {
private static SessionFactory sessionFactory;
private static final String CREATION_TABLE_1 = """
CREATE TABLE "MY_FIRST_ENTITY" (
"ID" UUID NOT NULL,
"TITLE" VARCHAR(100) UNIQUE NOT NULL,
PRIMARY KEY ("ID")
);
""";
private static final String CREATION_TABLE_2 = """
CREATE TABLE "MY_SECOND_ENTITY" (
"ID" UUID NOT NULL,
"TITLE" VARCHAR(100) NOT NULL,
PRIMARY KEY ("ID")
);
""";
private static final String CREATION_TABLE_3 = """
CREATE TABLE "MY_SECOND_ENTITY_MAPPING" (
"A" UUID NOT NULL,
"B" UUID NOT NULL,
PRIMARY KEY ("A", "B"),
FOREIGN KEY("A") REFERENCES MY_SECOND_ENTITY("ID"),
FOREIGN KEY("B") REFERENCES MY_SECOND_ENTITY("ID")
);
""";
public static void main(String[] args) {
createTables();
executeQueries();
}
private static void createTables() {
try (Session session = getSessionFactory().openSession()) {
Transaction transaction = session.beginTransaction();
session.createNativeMutationQuery(CREATION_TABLE_1)
.executeUpdate();
session.createNativeMutationQuery(CREATION_TABLE_2)
.executeUpdate();
session.createNativeMutationQuery(CREATION_TABLE_3)
.executeUpdate();
transaction.commit();
}
}
private static void executeQueries() {
try (Session session = getSessionFactory().openSession()) {
Transaction transaction = session.beginTransaction();
session.persist(newMyFirstEntityWithTitle("a"));
session.flush();
session.createMutationQuery("UPDATE MY_SECOND_ENTITY SET title=null")
.executeUpdate();
transaction.rollback();
}
try (Session session = getSessionFactory().openSession()) {
Transaction transaction = session.beginTransaction();
session.persist(newMyFirstEntityWithTitle("a"));
transaction.commit();
}
}
private static MyFirstEntity newMyFirstEntityWithTitle(String title) {
MyFirstEntity myFirstEntity = new MyFirstEntity();
myFirstEntity.setId(UUID.randomUUID());
myFirstEntity.setTitle(title);
return myFirstEntity;
}
private static SessionFactory getSessionFactory() {
if (sessionFactory == null) {
StandardServiceRegistry registry = new StandardServiceRegistryBuilder().build();
sessionFactory = new MetadataSources(registry)
.addAnnotatedClass(MyFirstEntity.class)
.addAnnotatedClass(MySecondEntity.class)
.buildMetadata()
.buildSessionFactory();
}
return sessionFactory;
}
@Entity(name = "MY_FIRST_ENTITY")
public static class MyFirstEntity {
@Id
private UUID id;
@Column(nullable = false, unique = true, columnDefinition = "nvarchar")
private String title;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
@Entity(name = "MY_SECOND_ENTITY")
public static class MySecondEntity {
@Id
private UUID id;
@Column(nullable = false, columnDefinition = "nvarchar")
private String title;
@OneToOne
@JoinTable(name = "MY_SECOND_ENTITY_MAPPING", joinColumns = { @JoinColumn(name = "a") }, inverseJoinColumns = { @JoinColumn(name = "b") })
private MySecondEntity anotherMySecondEntity;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public MySecondEntity getAnotherMySecondEntity() {
return anotherMySecondEntity;
}
public void setAnotherMySecondEntity(MySecondEntity anotherMySecondEntity) {
this.anotherMySecondEntity = anotherMySecondEntity;
}
}
}
hibernate.connection.url=jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1
hibernate.connection.username=sa
hibernate.connection.password=
hibernate.show_sql=true
hibernate.hbm2ddl.auto=none
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo-hibernate</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-hibernate</name>
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.1.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
</dependencies>
</project>
这是 H2executeUpdate 函数特有的。来电的时候
session.createMutationQuery("UPDATE MY_SECOND_ENTITY SET title=null")
.executeUpdate();
如果您检查上面的文档行,它就会转到 executeUpdate。
如果自动提交开启,则该语句将被提交。如果该语句是 DDL 语句(create、drop、alter)并且不 抛出异常,执行该语句后提交当前事务(如果有)。
这意味着即使在“回滚”之前,executeUpdate 也会提交在内部生成数据的事务,之后您会收到“唯一索引或主键冲突”
这里是一个使用 postgresql 方法的示例。基本上它有休眠属性和额外的配置只是为了测试。
hibernate.connection.url=jdbc:h2:mem:db1;TRACE_LEVEL_SYSTEM_OUT=2;AUTOCOMMIT=OFF
#hibernate.connection.url=jdbc:postgresql://localhost/test
hibernate.connection.username=postgres
hibernate.connection.password=
hibernate.show_sql=true
#hibernate.hbm2ddl.auto=create-drop
hibernate.hbm2ddl.auto=none
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
当我在本地使用 postgresql 运行时,我看到下面的日志。
/home/oz-mint/.sdkman/candidates/java/21-graalce/bin/java -javaagent:/home/oz-mint/.local/share/JetBrains/Toolbox/apps/intellij-idea-ultimate/lib/idea_rt.jar=32809:/home/oz-mint/.local/share/JetBrains/Toolbox/apps/intellij-idea-ultimate/bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /home/oz-mint/projects/java-examlpes/hibernate6/target/classes:/home/oz-mint/.m2/repository/org/hibernate/orm/hibernate-core/6.4.1.Final/hibernate-core-6.4.1.Final.jar:/home/oz-mint/.m2/repository/jakarta/persistence/jakarta.persistence-api/3.1.0/jakarta.persistence-api-3.1.0.jar:/home/oz-mint/.m2/repository/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar:/home/oz-mint/.m2/repository/org/jboss/logging/jboss-logging/3.5.0.Final/jboss-logging-3.5.0.Final.jar:/home/oz-mint/.m2/repository/org/hibernate/common/hibernate-commons-annotations/6.0.6.Final/hibernate-commons-annotations-6.0.6.Final.jar:/home/oz-mint/.m2/repository/io/smallrye/jandex/3.1.2/jandex-3.1.2.jar:/home/oz-mint/.m2/repository/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar:/home/oz-mint/.m2/repository/net/bytebuddy/byte-buddy/1.14.7/byte-buddy-1.14.7.jar:/home/oz-mint/.m2/repository/jakarta/xml/bind/jakarta.xml.bind-api/4.0.0/jakarta.xml.bind-api-4.0.0.jar:/home/oz-mint/.m2/repository/jakarta/activation/jakarta.activation-api/2.1.0/jakarta.activation-api-2.1.0.jar:/home/oz-mint/.m2/repository/org/glassfish/jaxb/jaxb-runtime/4.0.2/jaxb-runtime-4.0.2.jar:/home/oz-mint/.m2/repository/org/glassfish/jaxb/jaxb-core/4.0.2/jaxb-core-4.0.2.jar:/home/oz-mint/.m2/repository/org/eclipse/angus/angus-activation/2.0.0/angus-activation-2.0.0.jar:/home/oz-mint/.m2/repository/org/glassfish/jaxb/txw2/4.0.2/txw2-4.0.2.jar:/home/oz-mint/.m2/repository/com/sun/istack/istack-commons-runtime/4.1.1/istack-commons-runtime-4.1.1.jar:/home/oz-mint/.m2/repository/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar:/home/oz-mint/.m2/repository/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar:/home/oz-mint/.m2/repository/com/h2database/h2/2.2.224/h2-2.2.224.jar:/home/oz-mint/.m2/repository/org/postgresql/postgresql/42.7.1/postgresql-42.7.1.jar:/home/oz-mint/.m2/repository/org/checkerframework/checker-qual/3.41.0/checker-qual-3.41.0.jar DemoHibernateApplication
Jan 13, 2024 12:02:39 AM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate ORM core version 6.4.1.Final
Jan 13, 2024 12:02:39 AM org.hibernate.cfg.Environment <clinit>
INFO: HHH000205: Loaded properties from resource hibernate.properties: {hibernate.hbm2ddl.auto=create-drop, hibernate.connection.password=****, hibernate.connection.username=postgres, logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE, hibernate.connection.url=jdbc:postgresql://localhost/test, hibernate.show_sql=true, logging.level.org.hibernate.SQL=DEBUG}
Jan 13, 2024 12:02:39 AM org.hibernate.cache.internal.RegionFactoryInitiator initiateService
INFO: HHH000026: Second-level cache disabled
Jan 13, 2024 12:02:39 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
WARN: HHH10001002: Using built-in connection pool (not intended for production use)
Jan 13, 2024 12:02:39 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001005: Loaded JDBC driver class: org.postgresql.Driver
Jan 13, 2024 12:02:39 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001012: Connecting with JDBC URL [jdbc:postgresql://localhost/test]
Jan 13, 2024 12:02:39 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001001: Connection properties: {password=****, user=postgres}
Jan 13, 2024 12:02:39 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl buildCreator
INFO: HHH10001003: Autocommit mode: false
Jan 13, 2024 12:02:39 AM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections <init>
INFO: HHH10001115: Connection pool size: 20 (min=1)
Jan 13, 2024 12:02:40 AM org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate: alter table if exists MY_SECOND_ENTITY_MAPPING drop constraint if exists FK1c0qvkaoyxgvk8j7lge0f4dvx
Hibernate: alter table if exists MY_SECOND_ENTITY_MAPPING drop constraint if exists FKosyih1e60ld4bp8f7hege9xar
Hibernate: drop table if exists MY_FIRST_ENTITY cascade
Hibernate: drop table if exists MY_SECOND_ENTITY cascade
Hibernate: drop table if exists MY_SECOND_ENTITY_MAPPING cascade
Jan 13, 2024 12:02:40 AM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@6e03db1f] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
Jan 13, 2024 12:02:40 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: SQL Warning Code: 0, SQLState: 00000
Jan 13, 2024 12:02:40 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: constraint "fk1c0qvkaoyxgvk8j7lge0f4dvx" of relation "my_second_entity_mapping" does not exist, skipping
Jan 13, 2024 12:02:40 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: SQL Warning Code: 0, SQLState: 00000
Jan 13, 2024 12:02:40 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: constraint "fkosyih1e60ld4bp8f7hege9xar" of relation "my_second_entity_mapping" does not exist, skipping
Jan 13, 2024 12:02:40 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: SQL Warning Code: 0, SQLState: 00000
Jan 13, 2024 12:02:40 AM org.hibernate.engine.jdbc.spi.SqlExceptionHelper$StandardWarningHandler logWarning
WARN: drop cascades to 2 other objects
Hibernate: create table MY_FIRST_ENTITY (id uuid not null, title varchar(255) not null unique, primary key (id))
Hibernate: create table MY_SECOND_ENTITY (id uuid not null, title varchar(255) not null, primary key (id))
Hibernate: create table MY_SECOND_ENTITY_MAPPING (anotherMySecondEntity_id uuid unique, id uuid not null, primary key (id))
Hibernate: alter table if exists MY_SECOND_ENTITY_MAPPING add constraint FK1c0qvkaoyxgvk8j7lge0f4dvx foreign key (anotherMySecondEntity_id) references MY_SECOND_ENTITY
Jan 13, 2024 12:02:40 AM org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@7d97e06c] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
Hibernate: alter table if exists MY_SECOND_ENTITY_MAPPING add constraint FKosyih1e60ld4bp8f7hege9xar foreign key (id) references MY_SECOND_ENTITY
Hibernate: insert into MY_FIRST_ENTITY (title,id) values (?,?)
Hibernate: with id_cte (id) as materialized (select mse1_0.id from MY_SECOND_ENTITY mse1_0 left join MY_SECOND_ENTITY_MAPPING mse1_1 on mse1_0.id=mse1_1.id),update_cte_MY_SECOND_ENTITY (id) as (update MY_SECOND_ENTITY set title=null where id in (select id.id from id_cte id) returning id) select count(*) from id_cte id
Hibernate: insert into MY_FIRST_ENTITY (title,id) values (?,?)
Process finished with exit code 0
并且相同的 Java 代码不会生成唯一索引异常。