使用休眠功能根据唯一键查找或插入

我正在尝试编写一个方法,该方法将基于唯一但非主键返回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]

有谁知道是什么原因导致引发此异常?休眠是否支持实现此目的的更好方法?

让我先发制人地解释为什么我首先插入,然后选择是否以及何时失败。这需要在分布式环境中工作,因此我无法在检查中同步以查看记录是否已存在和插入。执行此操作的最简单方法是让数据库通过检查每次插入时的约束冲突来处理此同步。


答案 1

我有类似的批处理要求,进程在多个JVM上运行。为此,我采取了如下方法。这很像jtahlborn的建议。但是,正如 vbence 所指出的,如果使用 NESTED 事务,则当您遇到约束冲突异常时,会话将失效。相反,我使用REQUIRES_NEW,它暂停当前事务并创建一个新的独立事务。如果新事务回滚,则不会影响原始事务。

我正在使用Spring的TranscourseTemplate,但我相信如果你不想依赖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;
}

答案 2

您需要使用 或 E 来实现此目标。UPSERTMERG

但是,Hibernate 不支持此构造,因此您需要改用 jOOQ

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();
}

在 PostgreSQL 上调用此方法:

PostDetailsRecord postDetailsRecord = upsertPostDetails(
    sql, 
    1L, 
    "Alice",
    Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))
);

生成以下 SQL 语句:

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

在Oracle和SQL Server上,jOOQ将使用,而在MySQL上它将使用.MERGEON DUPLICATE KEY

并发机制由插入、更新或删除记录时采用的行级别锁定机制来确保,您可以在下图中查看该机制:

enter image description here

代码在 GitHub 上可用。


推荐