Java 使用Hibernate基于唯一密钥查找或插入
我正在尝试编写一个方法,该方法将基于唯一但非主键返回Hibernate对象。如果实体已经存在于数据库中,我想返回它,但如果它不存在,我想创建一个新实例并在返回之前保存它 更新:让我澄清一下,我写这篇文章的应用程序基本上是一个输入文件的批处理程序。系统需要逐行读取文件并将记录插入数据库。文件格式基本上是模式中多个表的非规范化视图,因此我要做的是解析出父记录,或者将其插入数据库中,以便获得新的合成键,或者如果它已经存在,则选择它。然后,我可以在其他表中添加额外的关联记录,这些表中有外键返回到该记录 这变得棘手的原因是,每个文件都需要完全导入或根本不导入,即为给定文件所做的所有插入和更新都应该是一个事务的一部分。如果只有一个进程执行所有导入,那么这就很容易了,但是如果可能的话,我想在多个服务器之间进行分解。由于这些限制,我需要能够呆在一个事务中,但处理已经存在记录的异常 父记录的映射类如下所示:Java 使用Hibernate基于唯一密钥查找或插入,java,hibernate,unique-constraint,upsert,unique-key,Java,Hibernate,Unique Constraint,Upsert,Unique Key,我正在尝试编写一个方法,该方法将基于唯一但非主键返回Hibernate对象。如果实体已经存在于数据库中,我想返回它,但如果它不存在,我想创建一个新实例并在返回之前保存它 更新:让我澄清一下,我写这篇文章的应用程序基本上是一个输入文件的批处理程序。系统需要逐行读取文件并将记录插入数据库。文件格式基本上是模式中多个表的非规范化视图,因此我要做的是解析出父记录,或者将其插入数据库中,以便获得新的合成键,或者如果它已经存在,则选择它。然后,我可以在其他表中添加额外的关联记录,这些表中有外键返回到该记录
@Entity
public class Foo {
@Id
@GeneratedValue(strategy = IDENTITY)
private int id;
@Column(unique = true)
private String name;
...
}
我编写此方法的初始尝试如下所示:
public Foo findOrCreate(String name) {
Foo foo = new Foo();
foo.setName(name);
try {
session.save(foo)
} catch(ConstraintViolationException e) {
foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
}
return foo;
}
问题是,当我要查找的名称存在时,调用uniqueResult()会引发org.hibernate.AssertionFailure异常。完整堆栈跟踪如下所示:
org.hibernate.AssertionFailure: null id in com.searchdex.linktracer.domain.LinkingPage entry (don't flush the Session after an exception occurs)
at org.hibernate.event.def.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:82) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:190) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:147) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:219) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:1185) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1709) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:347) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
at org.hibernate.impl.CriteriaImpl.uniqueResult(CriteriaImpl.java:369) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
有人知道引发此异常的原因吗?hibernate是否支持更好的方法来实现这一点
让我先解释一下为什么我要先插入,然后选择是否失败以及何时失败。这需要在分布式环境中工作,因此我无法在整个检查过程中进行同步,以查看记录是否已经存在以及插入。最简单的方法是让数据库通过检查每次插入时的约束冲突来处理此同步。也许您应该更改策略: 首先找到名为的用户,仅当该用户不存在时,才创建它。状态为所有HibernateException都是不可恢复的,并且当前事务必须在遇到时立即回滚。这就解释了为什么上面的代码不起作用。最终,如果不退出事务并关闭会话,就永远不会捕获HibernateException
实现这一点的唯一真正方法似乎是在方法本身中管理旧会话的关闭和新会话的重新打开。根据我的发现,使用Hibernate实现一个可以参与现有事务并在分布式环境中安全的findOrCreate方法似乎是不可能的。好吧,这里有一种方法可以做到这一点,但它并不适用于所有情况
- 在Foo中,删除
上的“unique=true”属性。添加在每次插入时更新的时间戳李>name
- 在
中,不必费心检查具有给定名称的实体是否已经存在,只需每次插入一个新实体即可findOrCreate()
- 当通过
查找Foo实例时,可能有0个或更多具有给定名称的实例,因此您只需选择最新的实例name
这个方法的优点是它不需要任何锁定,所以一切都应该运行得非常快。不利的一面是,您的数据库将充斥着过时的记录,因此您可能不得不在其他地方做一些事情来处理它们。另外,如果其他表通过其
id
引用Foo,那么这将破坏这些关系。想到两种解决方案:
这就是表锁的用途
Hibernate不支持表锁,但当它们方便使用时就是这种情况。幸运的是,您可以通过会话使用本机SQL。createSQLQuery()
。例如(在MySQL上):
这样,当会话(客户端连接)获得锁时,所有其他连接都将被阻止,直到操作结束并释放锁。读操作也会被其他连接阻塞,所以不用说,只在原子操作的情况下使用它
Hibernate的锁呢?
Hibernate使用行级锁定。我们不能直接使用它,因为我们不能锁定不存在的行。但是我们可以用一条记录创建一个虚拟表,将其映射到ORM,然后使用SELECT。。。对于更新,请在该对象上设置锁以同步我们的客户端。基本上,我们只需要确保在我们工作时没有其他客户端(运行相同的软件,使用相同的约定)执行任何冲突操作
// begin transaction
Transaction transaction = session.beginTransaction();
// blocks until any other client holds the lock
session.load("dummy", 1, LockOptions.UPGRADE);
// virtual safe zone
Foo foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
if (foo == null) {
foo = new Foo();
foo.setName(name)
session.save(foo);
}
// ends transaction (releasing locks)
transaction.commit();
您的数据库必须知道选择。。。对于更新
语法(Hibernate将使用它),当然,这只在所有客户端都具有相同约定(它们需要锁定相同的虚拟实体)的情况下才有效。一些人提到了总体策略的不同部分。假设您通常希望找到现有对象的频率高于创建新对象的频率:
- 按名称搜索现有对象。如果找到,请返回
- 启动嵌套(单独)事务
- 尝试插入新对象
- 提交嵌套事务
- 捕获嵌套事务中的任何失败,如果不违反约束,则重新抛出
- 否则,按名称搜索现有对象并返回它
只是澄清一下,正如另一个答案所指出的,“嵌套”事务实际上是一个单独的事务(许多数据库甚至不支持真正的嵌套事务)。解决方案实际上非常简单。首先使用您的名称值执行选择。如果找到结果,则返回该结果。如果没有,创建一个新的。如果创建失败(例外情况为
// begin transaction
Transaction transaction = session.beginTransaction();
// blocks until any other client holds the lock
session.load("dummy", 1, LockOptions.UPGRADE);
// virtual safe zone
Foo foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
if (foo == null) {
foo = new Foo();
foo.setName(name)
session.save(foo);
}
// ends transaction (releasing locks)
transaction.commit();
public T findOrCreate(final T t) throws InvalidRecordException {
// 1) look for the record
T found = findUnique(t);
if (found != null)
return found;
// 2) if not found, start a new, independent transaction
TransactionTemplate tt = new TransactionTemplate((PlatformTransactionManager)
transactionManager);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
try {
found = (T)tt.execute(new TransactionCallback<T>() {
try {
// 3) store the record in this new transaction
return store(t);
} catch (ConstraintViolationException e) {
// another thread or process created this already, possibly
// between 1) and 2)
status.setRollbackOnly();
return null;
}
});
// 4) if we failed to create the record in the second transaction, found will
// still be null; however, this would happy only if another process
// created the record. let's see what they made for us!
if (found == null)
found = findUnique(t);
} catch (...) {
// handle exceptions
}
return found;
}
private PostDetailsRecord upsertPostDetails(
DSLContext sql, Long id, String owner, Timestamp timestamp) {
sql
.insertInto(POST_DETAILS)
.columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON)
.values(id, owner, timestamp)
.onDuplicateKeyIgnore()
.execute();
return sql.selectFrom(POST_DETAILS)
.where(field(POST_DETAILS.ID).eq(id))
.fetchOne();
}
PostDetailsRecord postDetailsRecord = upsertPostDetails(
sql,
1L,
"Alice",
Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))
);
INSERT INTO "post_details" ("id", "created_by", "created_on")
VALUES (1, 'Alice', CAST('2016-08-11 12:56:01.831' AS timestamp))
ON CONFLICT DO NOTHING;
SELECT "public"."post_details"."id",
"public"."post_details"."created_by",
"public"."post_details"."created_on",
"public"."post_details"."updated_by",
"public"."post_details"."updated_on"
FROM "public"."post_details"
WHERE "public"."post_details"."id" = 1