Hay un chiste en el que alguien le pregunta al técnico, «¿500 € solo por girar un tornillo?», y el técnico responde, «No, por girar el tornillo ha sido 1 €, por saber hacia que lado girarlo 499». Pues el siguiente artículo es lo equivalente que me ha pasado con un error en una aplicación de Java.
No creo que los creadores del lenguaje y ecosistema Java lo diseñaran pensando en que los desarrolladores pudiéramos parchear una clase de una librería redefiniendo y colocándola en el classpath con mayor prioridad de carga.
Pero esto que es posible es precisamente lo que he tenido que hacer recientemente para solucionar un problema en una aplicación legacy al migrar versiones antiguas de librerías.
El error
El error tenía el siguiente stacktrace motivado por una actualización mayor de Java y el driver de Oracle ojdbc desde Java 7 y ojdbc 5 a Java 8 y ojdbc 8, de una aplicación que usa Spring e Hibernate 3 en una versión antiguas.
El error indica que alguna comprobación que realiza Hibernate empieza a fallar con la actualización de librerías. Con este stacktrace toca investigar si alguien con menos suerte ha sido el primero que se ha encontrado con esta misma excepción y la ha compartido en stackoverflow o en un post de algún blog.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:85)
at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:70)
at org.hibernate.jdbc.BatchingBatcher.checkRowCounts(BatchingBatcher.java:90)
at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:268)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:185)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1216)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:383)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:133)
at org.springframework.orm.hibernate3.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:658)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:662)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:632)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:314)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:116)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
at com.sun.proxy.$Proxy657.buildAndPersistBuyerOrder(Unknown Source)
at com.acme.domain.accountmanagement.services.buyerorders.v1.impl.BuyerOrderServiceImpl.createOrder(BuyerOrderServiceImpl.java:113)
|
stacktrace.txt
La sugerencia de Claude Code
Otra vía de investigación con esto de la IA ha sido usar Claude Code, esta sugería cambiar la propiedad de configuración de Hibernate batch_size al valor 0 en vez de 100.
Claude Code lo comentaba con bastante seguridad, sin embargo, al cambiarla se seguía produciendo el mismo stacktrace.
El parche
Dado que ni una búsqueda en internet ni Claude Code daban una solución que funcionase no me ha quedado otro remedio que buscar alguna alternativa por mucho workaround o ñapa que fuera capaz de entrar en las primeras posiciones del salón de la fama.
En primer lugar inspecciono ese stacktrace donde se está dando la excepción, me fijo en la clases BatchingBatcher y Expectations que por el nombre de sus métodos están haciendo alguna comprobación confirmado por el mensaje de la excepción.
Con IntelliJ y su decompilador de archivos class con el bytecode de Java obtener el pseudo código fuente de esas clases que quizá no conserva los mismos números de línea o nombres de variables pero que sirve para obtener un aproximación del código fuente suficiente para inspeccionarlo y entender que está haciendo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class BatchingBatcher {
...
private void checkRowCounts(int[] rowCounts, PreparedStatement ps) throws SQLException, HibernateException {
int numberOfRowCounts = rowCounts.length;
if (numberOfRowCounts != this.batchSize) {
log.warn("JDBC driver did not return the expected number of row counts");
}
for(int i = 0; i < numberOfRowCounts; ++i) {
this.expectations[i].verifyOutcome(rowCounts[i], ps, i);
}
}
}
|
BatchingBatcher-1.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public static class Expectations implements Expectation {
...
public static class BasicExpectation implements Expectation {
public final void verifyOutcome(int rowCount, PreparedStatement statement, int batchPosition) {
rowCount = this.determineRowCount(rowCount, statement);
if (batchPosition < 0) {
this.checkNonBatched(rowCount);
} else {
this.checkBatched(rowCount, batchPosition);
}
}
protected int determineRowCount(int reportedRowCount, PreparedStatement statement) {
return reportedRowCount;
}
private void checkBatched(int rowCount, int batchPosition) {
if (rowCount == -2) {
if (Expectations.log.isDebugEnabled()) {
Expectations.log.debug("success of batch update unknown: " + batchPosition);
}
} else {
if (rowCount == -3) {
throw new BatchFailedException("Batch update failed: " + batchPosition);
}
if (this.expectedRowCount > rowCount) {
throw new StaleStateException("Batch update returned unexpected row count from update [" + batchPosition + "]; actual row count: " + rowCount + "; expected: " + this.expectedRowCount);
}
if (this.expectedRowCount < rowCount) {
String msg = "Batch update returned unexpected row count from update [" + batchPosition + "]; actual row count: " + rowCount + "; expected: " + this.expectedRowCount;
throw new BatchedTooManyRowsAffectedException(msg, this.expectedRowCount, rowCount, batchPosition);
}
}
}
}
}
}
|
Expectations.java
El cambio consiste en modificar esa llamada a verifyOutcome con el rowCount que debe obtener del driver y pasarle un el número mágico de -2 o -3 que la clase de Expectations acepta como si al ejecutar la sentencia SQL ha sido exitoso pero no se ha podido obtener el número de filas afectadas o ha habido algún error en el batch.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class BatchingBatcher {
...
private void checkRowCounts(int[] rowCounts, PreparedStatement ps) throws SQLException, HibernateException {
int numberOfRowCounts = rowCounts.length;
if (numberOfRowCounts != this.batchSize) {
log.warn("JDBC driver did not return the expected number of row counts");
}
for (int i = 0; i < numberOfRowCounts; ++i) {
int rowCount = (rowCounts[i] == -3) ? -3 : -2;
this.expectations[i].verifyOutcome(rowCount, ps, i);
}
}
}
|
BatchingBatcher-2.java
Con este cambio de una sola línea ya solo queda colocar esta nueva redefinición de la clase en un orden de prioridad que el de la librería. Para lo cual es necesario conocer que en las aplicaciones Java las clases en el classpath de la aplicación tiene más prioridad que el de las clases Java de las librerías.
Sí, puede que el parche sea feo pero es que la mejor solución sería reescribir esa aplicación usando las versiones tanto de Java como del conjunto de librerías que usa esa aplicación, pero eso cuesta algunos meses de trabajo y el parche solo unas horas o días. Así que determinar cual es la mejor solución depende por que criterio se mida.
Un código del que alguien dejó escrito esto en el README de un repositorio con código legacy parecido.
1
2
3
4
5
|
Dear developer,
If you got here I can only tell you one thing:
Run as fast as you can.
|
README.txt
La solución
La solución implica analizar el stacktrace, decompilar y conocer cómo inspeccionar el código fuente de las clases de ese stacktrace y donde colocar esa nueva clase para que sea efectiva.
El cambio es sencillo, modificar solo una línea de código fuente, para lo demás uno puede que necesite 25 años de experiencia trabajando con Java para idear esta solución.