Cursores avanzados

Normalmente los ResultSets operan hacia adelante y vamos pasando de un registro a otro con next(), pero se les puede añadir la posibilidad de ser dependientes del contexto. Si añadimos a un ResultSet la capacidad de ser tolerante a scroll, podemos mover el cursor por los resultados de una tabla utilizando los métodos first, next, previous, last, beforeFirst, afterLast...

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.

Representación de un ResultSet, con el cursor como una flecha

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:

Representación más realista de cómo es un ResultSet en Java

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 como absolute(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í que absolute(-1) lo llevaría a la última fila, y absolute(-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 a next(), relative(-1) sería como llamar a previous()..., pero también podrías hacer algo como relative(3) para bajar 3 filas, o relative(-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 SQLException
  • Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException
  • Statement 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.

Lista de reproducción
  1. 1
    ¿Qué es JDBC?
    5 minutos
  2. 2
    Configurar un driver
    7 minutos
  3. 3
    Deja de poner Class.forName
    8 minutos
  4. 4
    Conectarse en JDBC
    10 minutos
  5. 5
    Cómo ejecutar consultas
    10 minutos
  6. 6
    PreparedStatement, ¿por qué usarlo?
    11 minutos
  7. 7
    Cómo insertar, modificar y borrar datos
    10 minutos
  8. 8
    Transacciones
    12 minutos
  9. 9
    Cursores avanzados
    11 minutos
  10. 10
    ResultSets concurrentes
    10 minutos
  11. 11
    DataSource: así se usa JDBC en la vida real
    14 minutos
  12. 12
    ¿Se sigue usando JDBC?
    10 minutos