JPA – 如果不存在创建实体?

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

我想写一个像这样的方法

 T getOrCreate(Class klass, Object primaryKey) 

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

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

在JPA中有没有惯用的方法,或者有更好的方法来解决我的问题?

  1. 创建一个EntityManager实例(让我们称之为“em”),除非您已经有一个活动实例
  2. 创建一个新的交易(让我们称之为“tx”)
  3. 调用em.find(Object pk)
  4. 调用tx.begin()
    • 如果find()返回非null实体引用,则需要进行更新。 将更改应用于返回的实体,然后调用em.merge(对象实体)。
    • 如果find()返回一个空引用,那么该数据库中不存在该PK。 创建一个新实体,然后调用em.persist(Object newEntity)。
  5. 打电话给em.flush()
  6. 致电tx.commit()
  7. 根据您的方法签名返回您的实体引用。

我想写一个像 T getOrCreate(Class klass, Object primaryKey)

这并不容易。

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

 public  T findOrCreate(Class 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;  //返回null
 T2:SELECT w / id = 123;  //返回null

 T1:INSERT w / id = 123;
 T1:COMMIT;  //插入行

 T2:INSERT w / name = 123;
 T2:COMMIT;  //约束违规

如果您运行多个JVM,同步将无济于事。 并且没有获得表锁(这非常可怕),我真的没有看到你如何解决这个问题。

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

您应该添加一些有关上述约束的细节(multithreading?分布式环境?)。

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

先决条件:

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

码:

 public  T findOrCreate(Supplier finder, Supplier 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(); }