Tengo algo de código que realiza una UPSERT, también conocidos como Merge . Quiero limpieza de este código, en concreto, quiero alejarse de manejo de excepciones, y reducir la verbosidad general y la enorme complejidad del código para una operación tan sencilla. El requisito es que insertar cada artículo a menos que ya existe:
public void batchInsert(IncomingItem[] items) {
try(Session session = sessionFactory.openSession()) {
batchInsert(session, items);
}
catch(PersistenceException e) {
if(e.getCause() instanceof ConstraintViolationException) {
logger.warn("attempting to recover from constraint violation");
DateTimeFormatter dbFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
items = Arrays.stream(items).filter(item -> {
int n = db.queryForObject("select count(*) from rets where source = ? and systemid = ? and updtdate = ?::timestamp",
Integer.class,
item.getSource().name(), item.getSystemID(),
dbFormat.format(item.getUpdtDateObj()));
if(n != 0) {
logger.warn("REMOVED DUPLICATE: " +
item.getSource() + " " + item.getSystemID() + " " + item.getUpdtDate());
return false;
}
else {
return true; // keep
}
}).toArray(IncomingItem[]::new);
try(Session session = sessionFactory.openSession()) {
batchInsert(session, items);
}
}
}
}
Una búsqueda inicial de SO no es satisfactoria:
- Hibernate idempotente Actualización - conceptualmente similar pero mucho más simple escenario sin tener en cuenta múltiples hilos o multi-procesamiento.
- Hibernate puede trabajar con sintaxis "en la llave duplicada UPDATE" de MySQL? mucho mejor, elimina la condición de carrera empujando la atomicidad de la base de datos utilizando
@SQLInsert
la anotación; Por desgracia, esta solución es demasiado propenso a errores a utilizar en tablas más anchas y mantenimiento intensivo en la evolución de las aplicaciones. - Cómo comportamiento upsert imitan usando Hibernate? muy similar a la pregunta anterior, con una respuesta similar
- Hibernate + "ON DUPLICATE KEY" lógica mismo que el anterior, la respuesta menciona
merge()
que está bien cuando un único subproceso - Inserción masiva o actualización con Hibernate? pregunta similar pero la respuesta elegida es off-the-carriles, utilizando procedimientos almacenados
- La mejor manera de prevenir violaciónes de restricción únicos con la APP de nuevo muy ingenua, orientado a un solo hilo de preguntas y respuestas
En la pregunta ¿Cómo hacer una llave duplicada EN ACTUALIZACIÓN en la primavera de datos JPA? que fue marcado como duplicado, me di cuenta de este comentario intrigante:
Eso era un callejón sin salida ya que realmente no entiendo el comentario, a pesar de que suene como una solución inteligente, y la mención de "misma instrucción SQL real".
Otro enfoque prometedor es la siguiente: Hibernate y primavera modificar consulta antes de someterse a DB
EN CONFLICTO no hacer nada / EN ACTUALIZACIÓN una llave duplicada
Tanto de las principales bases de datos de código de soporte abierto un mecanismo para empujar idempotencia abajo a la base de datos. Los ejemplos siguientes utilizan la sintaxis PostgreSQL, pero pueden ser fácilmente adaptados para MySQL.
Siguiendo las ideas de Hibernate y primavera modifico consulta antes de someterse a DB , enganchar en la generación de consultas de Hibernate , y ¿Cómo puedo configurar StatementInspector en Hibernate? , He implementado:
import org.hibernate.resource.jdbc.spi.StatementInspector;
@SuppressWarnings("serial")
public class IdempotentInspector implements StatementInspector {
@Override
public String inspect(String sql) {
if(sql.startsWith("insert into rets")) {
sql += " ON CONFLICT DO NOTHING";
}
return sql;
}
}
con la propiedad
<prop key="hibernate.session_factory.statement_inspector">com.myapp.IdempotentInspector</prop>
Por desgracia esto lleva a la siguiente mensaje de error cuando se encuentra un duplicado:
Causada por: org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: actualización por lotes volvió recuento de filas inesperado de actualización [0]; real recuento de filas: 0; esperado: 1; excepción anidada es org.hibernate.StaleStateException: actualización por lotes volvió recuento de filas inesperado de actualización [0]; real recuento de filas: 0; esperada: 1
Lo cual tiene sentido, si se piensa en lo que está pasando debajo de las sábanas: la ON CONFLICT DO NOTHING
causa cero filas que se insertan, pero se espera una inserción.
¿Hay una solución que permite inserciones concurrentes idempotent sin excepción compatibles con el proceso y no requiere definir manualmente toda la instrucción de inserción SQL para su ejecución por Hibernate?
Por si sirve de algo, siento que los enfoques que empujan hacia abajo el dupcheck a la base de datos son el camino a una solución adecuada.
ACLARACIÓN El IncomingItem
objetos consumida por el batchInsert
método de proceder de un sistema en el que los registros son inmutables. Bajo esta condición especial del ON CONFLICT DO NOTHING
comporta igual que un UPSERT, a pesar de posibles pérdidas de la actualización enésimo .
Respuesta corta - Hibernate no lo soporta fuera de la caja (como se confirma por un gurú de hibernación en esta entrada del blog ). Probablemente se podría hacer que funcione en cierta medida en algunos escenarios con los mecanismos que ya se ha descrito, pero sólo el uso de consultas nativas mira directamente el enfoque más sencillo para mí para este propósito.
Respuesta larga sería que sería difícil para apoyarlo teniendo en cuenta todos los aspectos de la hibernación supongo, por ejemplo:
- ¿Qué hacer con los casos para los que se encuentran duplicados, ya que se supone para convertirse logrado después de persistir? Fusionarlas en un contexto de persistencia?
- ¿Qué hacer con las asociaciones que ya se han persistido, que las operaciones en cascada para aplicar en ellos (persistir / fusión / something_new; o es demasiado tarde en ese momento para tomar esa decisión)?
- ¿Las bases de datos de información de retorno suficiente upsert operaciones para cubrir todos los casos de uso (saltado filas; claves generadas para no-omiten en el lote de inserción modos, etc).
- ¿Qué pasa con
@Audit
las entidades -ed, se les crea o se actualiza, si actualiza lo que ha cambiado? - O de versiones y bloqueo optimista (por la definición que realmente desea excepción en este caso)?
Incluso si Hibernate apoyado de alguna manera, no estoy seguro de que estaría utilizando esa característica si había demasiadas advertencias a tener en cuenta y tener en cuenta.
Por lo tanto, la regla general es que sigo:
- Para los escenarios simples (que son la mayoría del tiempo): persistir + reintento. Reintentos en caso de errores específicos (por tipo de excepción o similares) se pueden configurar a nivel mundial con AOP-como enfoques (anotaciones, interceptores personalizados y similares) en función de los cuales los marcos que utiliza en su proyecto y es una práctica bien de todos modos, especialmente en entornos distribuidos .
- Para los escenarios complejos y operaciones intensivas de rendimiento (sobre todo cuando se trata de procesamiento por lotes, consultas muy complejas y por igual): consultas nativas para maximizar la utilización de funciones de bases de datos específicas.