java - Cómo recuperar*todo*de un procedimiento almacenado usando JDBC
sql-server stored-procedures (1)
Ocasionalmente encuentro dos formas de comportamiento extraño cuando uso JDBC para trabajar con procedimientos almacenados de SQL Server:
Problema 1: ejecuto un procedimiento almacenado en SQL Server Management Studio (SSMS) y devuelve un conjunto de resultados. Sin embargo, cuando lo intento
try (CallableStatement cs = conn.prepareCall("{call dbo.TroublesomeSP}")) {
ResultSet rs = cs.executeQuery();
Me sale la excepcion
com.microsoft.sqlserver.jdbc.SQLServerException: la instrucción no devolvió un conjunto de resultados.
Problema 2:
ejecuto un procedimiento almacenado en SSMS y genera un error, pero cuando uso JDBC para
.execute
el procedimiento almacenado no se produce ninguna excepción.
¿Por qué ocurren estos problemas y cómo puedo evitarlos?
Cuando ejecutamos un procedimiento almacenado en JDBC, obtenemos una serie de cero o más "resultados".
Entonces podemos procesar esos "resultados" secuencialmente llamando a
CallableStatement#getMoreResults()
.
Cada "resultado" puede contener
-
cero o más filas de datos que podemos recuperar con un objeto
ResultSet
, -
un recuento de actualizaciones para una instrucción DML (INSERT, UPDATE, DELETE) que podemos recuperar con
CallableStatement#getUpdateCount()
, o - Un error que arroja una SQLServerException.
Para el "Problema 1", el problema a menudo es que el procedimiento almacenado no comienza con
SET NOCOUNT ON;
y ejecuta una instrucción DML antes de hacer una SELECCIÓN para producir un conjunto de resultados.
El recuento de actualizaciones para el DML se devuelve como el primer "resultado", y las filas de datos están "atrapadas detrás de él" hasta que llamemos a
getMoreResults
.
El "problema 2" es esencialmente el mismo problema.
El procedimiento almacenado produce un "resultado" (generalmente un SELECCIONAR, o posiblemente un recuento de actualizaciones) antes de que ocurra el error.
El error se devuelve en un "resultado" posterior y no causa una excepción hasta que lo "recuperemos" usando
getMoreResults
.
En muchos casos, el problema se puede evitar simplemente agregando
SET NOCOUNT ON;
como la primera instrucción ejecutable en el procedimiento almacenado.
Sin embargo, un cambio en el procedimiento almacenado no siempre es posible y el hecho es que para recuperar
todo
del procedimiento almacenado debemos seguir llamando a
getMoreResults
hasta que, como dice el Javadoc:
There are no more results when the following is true:
// stmt is a Statement object
((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1))
Eso suena bastante simple pero, como siempre, "el diablo está en los detalles", como se ilustra en el siguiente ejemplo. Para un procedimiento almacenado de SQL Server ...
ALTER PROCEDURE dbo.TroublesomeSP AS
BEGIN
-- note: no `SET NOCOUNT ON;`
DECLARE @tbl TABLE (id VARCHAR(3) PRIMARY KEY);
DROP TABLE NonExistent;
INSERT INTO @tbl (id) VALUES (''001'');
SELECT id FROM @tbl;
INSERT INTO @tbl (id) VALUES (''001''); -- duplicate key error
SELECT 1/0; -- error _inside_ ResultSet
INSERT INTO @tbl (id) VALUES (''101'');
INSERT INTO @tbl (id) VALUES (''201''),(''202'');
SELECT id FROM @tbl;
END
... el siguiente código Java devolverá todo ...
try (CallableStatement cs = conn.prepareCall("{call dbo.TroublesomeSP}")) {
boolean resultSetAvailable = false;
int numberOfResultsProcessed = 0;
try {
resultSetAvailable = cs.execute();
} catch (SQLServerException sse) {
System.out.printf("Exception thrown on execute: %s%n%n", sse.getMessage());
numberOfResultsProcessed++;
}
int updateCount = -2; // initialize to impossible(?) value
while (true) {
boolean exceptionOccurred = true;
do {
try {
if (numberOfResultsProcessed > 0) {
resultSetAvailable = cs.getMoreResults();
}
exceptionOccurred = false;
updateCount = cs.getUpdateCount();
} catch (SQLServerException sse) {
System.out.printf("Current result is an exception: %s%n%n", sse.getMessage());
}
numberOfResultsProcessed++;
} while (exceptionOccurred);
if ((!resultSetAvailable) && (updateCount == -1)) {
break; // we''re done
}
if (resultSetAvailable) {
System.out.println("Current result is a ResultSet:");
try (ResultSet rs = cs.getResultSet()) {
try {
while (rs.next()) {
System.out.println(rs.getString(1));
}
} catch (SQLServerException sse) {
System.out.printf("Exception while processing ResultSet: %s%n", sse.getMessage());
}
}
} else {
System.out.printf("Current result is an update count: %d %s affected%n",
updateCount,
updateCount == 1 ? "row was" : "rows were");
}
System.out.println();
}
System.out.println("[end of results]");
}
... produciendo la siguiente salida de consola:
Exception thrown on execute: Cannot drop the table ''NonExistent'', because it does not exist or you do not have permission.
Current result is an update count: 1 row was affected
Current result is a ResultSet:
001
Current result is an exception: Violation of PRIMARY KEY constraint ''PK__#314D4EA__3213E83F3335971A''. Cannot insert duplicate key in object ''dbo.@tbl''. The duplicate key value is (001).
Current result is a ResultSet:
Exception while processing ResultSet: Divide by zero error encountered.
Current result is an update count: 1 row was affected
Current result is an update count: 2 rows were affected
Current result is a ResultSet:
001
101
201
202
[end of results]