java - proveedores - JPA-¿Crear si no existe una entidad?
onetomany jpa ejemplo (3)
Tengo varios objetos asignados en mi aplicación JPA / Hibernate. En la red recibo paquetes que representan actualizaciones de estos objetos, o de hecho puedo representar objetos nuevos por completo.
Me gustaría escribir un método como
<T> T getOrCreate(Class<T> klass, Object primaryKey)
que devuelve un objeto de la clase provista si existe uno en la base de datos con pk primaryKey, y de lo contrario crea un nuevo objeto de esa clase, lo persiste y lo devuelve.
Lo siguiente que haré con el objeto será actualizar todos sus campos, dentro de una transacción.
¿Hay una forma idiomática de hacer esto en JPA, o hay una mejor manera de resolver mi problema?
Me gustaría escribir un método como
<T> T getOrCreate(Class<T> klass, Object primaryKey)
Esto no será fácil.
Un enfoque ingenuo sería hacer algo como esto (asumiendo que el método se ejecuta dentro de una transacción):
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);
}
}
}
Pero en un entorno concurrente, este código podría fallar debido a alguna condición de carrera:
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
Y si está ejecutando varias JVM, la sincronización no ayudará. Y sin adquirir un bloqueo de mesa (que es bastante horrible), realmente no veo cómo podría resolver esto.
En tal caso, me pregunto si no sería mejor insertar sistemáticamente primero y manejar una posible excepción para realizar una selección posterior (en una nueva transacción).
Probablemente debería agregar algunos detalles con respecto a las restricciones mencionadas (¿multihilo? ¿Ambiente distribuido?).
- Cree una instancia de EntityManager (llamémosla "em"), a menos que ya tenga una activa
- Crear una nueva transacción (llamémosla "tx")
- Call em.find (Object pk)
- Llame a tx.begin ()
- Si find () devuelve una referencia de entidad no nula, entonces necesita hacer una actualización. Aplique sus cambios a la entidad devuelta y luego llame a em.merge (entidad de objeto).
- Si find () devuelve una referencia nula, entonces esa PK no existe en la base de datos. Cree una nueva entidad y luego llame a em.persist (Object newEntity).
- Llame a em.flush ()
- Llame a tx.commit ()
- Devuelva la referencia de su entidad, según su método de firma.
El uso de JPA puro puede resolver esto de manera optimista en una solución de subprocesos múltiples con administradores de entidades anidadas (en realidad solo necesitamos transacciones anidadas, pero no creo que sea posible con JPA pura). Esencialmente, uno necesita crear una micro-transacción que encapsule la operación de buscar o crear. Este rendimiento no será fantástico y no es adecuado para grandes creaciones por lotes, pero debería ser suficiente para la mayoría de los casos.
Requisitos previos:
- La entidad debe tener una violación de restricción única que fallará si se crean dos instancias
- Usted tiene algún tipo de buscador para encontrar la entidad (puede encontrar por clave principal con EntityManager.find o por alguna consulta) nos referiremos a esto como
finder
- Tiene algún tipo de método de fábrica para crear una nueva entidad, si el que está buscando no existe, lo llamaremos
factory
. - Supongo que el método findOrCreate dado existiría en algún objeto del repositorio y se llama en el contexto de un administrador de entidades existente y una transacción existente.
- Si el nivel de aislamiento de la transacción es serializable o instantánea, esto no funcionará. Si la transacción es de lectura repetible, entonces no debe haber intentado leer la entidad en la transacción actual.
- Recomiendo dividir la lógica a continuación en múltiples métodos para mantenerla.
Código:
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();
}