JPA - 创建不存在的实体?

2022-09-01 12:05:27

我在JPA / Hibernate应用程序中有几个映射的对象。在网络上,我收到表示这些对象更新的数据包,或者实际上可能完全表示新对象的数据包。

我想写一个像这样的方法

<T> T getOrCreate(Class<T> klass, Object primaryKey)

如果数据库中存在 pk primaryKey 的对象,则返回所提供类的对象,否则创建该类的新对象,保留该对象并返回它。

我将对该对象执行的下一件事是在事务中更新其所有字段。

有没有一种惯用的方法可以在JPA中做到这一点,或者有没有更好的方法来解决我的问题?


答案 1

我想写一个像这样的方法<T> T getOrCreate(Class<T> klass, Object primaryKey)

这并不容易。

一种幼稚的方法是做这样的事情(假设该方法在事务中运行):

public <T> T findOrCreate(Class<T> entityClass, Object primaryKey) {
    T entity = em.find(entityClass, primaryKey);
    if ( entity != null ) {
        return entity;
    } else {
        try {
            entity = entityClass.newInstance();
            /* use more reflection to set the pk (probably need a base entity) */
            return entity;
        } catch ( Exception e ) {
            throw new RuntimeException(e);
        }
    }
}

但在并发环境中,此代码可能会由于某些争用条件而失败:

T1: BEGIN TX;
T2: BEGIN TX;

T1: SELECT w/ id = 123; //returns null
T2: SELECT w/ id = 123; //returns null

T1: INSERT w/ id = 123;
T1: COMMIT; //row inserted

T2: INSERT w/ name = 123;
T2: COMMIT; //constraint violation

而且,如果您正在运行多个 JVM,则同步将无济于事。如果不获得表锁(这非常可怕),我真的看不出你如何解决这个问题。

在这种情况下,我想知道是否最好先系统地插入并处理可能的异常以执行后续选择(在新事务中)。

您可能应该添加一些有关上述约束(多线程?分布式环境?)的详细信息。


答案 2

使用纯JPA,可以在具有嵌套实体管理器的多线程解决方案中乐观地解决这个问题(实际上我们只需要嵌套事务,但我认为纯JPA不可能)。从本质上讲,需要创建一个封装查找或创建操作的微事务。这种性能不会很棒,也不适合大批量创建,但在大多数情况下应该足够了。

先决条件:

  • 实体必须具有唯一的约束冲突,如果创建了两个实例,则该冲突将失败
  • 您有某种查找器来查找实体(可以通过EntityManager.find的主键或通过某些查询进行查找),我们将将其称为finder
  • 您有某种工厂方法来创建一个新实体,如果您要查找的实体不存在,我们将将其称为 。factory
  • 我假设给定的findOrCreate方法将存在于某个存储库对象上,并且在现有实体管理器和现有事务的上下文中调用它。
  • 如果事务隔离级别是可序列化的或快照的,这将不起作用。如果事务是可重复读取的,则您一定没有尝试读取当前事务中的实体。
  • 为了便于维护,我建议将下面的逻辑分解为多种方法。

法典:

public <T> T findOrCreate(Supplier<T> finder, Supplier<T> factory) {
    EntityManager innerEntityManager = entityManagerFactory.createEntityManager();
    innerEntityManager.getTransaction().begin();
    try {
        //Try the naive find-or-create in our inner entity manager
        if(finder.get() == null) {
            T newInstance = factory.get();
            innerEntityManager.persist(newInstance);
        }
        innerEntityManager.getTransaction().commit();
    } catch (PersistenceException ex) {
        //This may be a unique constraint violation or it could be some
        //other issue.  We will attempt to determine which it is by trying
        //to find the entity.  Either way, our attempt failed and we
        //roll back the tx.
        innerEntityManager.getTransaction().rollback();
        T entity = finder.get();
        if(entity == null) {
            //Must have been some other issue
            throw ex;
        } else {
            //Either it was a unique constraint violation or we don't
            //care because someone else has succeeded
            return entity;
        }
    } catch (Throwable t) {
        innerEntityManager.getTransaction().rollback();
        throw t;
    } finally {
        innerEntityManager.close();
    }
    //If we didn't hit an exception then we successfully created it
    //in the inner transaction.  We now need to find the entity in
    //our outer transaction.
    return finder.get();
}

推荐