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

时间:2011-02-16 22:16:13

标签: java hibernate unique-constraint upsert unique-key

我正在尝试编写一个方法,该方法将基于唯一但非主键返回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是否支持更好的方法来实现这个目标?

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

9 个答案:

答案 0 :(得分: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;
}

答案 1 :(得分:8)

我想到了两个解决方案:

这就是用于

的TABLE LOCKS

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

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

Hibernate的锁怎么样?

Hibernate使用行级锁定。我们不能直接使用它,因为我们无法锁定不存在的行。但我们可以使用单个记录创建 dummy 表,将其映射到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();

您的数据库必须知道SELECT ... FOR UPDATE语法(Hibernate才能使用它),当然这仅适用于所有客户端都具有相同约定(它们需要锁定相同的虚拟实体)。 / p>

答案 2 :(得分:5)

这是一个很好的问题,所以我决定写an article to explain it in more detail

正如我在此free chapter of my book高性能Java持久性中所解释的那样,您需要使用UPSERT或MERGE来实现此目标。

但是,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将使用MERGE而在MySQL上它将使用ON DUPLICATE KEY

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

enter image description here

GitHub上的代码可用。

答案 3 :(得分:2)

Hibernate documentation on transactions and exceptions表示所有HibernateExceptions都是不可恢复的,并且遇到当前事务必须立即回滚。这解释了为什么上面的代码不起作用。最终,你不应该在不退出事务和关闭会话的情况下捕获HibernateException。

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

答案 4 :(得分:2)

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

  • 按名称搜索现有对象。如果找到,请返回
  • 启动嵌套(单独)事务
    • 尝试插入新对象
    • 提交嵌套事务
  • 从嵌套事务中捕获任何失败,如果有任何约束违规,则重新抛出
  • 否则按名称搜索现有对象并将其返回

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

答案 5 :(得分:2)

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

你可以在这里看到有关使用hibernate进行乐观和悲观锁定的策略的解释:http://docs.jboss.org/hibernate/core/3.3/reference/en/html/transactions.html

答案 6 :(得分:1)

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

  • 在Foo中,删除name上的“unique = true”属性。添加每个插入更新的时间戳。
  • findOrCreate()中,不要费心检查具有给定名称的实体是否已经存在 - 只需每次都插入一个新实体。
  • 当按name查找Foo实例时,可能有0个或更多具有给定名称的实例,因此您只需选择最新的实例。

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

答案 7 :(得分:0)

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

答案 8 :(得分:0)

我会尝试以下策略:

A 的。开始主要交易(在时间1)
的。开始子交易(在时间2)

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

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

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

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

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

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