Las filas virtuales de un ResultSet
En JDBC, cuando recorras un ResultSet, puedes usar el método next() para avanzar de una fila de resultados a la siguiente. Esto ya te lo conté en el capítulo dedicado a consultas. Esto se debe a que en JDBC, un ResultSet está dirigido por un cursor, que es el que se va moviendo de fila a fila. Cada llamada a next() hace avanzar el cursor al siguiente registro.

Sin embargo, lo que posiblemente no sepas es que en realidad JDBC te permite recorrer un ResultSet de forma bidireccional. Igual que puedes avanzar a la siguiente fila, puedes retroceder a la fila anterior.
La base de todo esto está en que, en JDBC, un ResultSet realmente tiene dos filas más, que son virtuales y que realmente no existen:
- Hay una fila virtual antes del primer elemento.
- Hay una fila virtual después del último elemento.
Así que, en realidad, un ResultSet tiene más bien un aspecto como el siguiente, donde las flechas rojas representan cada una de las posiciones que puede tener el cursor:

Esto es lo que explica que tengas que hacer next() para acceder a la primera fila de un registro: el cursor originalmente estará apuntando a una fila virtual pero no a los datos de tu consulta.
Los otros métodos de ResultSet
Sin embargo, next() no es el único método que hay en ResultSet. Esta clase tiene métodos adicionales que también sirven para hacer movimientos de cursor diferentes a next():
previous(): retrocede a la fila anterior de la tabla.first(): retrocede a la primera fila de la tabla.last(): avanza a la última fila de la tabla.beforeFirst(): retrocede a la fila virtual que viene antes del primer registro.afterLast(): avanza a la fila virtual que viene tras el último registro.
También hay un par de métodos para hacer un control más directo del cursor:
absolute(int): este método coloca el cursor en la fila número tal de un ResultSet. Por ejemplo, podrías invocarlo comoabsolute(2)para desplazar el cursor a la segunda fila,absolute(5)para llevarlo a la quinta fila... Incluso puedes usar números negativos para empezar por el final, así queabsolute(-1)lo llevaría a la última fila, yabsolute(-2)a la penúltima.relative(int): te permite mover el cursor N filas arriba o N filas abajo. Si le das un número positivo, lo mueve hacia adelante.relative(1)sería como llamar anext(),relative(-1)sería como llamar aprevious()..., pero también podrías hacer algo comorelative(3)para bajar 3 filas, orelative(-2)para subir 2 filas.
Configurar un ResultSet bidireccional
Ninguno de los métodos que te he presentado en la parte anterior va a funcionar bien si no se configura previamente el ResultSet. En otras palabras: el ResultSet no es bidireccional por defecto.
Para configurarlo, tenemos que usar el mismo método createStatement(). Hasta ahora, yo solo he contado en este curso la llamada a createStatement() que no tiene argumentos. Sin embargo, en realidad createStatement() es otro de esos métodos sobrecargados que tienen varias formas de ser invocados:
Statement createStatement() throws SQLExceptionStatement createStatement(int resultSetType, int resultSetConcurrency) throws SQLExceptionStatement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException
El primer parámetro de las sobrecargas, resultSetType, es el que especifica precisamente si el ResultSet es bidireccional o no. Acepta tres posibles valores:
ResultSet.TYPE_FORWARD_ONLY: este es el valor por defecto, y quiere decir que sólo puede avanzar hacia adelante. Si intentas llamar a alguno de los métodos que vuelve atrás, te dará un error.ResultSet.TYPE_SCROLL_SENSITIVE: este tipo de ResultSet sí que te permite volver atrás. Cuando un ResultSet es sensitive, se considera que los datos navegables mediante el ResultSet pueden cambiar, por lo que cuando se cambia de registro, se preocupa de ver si la fila ha sido modificada.ResultSet.TYPE_SCROLL_INSENSITIVE: este también te deja volver atrás, pero no es sensitive, así que si se producen modificaciones luego de lanzar la query original, no se consideran y siempre se devuelven los datos originales del instante en el que se lanzó la query.
Es obligatorio especificar también, al menos, el resultSetConcurrency. Este tiene que ver con el tipo de concurrencia de un ResultSet de cara a modificaciones, algo de lo que te hablo en la siguiente lección. Por ahora, si quieres ignorar este valor, le puedes pasar ResultSet.CONCUR_READ_ONLY, el valor por defecto.
Contado todo esto, podemos fabricar un ResultSet que puede avanzar adelante y atrás:
try (Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
var query = "SELECT nombre, apellido, fecha_nacimiento FROM pacientes";
try (ResultSet rs = stmt.executeQuery(query)) {
// Nos vamos al final de la tabla.
rs.afterLast();
// Vamos recorriendo hasta llegar al principio.
while (rs.previous()) {
// TODO: trabajar con este registro
}
}
} catch (SQLException e) {
// Por favor, no te olvides de mi...
e.printStackTrace();
}
Como puedes ver, el método previous() también devuelve un valor booleano que indica si se ha podido aterrizar sobre una fila real de la tabla o si hemos tocado el borde virtual de la tabla. De este modo, también podemos hacer un bucle while que termine cuando hayamos recorrido la tabla completa, aunque en este caso al revés.