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.
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.
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?
Therefore, if you want to check a record's existence with Spring Data, the easiest way to do so is using the existsBy query method.
CrudRepository provides CRUD functions. PagingAndSortingRepository provides methods to do pagination and sort records. JpaRepository provides JPA related methods such as flushing the persistence context and delete records in a batch.
The findById() method is used to retrieves an entity by its id and it is available in CrudRepository interface. The CrudRepository extends Repository interface. In Spring Data JPA Repository is top-level interface in the hierarchy.
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 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.
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:
finder
factory
.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(); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With