使用 Hibernate 的 @PostPersist 方法的外键中不允许使用 NULL
NULL not allowed in foreign key in @PostPersist method with Hibernate
我将 Quarkus 与 Hibernate 和 Resteasy 结合使用。
持久化实体时,我想自动创建引用第一个实体的第二个实体。
我尝试使用 @PostPersist
注释方法,但失败并出现 SQL 异常。当手动调用该方法时,它会起作用。
MWE:
CREATE TABLE "entity_a" (
"id" IDENTITY
);
CREATE TABLE "entity_b" (
"id" IDENTITY,
"entity_a_id" BIGINT NOT NULL,
FOREIGN KEY ("entity_a_id") REFERENCES "entity_a"("id"),
);
@Entity
public class EntityA {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
// getter, setter
@PostPersist
public void postPersist() {
EntityB b = new EntityB();
b.setEntityA(this);
JpaOperations.getEntityManager().persist(b);
}
}
@Entity
public class EntityB {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@OneToOne
private EntityA entityA;
// getters, setters
}
// in the rest resource class
@POST
@Transactional
public void create() {
EntityA a = new EntityA();
JpaOperations.getEntityManager().persist(a);
}
调用操作时,出现以下错误:
org.h2.jdbc.JdbcSQLException: NULL not allowed for column "entity_a_id"
NULL not allowed for column "entity_a_id"; SQL statement:
insert into "entity_b" ("id", "entity_a_id") values (null, ?) [23502-197]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at org.h2.message.DbException.get(DbException.java:179)
at org.h2.message.DbException.get(DbException.java:155)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:374)
at org.h2.table.Table.validateConvertUpdateSequence(Table.java:798)
at org.h2.command.dml.Insert.insertRows(Insert.java:177)
at org.h2.command.dml.Insert.update(Insert.java:134)
at org.h2.command.CommandContainer.update(CommandContainer.java:102)
at org.h2.command.Command.executeUpdate(Command.java:261)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:199)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:153)
at io.agroal.pool.wrapper.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:86)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:45)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3200)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3806)
at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:84)
at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:645)
at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:282)
at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:263)
at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:317)
at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:330)
at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:287)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:193)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:123)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:185)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:128)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:720)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:706)
at io.quarkus.hibernate.orm.runtime.entitymanager.TransactionScopedEntityManager.persist(TransactionScopedEntityManager.java:116)
at io.quarkus.hibernate.orm.runtime.entitymanager.ForwardingEntityManager.persist(ForwardingEntityManager.java:27)
at example.EntityA.postPersist(EntityA.java:24)
...
当我删除 @PostPersist
注释并从剩余资源(getEntityManager().persist
之后)调用该方法时,它按预期工作。
当我在 postPersist
中记录 id 值时,它已被设置。为什么会失败,我该如何解决?
虽然这很奇怪但是是正确的行为。基本上 entityManager
不必在调用 persist()
时立即生成身份。这取决于所选的 IdentifierGenerator 的策略。
但是IDENTITY
表示持久性提供者必须使用数据库标识列为实体分配主键,它只保证在事务提交或调用flush()时分配。
有很多解决方案和变通方法可以解决这个问题。
解决方案 #1:更改生成策略
据我所知,SEQUENCE
策略可能没问题,但老实说我还没有检查过。就是强烈的感悟。
解决方案 #2:使用合并而不是持久化
使用 merge()
持久化实体的智能解决方案。关键是当 merge()
调用一个新的实体实例时,然后创建一个新的托管实体实例,并将原始实体的状态复制到同一实体的托管实例。很容易执行 SQL SELECT 语句以从数据库中检索托管实体。
所以只需更换
JpaOperations.getEntityManager().persist(b);
// and
JpaOperations.getEntityManager().persist(a);
来自
JpaOperations.getEntityManager().merge(b);
// and
JpaOperations.getEntityManager().merge(a);
如您所见,Hibernate 从数据库中检索了之前保存(合并)EntityA
的以下日志条目。
2020-12-28 23:44:09,822 TRACE [org.hib.eng.spi.IdentifierValue] (executor-thread-1) ID unsaved-value: 0
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.EntityState] (executor-thread-1) Transient instance of: io.github.zforgo.Whosebug.quarkus.model.EntityB
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultMergeEventListener] (executor-thread-1) Merging transient instance
2020-12-28 23:44:09,822 TRACE [org.hib.eng.spi.IdentifierValue] (executor-thread-1) ID unsaved-value: 0
2020-12-28 23:44:09,822 TRACE [org.hib.eng.spi.IdentifierValue] (executor-thread-1) ID unsaved-value: 0
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultLoadEventListener] (executor-thread-1) Loading entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultLoadEventListener] (executor-thread-1) Attempting to resolve: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultLoadEventListener] (executor-thread-1) Object not resolved in any cache: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 TRACE [org.hib.per.ent.AbstractEntityPersister] (executor-thread-1) Fetching entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 DEBUG [org.hib.SQL] (executor-thread-1) select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=?
2020-12-28 23:44:09,823 TRACE [org.hib.res.jdb.int.ResourceRegistryStandardImpl] (executor-thread-1) Registering statement [wrapped[ prep7: select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=? ]]
2020-12-28 23:44:09,823 TRACE [org.hib.eng.jdb.int.JdbcCoordinatorImpl] (executor-thread-1) Registering last query statement [wrapped[ prep7: select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=? ]]
2020-12-28 23:44:09,823 TRACE [org.hib.typ.des.sql.BasicBinder] (executor-thread-1) binding parameter [1] as [BIGINT] - [4]
2020-12-28 23:44:09,823 TRACE [org.hib.loa.pla.exe.int.AbstractLoadPlanBasedLoader] (executor-thread-1) Bound [2] parameters total
使用 persist()
时缺少的最相关部分是:
Loading entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
Attempting to resolve: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
Object not resolved in any cache: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
Fetching entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=?
解决方案 #3 使用分离的服务和不同的事务
虽然我更喜欢这个,但它最需要努力。原始代码根本不符合 SOLID 原则。这个想法是
- 使用
@Transactional(Transactional.TxType.REQUIRES_NEW)
注释创建另一个具有新事务边界的 bean
- 将事件处理程序移动到单独的事件侦听器中class 因为 JPA 2.2 事件侦听器支持 CDI 注入
- 在侦听器中 class 注入该服务 bean 并调用所需的方法。
但是由于这个原因open bug我无法检查它。
我将 Quarkus 与 Hibernate 和 Resteasy 结合使用。
持久化实体时,我想自动创建引用第一个实体的第二个实体。
我尝试使用 @PostPersist
注释方法,但失败并出现 SQL 异常。当手动调用该方法时,它会起作用。
MWE:
CREATE TABLE "entity_a" (
"id" IDENTITY
);
CREATE TABLE "entity_b" (
"id" IDENTITY,
"entity_a_id" BIGINT NOT NULL,
FOREIGN KEY ("entity_a_id") REFERENCES "entity_a"("id"),
);
@Entity
public class EntityA {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
// getter, setter
@PostPersist
public void postPersist() {
EntityB b = new EntityB();
b.setEntityA(this);
JpaOperations.getEntityManager().persist(b);
}
}
@Entity
public class EntityB {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@OneToOne
private EntityA entityA;
// getters, setters
}
// in the rest resource class
@POST
@Transactional
public void create() {
EntityA a = new EntityA();
JpaOperations.getEntityManager().persist(a);
}
调用操作时,出现以下错误:
org.h2.jdbc.JdbcSQLException: NULL not allowed for column "entity_a_id"
NULL not allowed for column "entity_a_id"; SQL statement:
insert into "entity_b" ("id", "entity_a_id") values (null, ?) [23502-197]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at org.h2.message.DbException.get(DbException.java:179)
at org.h2.message.DbException.get(DbException.java:155)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:374)
at org.h2.table.Table.validateConvertUpdateSequence(Table.java:798)
at org.h2.command.dml.Insert.insertRows(Insert.java:177)
at org.h2.command.dml.Insert.update(Insert.java:134)
at org.h2.command.CommandContainer.update(CommandContainer.java:102)
at org.h2.command.Command.executeUpdate(Command.java:261)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:199)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:153)
at io.agroal.pool.wrapper.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:86)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:45)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3200)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3806)
at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:84)
at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:645)
at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:282)
at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:263)
at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:317)
at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:330)
at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:287)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:193)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:123)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:185)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:128)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:720)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:706)
at io.quarkus.hibernate.orm.runtime.entitymanager.TransactionScopedEntityManager.persist(TransactionScopedEntityManager.java:116)
at io.quarkus.hibernate.orm.runtime.entitymanager.ForwardingEntityManager.persist(ForwardingEntityManager.java:27)
at example.EntityA.postPersist(EntityA.java:24)
...
当我删除 @PostPersist
注释并从剩余资源(getEntityManager().persist
之后)调用该方法时,它按预期工作。
当我在 postPersist
中记录 id 值时,它已被设置。为什么会失败,我该如何解决?
虽然这很奇怪但是是正确的行为。基本上 entityManager
不必在调用 persist()
时立即生成身份。这取决于所选的 IdentifierGenerator 的策略。
但是IDENTITY
表示持久性提供者必须使用数据库标识列为实体分配主键,它只保证在事务提交或调用flush()时分配。
有很多解决方案和变通方法可以解决这个问题。
解决方案 #1:更改生成策略
据我所知,SEQUENCE
策略可能没问题,但老实说我还没有检查过。就是强烈的感悟。
解决方案 #2:使用合并而不是持久化
使用 merge()
持久化实体的智能解决方案。关键是当 merge()
调用一个新的实体实例时,然后创建一个新的托管实体实例,并将原始实体的状态复制到同一实体的托管实例。很容易执行 SQL SELECT 语句以从数据库中检索托管实体。
所以只需更换
JpaOperations.getEntityManager().persist(b);
// and
JpaOperations.getEntityManager().persist(a);
来自
JpaOperations.getEntityManager().merge(b);
// and
JpaOperations.getEntityManager().merge(a);
如您所见,Hibernate 从数据库中检索了之前保存(合并)EntityA
的以下日志条目。
2020-12-28 23:44:09,822 TRACE [org.hib.eng.spi.IdentifierValue] (executor-thread-1) ID unsaved-value: 0
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.EntityState] (executor-thread-1) Transient instance of: io.github.zforgo.Whosebug.quarkus.model.EntityB
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultMergeEventListener] (executor-thread-1) Merging transient instance
2020-12-28 23:44:09,822 TRACE [org.hib.eng.spi.IdentifierValue] (executor-thread-1) ID unsaved-value: 0
2020-12-28 23:44:09,822 TRACE [org.hib.eng.spi.IdentifierValue] (executor-thread-1) ID unsaved-value: 0
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultLoadEventListener] (executor-thread-1) Loading entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultLoadEventListener] (executor-thread-1) Attempting to resolve: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 TRACE [org.hib.eve.int.DefaultLoadEventListener] (executor-thread-1) Object not resolved in any cache: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 TRACE [org.hib.per.ent.AbstractEntityPersister] (executor-thread-1) Fetching entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
2020-12-28 23:44:09,822 DEBUG [org.hib.SQL] (executor-thread-1) select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=?
2020-12-28 23:44:09,823 TRACE [org.hib.res.jdb.int.ResourceRegistryStandardImpl] (executor-thread-1) Registering statement [wrapped[ prep7: select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=? ]]
2020-12-28 23:44:09,823 TRACE [org.hib.eng.jdb.int.JdbcCoordinatorImpl] (executor-thread-1) Registering last query statement [wrapped[ prep7: select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=? ]]
2020-12-28 23:44:09,823 TRACE [org.hib.typ.des.sql.BasicBinder] (executor-thread-1) binding parameter [1] as [BIGINT] - [4]
2020-12-28 23:44:09,823 TRACE [org.hib.loa.pla.exe.int.AbstractLoadPlanBasedLoader] (executor-thread-1) Bound [2] parameters total
使用 persist()
时缺少的最相关部分是:
Loading entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
Attempting to resolve: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
Object not resolved in any cache: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
Fetching entity: [io.github.zforgo.Whosebug.quarkus.model.EntityA#4]
select entitya0_.id as id1_0_0_ from EntityA entitya0_ where entitya0_.id=?
解决方案 #3 使用分离的服务和不同的事务
虽然我更喜欢这个,但它最需要努力。原始代码根本不符合 SOLID 原则。这个想法是
- 使用
@Transactional(Transactional.TxType.REQUIRES_NEW)
注释创建另一个具有新事务边界的 bean - 将事件处理程序移动到单独的事件侦听器中class 因为 JPA 2.2 事件侦听器支持 CDI 注入
- 在侦听器中 class 注入该服务 bean 并调用所需的方法。
但是由于这个原因open bug我无法检查它。