ResultSet

Navegación y mapeo de filas a objetos

ResultSet es un cursor que empieza antes de la primera fila. next() avanza una posición y devuelve true si la nueva fila existe, false si se ha llegado al final. El patrón while (rs.next()) recorre todas las filas.

import java.sql.*;

// ResultSet: cursor que apunta a las filas del resultado
// Por defecto: TYPE_FORWARD_ONLY — solo avanza, una fila a la vez

try (Connection con = dataSource.getConnection();
     PreparedStatement ps = con.prepareStatement("SELECT id, nombre, email FROM clientes");
     ResultSet rs = ps.executeQuery()) {

    // rs.next() avanza el cursor y devuelve false cuando no hay más filas
    while (rs.next()) {
        // Acceso por nombre de columna (más legible, recomendado)
        long   id     = rs.getLong("id");
        String nombre = rs.getString("nombre");
        String email  = rs.getString("email");

        // Acceso por índice (base 1, más rápido pero menos mantenible)
        long   id2     = rs.getLong(1);
        String nombre2 = rs.getString(2);

        System.out.println(id + ": " + nombre + " <" + email + ">");
    }
}

Tipos y NULL

Cada tipo SQL tiene un método getXxx correspondiente. Para tipos numéricos primitivos (getInt, getLong...), si el valor es SQL NULL el método devuelve 0 — hay que llamar a wasNull() inmediatamente después para distinguir el 0 real del NULL.

// Métodos de lectura del ResultSet según el tipo SQL:
ResultSet rs = ps.executeQuery();
while (rs.next()) {
    long    id        = rs.getLong("id");          // BIGINT
    int     cantidad  = rs.getInt("cantidad");     // INT
    String  nombre    = rs.getString("nombre");    // VARCHAR, TEXT, CHAR
    double  precio    = rs.getDouble("precio");    // FLOAT, DOUBLE (usar BigDecimal para dinero)
    BigDecimal total  = rs.getBigDecimal("total"); // DECIMAL, NUMERIC
    boolean activo    = rs.getBoolean("activo");   // BOOLEAN, BIT
    Date    fecha     = rs.getDate("fecha");       // DATE → java.sql.Date
    Timestamp ts      = rs.getTimestamp("creado"); // TIMESTAMP → java.sql.Timestamp
    byte[]  datos     = rs.getBytes("blob_col");   // BLOB, BINARY

    // Detectar NULL: getXxx devuelve 0/false/null para tipos primitivos y referencia
    // Para primitivos, verificar con wasNull() después de la llamada
    int valorNulable = rs.getInt("opcional");
    if (rs.wasNull()) {
        System.out.println("El campo era NULL");
    }
}

Row mapping a objetos

El patrón habitual es encapsular la conversión de ResultSet a objeto de dominio en una clase o método separado (row mapper). Esto es exactamente lo que Spring JDBC hace con RowMapper<T>.

// Mapeo de fila a objeto de dominio (row mapping)
public class ClienteMapper {
    public static Cliente mapear(ResultSet rs) throws SQLException {
        Cliente c = new Cliente();
        c.setId(rs.getLong("id"));
        c.setNombre(rs.getString("nombre"));
        c.setEmail(rs.getString("email"));
        c.setActivo(rs.getBoolean("activo"));

        // Convertir java.sql.Date a java.time.LocalDate
        Date fecha = rs.getDate("fecha_alta");
        if (fecha != null) c.setFechaAlta(fecha.toLocalDate());

        return c;
    }
}

// Uso en el repositorio:
public List<Cliente> buscarActivos() throws SQLException {
    String sql = "SELECT id, nombre, email, activo, fecha_alta FROM clientes WHERE activo = TRUE";
    List<Cliente> resultado = new ArrayList<>();
    try (Connection con = dataSource.getConnection();
         PreparedStatement ps = con.prepareStatement(sql);
         ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            resultado.add(ClienteMapper.mapear(rs));
        }
    }
    return resultado;
}

Metadatos del resultado

// Metadatos del ResultSet: estructura de las columnas
try (ResultSet rs = ps.executeQuery()) {
    ResultSetMetaData meta = rs.getMetaData();
    int numColumnas = meta.getColumnCount();

    for (int i = 1; i <= numColumnas; i++) {
        System.out.println(
            meta.getColumnName(i) + " (" + meta.getColumnTypeName(i) + ")"
        );
    }
    // Útil para herramientas genéricas que no conocen la estructura de antemano
}
Acceso por nombre vs índice: usa nombres de columna (rs.getString("nombre")) en lugar de índices (rs.getString(2)). Los nombres son más legibles y no se rompen si la consulta cambia el orden de las columnas. El impacto de rendimiento es insignificante en la mayoría de casos.

Siguiente apartado → CRUD completo con JDBC

Índice de la sección

Índice del curso