JPA - create-if-not-exists entity? JPA - create-if-not-exists entity? database database

JPA - create-if-not-exists entity?


I'd like to write a method like <T> T getOrCreate(Class<T> 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 nullT2: SELECT w/ id = 123; //returns nullT1: INSERT w/ id = 123;T1: COMMIT; //row insertedT2: 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.

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?).


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.

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


I must point out there's some flaw in @gus an's answer. It could lead to an apparent problem in a concurrent situation. If there are 2 threads reading the count, they would both get 0 and then do the insertion. So duplicate rows created.

My suggestion here is to write your native query like the one below:

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)

It's just like an optimistic lock in the program. But we make the db engine to decide and find the duplicates by your customized attributes.