Mik*_*eck 19 java hibernate upsert unique-key unique-constraint
我正在尝试编写一个方法,它将返回一个基于唯一但非主键的Hibernate对象.如果实体已存在于数据库中,我想返回它,但如果不存在,我想创建一个新实例并在返回之前保存它.
更新:让我澄清一下,我正在编写的应用程序基本上是输入文件的批处理器.系统需要逐行读取文件并将记录插入数据库.文件格式基本上是我们模式中几个表的非规范化视图,所以我要做的是解析父记录,或者将其插入到db中,这样我就可以得到一个新的合成密钥,或者如果它已经存在则选择它.然后我可以在其他表中添加其他关联记录,这些表具有返回该记录的外键.
这很棘手的原因是每个文件需要完全导入或根本不导入,即为给定文件完成的所有插入和更新应该是一个事务的一部分.如果只有一个进程正在执行所有导入,这很容易,但是如果可能的话,我想在多个服务器上解决这个问题.由于这些约束,我需要能够保留在一个事务中,但是处理已经存在记录的异常.
父记录的映射类如下所示:
@Entity
public class Foo {
@Id
@GeneratedValue(strategy = IDENTITY)
private int id;
@Column(unique = true)
private String name;
...
}
Run Code Online (Sandbox Code Playgroud)
我最初尝试编写此方法的方法如下:
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;
}
Run Code Online (Sandbox Code Playgroud)
问题是当我正在寻找的名称存在时,调用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]
Run Code Online (Sandbox Code Playgroud)
有谁知道导致这个异常的原因是什么?hibernate是否支持更好的方法来实现这一目标?
让我先解释一下为什么我先插入然后选择是否以及何时失败.这需要在分布式环境中工作,因此我无法在检查中同步以查看记录是否已存在以及插入.最简单的方法是让数据库通过检查每个插入的约束违规来处理此同步.
Law*_*pin 11
我有一个类似的批处理要求,进程在多个JVM上运行.我采取的方法如下.这非常像jtahlborn的建议.但是,正如vbence所指出的,如果您使用NESTED事务,当您获得约束违规异常时,您的会话将失效.相反,我使用REQUIRES_NEW,它暂停当前事务并创建一个新的独立事务.如果新事务回滚,则不会影响原始事务.
我正在使用Spring的TransactionTemplate,但我相信如果你不想依赖Spring,你可以轻松地翻译它.
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;
}
Run Code Online (Sandbox Code Playgroud)
想到两个解决方案:
Hibernate不支持表锁,但是当它们派上用场时就是这种情况.幸运的是,您可以使用本机SQL Session.createSQLQuery()
.例如(在MySQL上):
// no access to the table for any other clients
session.createSQLQuery("LOCK TABLES foo WRITE").executeUpdate();
// 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);
}
// releasing locks
session.createSQLQuery("UNLOCK TABLES").executeUpdate();
Run Code Online (Sandbox Code Playgroud)
这样,当会话(客户端连接)获得锁定时,所有其他连接都将被阻止,直到操作结束并释放锁定.读取操作也被阻止用于其他连接,因此不用说仅在原子操作的情况下使用它.
Hibernate使用行级锁定.我们不能直接使用它,因为我们无法锁定不存在的行.但是我们可以创建一个包含单个记录的虚拟表,将其映射到ORM,然后SELECT ... FOR UPDATE
在该对象上使用样式锁来同步我们的客户端.基本上我们只需要确保在我们工作的时候没有其他客户端(运行相同的软件,具有相同的约定)将执行任何冲突的操作.
// 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();
Run Code Online (Sandbox Code Playgroud)
您的数据库必须知道SELECT ... FOR UPDATE
语法(Hibernate是goig使用它),当然这仅在所有客户端具有相同约定(它们需要锁定相同的虚拟实体)时才有效.
This is a great question, so I decided to write an article to explain it in more detail.
As I explained in this free chapter of my book, High-Performance Java Persistence, you need to use UPSERT or MERGE to achieve this goal.
However, Hibernate does not offer support for this construct, so you need to use jOOQ instead.
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();
}
Run Code Online (Sandbox Code Playgroud)
Calling this method on PostgreSQL:
PostDetailsRecord postDetailsRecord = upsertPostDetails(
sql,
1L,
"Alice",
Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))
);
Run Code Online (Sandbox Code Playgroud)
Yields the following SQL statements:
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
Run Code Online (Sandbox Code Playgroud)
On Oracle and SQL Server, jOOQ will use MERGE
while on MySQL it will use ON DUPLICATE KEY
.
The concurrency mechanism is ensured by the row-level locking mechanism emplyed when inserting, updating or deletinga record,w hich you can view in the following diagram:
Code avilable on GitHub.
归档时间: |
|
查看次数: |
15100 次 |
最近记录: |