java JPA -如果实体不存在就创建实体?

jdg4fx2g  于 2023-06-20  发布在  Java
关注(0)|答案(6)|浏览(190)

我的JPA / Hibernate应用程序中有几个Map对象。在网络上,我接收到的数据包代表了这些对象的更新,或者实际上可能代表了整个新对象。
我想写一个类似于

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

如果数据库中存在一个具有pk primaryKey的类,则返回所提供类的对象,否则创建该类的新对象,持久化并返回它。
接下来我将对该对象做的事情是在一个事务中更新它的所有字段。
在JPA中是否有一种惯用的方法来完成此操作,或者是否有更好的方法来解决我的问题?

uoifb46i

uoifb46i1#

我想写一个类似<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,同步就没有用了。如果不获取一个表锁(这是非常可怕的),我真的不知道如何解决这个问题。
在这种情况下,我想知道是否最好先系统地插入并处理可能的异常以执行后续的选择(在新事务中)。
您可能应该添加一些有关上述约束的细节(多线程?分布式环境?).

xmjla07d

xmjla07d2#

使用纯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();
}
fdx2calv

fdx2calv3#

我必须指出@gus an的回答中有一些缺陷。在并发情况下,它可能会导致明显的问题。如果有2个线程阅读计数,它们都将得到0,然后执行插入。因此创建了重复行。
我的建议是像下面这样编写您的原生查询:

insert into af_label (content,previous_level_id,interval_begin,interval_end) 
    select "test",32,9,13
    from dual 
    where not exists (select * from af_label where previous_level_id=32 and interval_begin=9 and interval_end=13)

它就像是程序中的乐观锁。但是我们让数据库引擎来决定和找到重复的由您自定义的属性。

jm81lzqq

jm81lzqq4#

如何在findByKeyword之后使用orElse函数?如果没有找到记录,可以返回一个新示例。

SearchCount searchCount = searchCountRepository.findByKeyword(keyword)
                .orElse(SearchCount.builder()
                        .keyword(keyword)
                        .count(0)
                        .build()) ;
7z5jn7bk

7z5jn7bk5#

  • 其他答案给予了解决问题的好方法。我将尝试总结各种方法,以给予一个良好的概述。

JPA支持

我想写一个方法[...],如果数据库中存在一个pk primaryKey的类,则返回所提供的类的对象,否则创建该类的新对象,持久化并返回它。
接下来我将对该对象做的事情是在一个事务中更新它的所有字段。
这是一种相当常见的情况,这种操作有一个名称:Upsert(UPDATE和INSERT的混合词)-表示如果记录不存在(由其键决定)则插入记录,如果存在则更新。
大多数关系数据库系统都内置了对这一点的支持,通过标准SQL关键字MERGE或其他一些关键字-请参阅the Wikipedia article for MERGE了解详细信息。这允许使用单个SQL语句执行upsert。
在JPA中是否有一种惯用的方法来完成此操作,或者是否有更好的方法来解决我的问题?
不幸的是:,JPA本身不支持upsert操作。在JPQL或JPA API中没有UPSERT或MERGE或类似的关键字。更确切地说:EntityManager.merge()将在单线程解决方案中执行您想要的操作(查找和更新实体或插入实体),但它不是线程安全的。
但是,有一些变通方法(一些在其他答案中解释)。

解决方法

Insert和catch约束冲突

确保要使用的关键字字段具有唯一索引。然后简单地使用EntityManager.persist()执行插入。如果记录不存在,则将插入该记录。如果记录已经存在,您将获得一个Exception,您可以捕获它。然后,您可以执行UPDATE(使用EntityManager.merge())。
这在佩斯的回答中有更详细的描述。
优点:不需要复杂的原生SQL。
缺点:异常处理将是非常讨厌的,因为JPA也没有一种可移植的方法来区分异常是由约束违反(在这里是可以的)还是由其他一些问题(死数据库,网络错误)引起的,你仍然想处理。

使用MERGE / UPSERT语句

您可以使用本机查询来执行MERGE或UPSERT语句,使用DB的内置支持来执行upsert。
优点:从DBMS的Angular 来看,这是最干净的解决方案,因为它使用了DBMS为这个问题提供的机制。缺点:需要一些讨厌的原生SQL;基本上就是“背着JPA”
详情请参见Using Merge statement for single table

使用DB锁

您还可以使用pessimistic locking的形式,通过获取某个记录上的数据库锁(使用EntityManager.lock())。要锁定的记录将是特定于应用程序的。通常,如果您有一个1:n关系,并且您正在插入到:n表中,您将锁定相应的“主”记录。例如,在向发票添加新项目时,您将锁定主发票记录。在获得锁之后,检查记录是否存在,并更新或插入它。
如果所有执行插入的代码都遵守这一点,那么锁定将确保一旦您获得了锁,其他进程/线程就无法干扰。
锁的获取和更新/插入必须放在事务中(事务结束时将自动释放锁)。
优点:不需要原生SQL。在某些情况下,可以比其他解决方案更快(尽管这取决于具体情况)。缺点:可能会降低性能,因为其他代码必须等待锁。特别地,DBMS可能决定锁定整个表而不是仅锁定一行,这将使情况变得更糟。如果锁定错误,可能会造成潜在的死锁。

推荐

根据我的经验,使用MERGE / UPSERT通常是最好的解决方案,因为它最佳地使用了DBMS提供的资源。所需的原生SQL有点难看,您必须确保不要意外地通过JPA持久化,但除此之外,它是最干净(通常也是最快)的解决方案。
如果这不可行,“插入并捕获”方法也可以工作-但它也有丑陋的代码(异常处理),并且再次,您必须确保始终应用此异常处理。
在某些情况下使用锁很有帮助,但我会把它作为最后的手段。

备注

  • EclipseLink(一个JPA实现)实际上有一个bug,它提供了对upsert -Bug 344329 - Add support for UPSERT/MERGE的支持。然而,这个bug是从2011年开始的,似乎什么都没有发生。而且,理想情况下,这应该被添加到JPA规范中,这似乎也没有发生。
  • 一个简单的解决方案是确保只有一个线程/进程在执行插入/更新-然后你可以简单地更新和插入。但这并不总是可行的:-)。
hs1ihplo

hs1ihplo6#

即使在并发环境中,也有一个简单的解决方案来解决这个问题。
在liquibase表的创建过程中,使用乐观锁定对实体进行@Version private Long version;<column name="version" type="BIGINT"/>操作。如果您尝试保存一个已经存在的(复合)p_pkey的新实体,则会向JpaRepository抛出DataIntegrityViolationException。因此,无需担心服务层中的并发锁定--数据库将知道实体是否存在。

相关问题