21

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

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

4

9 回答 9

12

我有类似的批处理要求,进程在多个 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;
}
于 2011-04-28T17:20:23.780 回答
11

你需要使用UPSERTor 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.

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

在此处输入图像描述

代码在GitHub上可用。

于 2017-11-03T11:51:59.053 回答
8

想到两个解决方案:

这就是 TABLE LOCKS 的用途

Hibernate 不支持表锁,但这是它们派上用场的情况。幸运的是,您可以通过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();

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

Hibernate 的锁呢?

Hibernate 使用行级锁定。我们不能直接使用它,因为我们不能锁定不存在的行。但是我们可以创建一个带有单个记录的虚拟SELECT ... FOR UPDATE表,将其映射到 ORM,然后在该对象上使用样式锁来同步我们的客户端。基本上我们只需要确保没有其他客户端(运行相同的软件,具有相同的约定)在我们工作时会执行任何冲突的操作。

// 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 是 goig 使用它),当然,这只有在您的所有客户端都具有相同的约定(他们需要锁定相同的虚拟实体)时才有效。

于 2011-04-27T23:53:54.527 回答
2

关于事务和异常的Hibernate 文档指出,所有 HibernateExceptions 都是不可恢复的,并且一旦遇到当前事务就必须回滚。这解释了为什么上面的代码不起作用。最终,您不应该在不退出事务并关闭会话的情况下捕获 HibernateException。

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

于 2011-02-16T23:41:40.000 回答
2

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

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

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

于 2011-04-28T00:20:40.103 回答
2

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

您可以在此处查看使用休眠的乐观和悲观锁定策略的解释:http: //docs.jboss.org/hibernate/core/3.3/reference/en/html/transactions.html

于 2011-04-28T09:15:39.407 回答
1

好吧,这是一种方法——但它并不适用于所有情况。

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

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

于 2011-02-16T23:42:11.620 回答
0

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

于 2011-02-16T23:06:03.400 回答
0

我会尝试以下策略:

一个。开始一个主要交易(在时间 1)
B。开始一个子交易(在时间 2)

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

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

只返回对象主键,然后使用 EntityManager.getReference(..) 获取您将在主事务中使用的对象。或者,在 D 之后启动主事务;我并不完全清楚你的主要交易中有多少竞争条件,但以上应该允许在“大”交易中使用 n 倍 BCD。

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

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

编辑:仔细检查 getReference(..) 是否像我认为的那样工作。

于 2011-05-02T01:05:41.547 回答