我的JPA / Hibernate应用程序中有几个Map对象。在网络上,我接收到的数据包代表了这些对象的更新,或者实际上可能代表了整个新对象。
我想写一个类似于
<T> T getOrCreate(Class<T> klass, Object primaryKey)
如果数据库中存在一个具有pk primaryKey的类,则返回所提供类的对象,否则创建该类的新对象,持久化并返回它。
接下来我将对该对象做的事情是在一个事务中更新它的所有字段。
在JPA中是否有一种惯用的方法来完成此操作,或者是否有更好的方法来解决我的问题?
6条答案
按热度按时间uoifb46i1#
我想写一个类似
<T> T getOrCreate(Class<T> klass, Object primaryKey)
的方法这可不容易
一种简单的方法是这样做(假设该方法在事务内部运行):
但在并发环境中,这段代码可能会因为某些竞争条件而失败:
如果您正在运行多个JVM,同步就没有用了。如果不获取一个表锁(这是非常可怕的),我真的不知道如何解决这个问题。
在这种情况下,我想知道是否最好先系统地插入并处理可能的异常以执行后续的选择(在新事务中)。
您可能应该添加一些有关上述约束的细节(多线程?分布式环境?).
xmjla07d2#
使用纯JPA可以在带有嵌套实体管理器的多线程解决方案中乐观地解决这个问题(实际上我们只需要嵌套事务,但我认为这是纯JPA不可能的)。本质上,需要创建一个封装查找或创建操作的微事务。这种性能不会很好,也不适合大型批量创建,但对于大多数情况来说应该足够了。
先决条件:
finder
factory
。代码:
fdx2calv3#
我必须指出@gus an的回答中有一些缺陷。在并发情况下,它可能会导致明显的问题。如果有2个线程阅读计数,它们都将得到0,然后执行插入。因此创建了重复行。
我的建议是像下面这样编写您的原生查询:
它就像是程序中的乐观锁。但是我们让数据库引擎来决定和找到重复的由您自定义的属性。
jm81lzqq4#
如何在
findByKeyword
之后使用orElse
函数?如果没有找到记录,可以返回一个新示例。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持久化,但除此之外,它是最干净(通常也是最快)的解决方案。
如果这不可行,“插入并捕获”方法也可以工作-但它也有丑陋的代码(异常处理),并且再次,您必须确保始终应用此异常处理。
在某些情况下使用锁很有帮助,但我会把它作为最后的手段。
备注
hs1ihplo6#
即使在并发环境中,也有一个简单的解决方案来解决这个问题。
在liquibase表的创建过程中,使用乐观锁定对实体进行
@Version private Long version;
和<column name="version" type="BIGINT"/>
操作。如果您尝试保存一个已经存在的(复合)p_pkey的新实体,则会向JpaRepository抛出DataIntegrityViolationException。因此,无需担心服务层中的并发锁定--数据库将知道实体是否存在。