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

时间:2022-06-01 19:51:43

I have several mapped objects in my JPA / Hibernate application. On the network I receive packets that represent updates to these objects, or may in fact represent new objects entirely.

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

I'd like to write a method like

我想写一个像这样的方法

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

that returns an object of the provided class if one exists in the database with pk primaryKey, and otherwise creates a new object of that class, persists it and returns it.

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

The very next thing I'll do with the object will be to update all its fields, within a transaction.

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

Is there an idiomatic way to do this in JPA, or is there a better way to solve my problem?

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

3 个解决方案

#1


-2  

  1. Create an EntityManager instance (let's call it "em"), unless you already have an active one
  2. 创建一个EntityManager实例(让我们称之为“em”),除非您已经有一个活动实例
  3. Create a new transaction (let's call it "tx")
  4. 创建一个新的交易(让我们称之为“tx”)
  5. Call em.find(Object pk)
  6. 调用em.find(Object pk)
  7. Call tx.begin()
  8. 调用tx.begin()
    • If find() returned a non-null entity reference then you need to do an update. Apply your changes to the returned entity and then call em.merge(Object entity).
    • 如果find()返回非null实体引用,则需要进行更新。将更改应用于返回的实体,然后调用em.merge(对象实体)。
    • if find() returned a null reference, then that PK does not exist in the database. Create a new entity and then call em.persist(Object newEntity).
    • 如果find()返回一个空引用,那么该数据库中不存在该PK。创建一个新实体,然后调用em.persist(Object newEntity)。
  9. 如果find()返回非null实体引用,则需要进行更新。将更改应用于返回的实体,然后调用em.merge(对象实体)。如果find()返回一个空引用,那么该数据库中不存在该PK。创建一个新实体,然后调用em.persist(Object newEntity)。
  10. Call em.flush()
  11. 打电话给em.flush()
  12. Call tx.commit()
  13. 致电tx.commit()
  14. Return your entity reference, per your method signature.
  15. 根据您的方法签名返回您的实体引用。

#2


15  

I'd like to write a method like <T> T getOrCreate(Class<T> klass, Object primaryKey)

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

This won't be easy.

这并不容易。

A naive approach would be to do something like this (assuming the method is running inside a transaction):

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

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);
        }
    }
}

But in a concurrent environment, this code could fail due to some race condition:

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

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

And if you are running multiple JVMs, synchronization won't help. And without acquiring a table lock (which is pretty horrible), I don't really see how you could solve this.

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

In such case, I wonder if it wouldn't be better to systematically insert first and handle a possible exception to perform a subsequent select (in a new transaction).

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

You should probably add some details regarding the mentioned constraints (multi-threading? distributed environment?).

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

#3


4  

Using pure JPA one can solve this optimistically in a multi-threaded solution with nested entity managers (really we just need nested transactions but I don't think that is possible with pure JPA). Essentially one needs to create a micro-transaction that encapsulates the find-or-create operation. This performance won't be fantastic and isn't suitable for large batched creates but should be sufficient for most cases.

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

Prerequisites:

先决条件:

  • The entity must have a unique constraint violation that will fail if two instances are created
  • 如果创建了两个实例,则实体必须具有唯一的约束冲突,该冲突将失败
  • You have some kind of finder to find the entity (can find by primary key with EntityManager.find or by some query) we will refer to this as finder
  • 您有某种查找器来查找实体(可以通过主键使用EntityManager.find或通过某些查询找到)我们将此作为查找程序引用
  • You have some kind of factory method to create a new entity should the one you are looking for fail to exist, we will refer to this as factory.
  • 如果您要查找的实体不存在,您可以使用某种工厂方法来创建新实体,我们将其称为工厂。
  • I'm assuming that the given findOrCreate method would exist on some repository object and it is called in the context of an existing entity manager and an existing transaction.
  • 我假设给定的findOrCreate方法将存在于某个存储库对象上,并且它在现有实体管理器和现有事务的上下文中被调用。
  • If the transaction isolation level is serializable or snapshot this won't work. If the transaction is repeatable read then you must not have attempted to read the entity in the current transaction.
  • 如果事务隔离级别是可序列化的或快照,则这将不起作用。如果事务是可重复读取的,则您不得尝试读取当前事务中的实体。
  • I'd recommend breaking the logic below into multiple methods for maintainability.
  • 我建议将下面的逻辑分解为多种可维护性方法。

Code:

码:

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();
}

#1


-2  

  1. Create an EntityManager instance (let's call it "em"), unless you already have an active one
  2. 创建一个EntityManager实例(让我们称之为“em”),除非您已经有一个活动实例
  3. Create a new transaction (let's call it "tx")
  4. 创建一个新的交易(让我们称之为“tx”)
  5. Call em.find(Object pk)
  6. 调用em.find(Object pk)
  7. Call tx.begin()
  8. 调用tx.begin()
    • If find() returned a non-null entity reference then you need to do an update. Apply your changes to the returned entity and then call em.merge(Object entity).
    • 如果find()返回非null实体引用,则需要进行更新。将更改应用于返回的实体,然后调用em.merge(对象实体)。
    • if find() returned a null reference, then that PK does not exist in the database. Create a new entity and then call em.persist(Object newEntity).
    • 如果find()返回一个空引用,那么该数据库中不存在该PK。创建一个新实体,然后调用em.persist(Object newEntity)。
  9. 如果find()返回非null实体引用,则需要进行更新。将更改应用于返回的实体,然后调用em.merge(对象实体)。如果find()返回一个空引用,那么该数据库中不存在该PK。创建一个新实体,然后调用em.persist(Object newEntity)。
  10. Call em.flush()
  11. 打电话给em.flush()
  12. Call tx.commit()
  13. 致电tx.commit()
  14. Return your entity reference, per your method signature.
  15. 根据您的方法签名返回您的实体引用。

#2


15  

I'd like to write a method like <T> T getOrCreate(Class<T> klass, Object primaryKey)

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

This won't be easy.

这并不容易。

A naive approach would be to do something like this (assuming the method is running inside a transaction):

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

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);
        }
    }
}

But in a concurrent environment, this code could fail due to some race condition:

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

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

And if you are running multiple JVMs, synchronization won't help. And without acquiring a table lock (which is pretty horrible), I don't really see how you could solve this.

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

In such case, I wonder if it wouldn't be better to systematically insert first and handle a possible exception to perform a subsequent select (in a new transaction).

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

You should probably add some details regarding the mentioned constraints (multi-threading? distributed environment?).

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

#3


4  

Using pure JPA one can solve this optimistically in a multi-threaded solution with nested entity managers (really we just need nested transactions but I don't think that is possible with pure JPA). Essentially one needs to create a micro-transaction that encapsulates the find-or-create operation. This performance won't be fantastic and isn't suitable for large batched creates but should be sufficient for most cases.

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

Prerequisites:

先决条件:

  • The entity must have a unique constraint violation that will fail if two instances are created
  • 如果创建了两个实例,则实体必须具有唯一的约束冲突,该冲突将失败
  • You have some kind of finder to find the entity (can find by primary key with EntityManager.find or by some query) we will refer to this as finder
  • 您有某种查找器来查找实体(可以通过主键使用EntityManager.find或通过某些查询找到)我们将此作为查找程序引用
  • You have some kind of factory method to create a new entity should the one you are looking for fail to exist, we will refer to this as factory.
  • 如果您要查找的实体不存在,您可以使用某种工厂方法来创建新实体,我们将其称为工厂。
  • I'm assuming that the given findOrCreate method would exist on some repository object and it is called in the context of an existing entity manager and an existing transaction.
  • 我假设给定的findOrCreate方法将存在于某个存储库对象上,并且它在现有实体管理器和现有事务的上下文中被调用。
  • If the transaction isolation level is serializable or snapshot this won't work. If the transaction is repeatable read then you must not have attempted to read the entity in the current transaction.
  • 如果事务隔离级别是可序列化的或快照,则这将不起作用。如果事务是可重复读取的,则您不得尝试读取当前事务中的实体。
  • I'd recommend breaking the logic below into multiple methods for maintainability.
  • 我建议将下面的逻辑分解为多种可维护性方法。

Code:

码:

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();
}