使用Hibernate基于唯一键查找或插入

问题描述 投票:17回答:9

我正在尝试编写一个方法,它将返回一个基于唯一但非主键的Hibernate对象。如果实体已存在于数据库中,我想返回它,但如果不存在,我想创建一个新实例并在返回之前保存它。

更新:让我澄清一下,我正在编写的应用程序基本上是输入文件的批处理器。系统需要逐行读取文件并将记录插入数据库。文件格式基本上是我们模式中几个表的非规范化视图,所以我要做的是解析父记录,或者将其插入到db中,这样我就可以得到一个新的合成密钥,或者如果它已经存在则选择它。然后我可以在其他表中添加其他关联记录,这些表具有返回该记录的外键。

这很棘手的原因是每个文件需要完全导入或根本不导入,即为给定文件完成的所有插入和更新应该是一个事务的一部分。如果只有一个进程正在执行所有导入,这很容易,但是如果可能的话,我想在多个服务器上解决这个问题。由于这些约束,我需要能够保持在一个事务中,但处理记录已存在的异常。

父记录的映射类如下所示:

@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是否支持更好的方法来实现这一目标?

让我先解释一下为什么我先插入然后选择是否以及何时失败。这需要在分布式环境中工作,因此我无法在检查中同步以查看记录是否已存在以及插入。最简单的方法是让数据库通过检查每个插入的约束违规来处理此同步。

java hibernate unique-constraint upsert unique-key
9个回答
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;
}

8
投票

想到两个解决方案:

That's what TABLE LOCKS are for

Hibernate不支持表锁,但是当它们派上用场时就是这种情况。幸运的是,您可以通过Session.createSQLQuery()使用本机SQL。例如(在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();

这样,当会话(客户端连接)获得锁定时,所有其他连接都将被阻止,直到操作结束并释放锁定。读取操作也被阻止用于其他连接,因此不用说仅在原子操作的情况下使用它。

What about Hibernate's locks?

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

你的数据库必须知道qazxsw poi语法(Hibernate将使用它),当然这仅适用于所有客户端都具有相同的约定(它们需要锁定相同的虚拟实体)。


5
投票

这是一个很好的问题,所以我决定写SELECT ... FOR UPDATE

正如我在an article to explain it in more detail,高性能Java持久性中所解释的那样,您需要使用UPSERT或MERGE来实现此目标。

但是,Hibernate不支持此构造,因此您需要使用free chapter of my book

jOOQ

在PostgreSQL上调用此方法:

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

产生以下SQL语句:

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

在Oracle和SQL Server上,jOOQ将使用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 而在MySQL上它将使用MERGE

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

ON DUPLICATE KEY

密码可在qazxsw poi上找到。


2
投票

enter image description here声明所有HibernateExceptions都是不可恢复的,并且当遇到一个时,必须立即回滚当前事务。这解释了为什么上面的代码不起作用。最终,您不应该在不退出事务和关闭会话的情况下捕获HibernateException。

实现这一目标的唯一真正方法似乎是管理旧会话的结束并在方法本身内重新打开新会话。实现一个可以参与现有事务并且在分布式环境中安全的findOrCreate方法似乎不可能使用基于我发现的Hibernate。


2
投票

解决方案实际上非常简单。首先使用您的名称值执行选择。如果找到结果,则返回该结果。如果没有,请创建一个新的。如果创建失败(有异常),这是因为另一个客户端在select和insert语句之间添加了这个非常相同的值。这是合乎逻辑的,你有一个例外。抓住它,回滚您的事务并再次运行相同的代码。因为该行已存在,所以select语句将找到它并且您将返回您的对象。

你可以在这里看到对hibernate的乐观和悲观锁定策略的解释:GitHub


2
投票

有几个人提到了整体战略的不同部分。假设您通常希望比创建新对象更频繁地找到现有对象:

  • 按名称搜索现有对象。如果找到,返回
  • 开始嵌套(单独)事务 尝试插入新对象 提交嵌套事务
  • 从嵌套事务中捕获任何失败,如果除了约束违规之外什么都没有,重新抛出
  • 否则按名称搜索现有对象并返回

只是为了澄清,正如另一个答案所指出的,“嵌套”事务实际上是一个单独的事务(许多数据库甚至不支持真正的嵌套事务)。


1
投票

嗯,这是一种方法 - 但它并不适合所有情况。

这个方法的好处是它不需要任何锁定,所以一切都应该运行得非常快。缺点是您的数据库中会堆满过时的记录,因此您可能需要在其他地方执行某些操作来处理它们。此外,如果其他表通过其findOrCreate()引用Foo,那么这将搞砸这些关系。


0
投票

也许你应该改变你的策略:首先找到具有名称的用户,并且只有当用户不存在时才创建它。


0
投票

我会尝试以下策略:

A.开始主要交易(在时间1) B.开始子交易(在时间2)

现在,在时间1之后创建的任何对象在主事务中都不可见。所以,当你这样做

C.创建新的竞争条件对象,提交子事务 D.通过启动新的子事务(在时间3)处理冲突并从查询中获取对象(来自B点的子事务现在超出范围)来处理冲突。

只返回对象主键,然后使用EntityManager.getReference(..)获取将在主事务中使用的对象。或者,在D之后开始主要交易;我在你的主要交易中你将拥有多少种族条件并不完全清楚,但上述应该允许在“大型”交易中进行n次B-C-D。

请注意,您可能希望执行多线​​程(每个CPU一个线程),然后您可以通过对这些类型的冲突使用共享静态缓存来大大减少此问题 - 并且第2点可以保持“乐观”,即不执行a .find(..)first。

编辑:对于新事务,您需要使用事务类型name注释的EJB接口方法调用。

编辑:仔细检查getReference(..)是否正常工作。

© www.soinside.com 2019 - 2024. All rights reserved.