PreparedStatement, ¿por qué usarlo?

PreparedStatement permite utilizar sentencias preparadas, donde se pueden parametrizar instrucciones SQL. Es importante usar este tipo de sentencias cuando vayamos a aceptar datos del mundo exterior para poder validarlos y evitar agujeros de seguridad.

En muchos casos, la consulta SQL que quieres tirar a una base de datos lleva parámetros para que puedas configurar los criterios de consulta. Por ejemplo, imagina la siguiente query SQL, que devuelve los pacientes nacidos en 2007:

SELECT * FROM pacientes WHERE DATE_PART('year', fecha_nacimiento) = 2007

El problema de esta query es que solo devuelve datos de las personas nacidas en 2007. Si quieres cambiar el año para que devuelva otro, tendrías que modificar la consulta.

Ahora imagina que quieres directamente que ese año venga de una variable, como puede ser lo que obtienes tras preguntarlo con un menú a la persona que usa el programa, o bien en un servicio web como parte de un parámetro enviado desde el navegador web. Es posible que estés pensando en un código como el siguiente:

int anio = 2007;
String sql = "SELECT * FROM pacientes WHERE DATE_PART('year', fecha_nacimiento) = " + anio; // ☢️
ResultSet rs = stmt.executeQuery(sql);

Pero lamento decirte que este código es inseguro. Se trata de una vulnerabilidad muy común conocida como inyección SQL. Si permites a cualquier persona, programa o servicio modificar directamente lo que se manda en tu consulta SQL, eventualmente algo o alguien intentará enviar un valor inválido o peligroso. Puede que alguien intente pasar como año -1. O peor aún, puede que alguien intente pasar como año 2007; DROP TABLE usuarios. (Ah ya, ¿cómo olvidar a la generación drop?)

Es importante limpiar los parámetros recibidos de fuentes no confiables antes de pasarlas a una query SQL. Y deberías asumir que todo es una fuente desconfiable. Así que deberías limpiar cualquier query que acepte parámetros externos para evitar tener agujeros de seguridad en tu problema.

PreparedStatement es un tipo de statement que hace esto automáticamente por ti. Una sentencia preparada se compone en dos etapas:

  1. Primero fabricamos la plantilla con la query SQL que querríamos tirar, pero sin especificar ningún parámetro, solamente señalamos los lugares donde irán los parámetros.
  2. Después, por separado, especificamos los parámetros con su tipo, para que Java los coloque donde deben ir, de forma limpia.

Se trata de que Java haga de forma segura lo que tal vez alguien olvide hacer de forma segura.

Para usar una sentencia de este tipo, en vez del método Connection.createStatement(), usaremos el método Connection.prepareStatement, que acepta como cadena de caracteres la plantilla de la query SQL que vayamos a preparar.

La query SQL preparada será una cadena de caracteres donde cada parámetro que vayamos a inyectar será sustituido por el símbolo ?. Por ejemplo:

String sql = "SELECT * FROM pacientes WHERE DATE_PART('year', fecha_nacimiento) = ?";
PreparedStatement stmt = conn.prepareStatement(sql);

Puedes poner varios argumentos, si los necesitas. En este caso, aceptamos desde fuera tanto nombre como apellido:

String sql = "SELECT * FROM doctores WHERE nombre = ? AND apellido = ?";
PreparedStatement stmt = conn.prepareStatement(sql);

El método prepareStatement te devuelve un objeto de tipo PreparedStatement, que es como un Statement, pero tiene métodos adicionales que nos permiten terminar la query introduciendo los parámetros que quedan. Estos métodos son como los get que tenía un ResultSet, pero ahora son set:

  • setString() para establecer un string.
  • setInt() para asignar un valor numérico entero.
  • setBoolean() para colocar un booleano.
  • setDate() para poner una fecha usando la clase tradicional de Java Date (de nuevo, puede que este método se te quede antiguo si quieres pasarle un LocalDate o un LocalTime, pero hay formas de arreglar esto).

En este caso, tienes que especificar el número de placeholder (o sea, interrogación) que quieres cambiar, y el propio valor a cambiar.

Por ejemplo, vamos a intentar cambiar los setters correspondientes para declarar el año en la query:

int anio = 2007;
String sql = "SELECT * FROM pacientes WHERE DATE_PART('year', fecha_nacimiento) = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
  pstmt.setInt(1, anio);
} catch (SQLException e) {
  // Por favor, no te olvides de mi...
  e.printStackTrace();
}

En este otro caso, voy a poner los distintos criterios de búsqueda a una query más compleja donde tengo que especificar nombre, apellidos y año de contratación:

String sql = "SELECT * FROM doctores WHERE nombre = ? AND apellido = ? AND anio_cont = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
  pstmt.setString(1, "Patricia"); // nombre
  pstmt.setString(2, "Gonzalez"); // apellido
  pstmt.setInt(3, 2023); // anio_cont
} catch (SQLException e) {
  // Por favor, no te olvides de mi...
  e.printStackTrace();
}

Una vez hayas fabricado esta query, puedes usar métodos como execute() o executeQuery() para lanzar la query parametrizada como cualquier otro statement:

int anio = 2007;
String sql = "SELECT * FROM pacientes WHERE DATE_PART('year', fecha_nacimiento) = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
  pstmt.setInt(1, anio);
  try (ResultSet rs = pstmt.executeQuery()) {
    while (rs.next()) {
      String nombre = rs.getString("nombre");
      String apellido = rs.getString("apellido");
      LocalDate fechaNac = rs.getDate("fecha_nacimiento").toLocalDate();
      System.out.println(nombre + " " + apellido + " - " + fechaNac);
    }
  }
} catch (SQLException e) {
  // Por favor, no te olvides de mi...
  e.printStackTrace();
}

Una última cosa. Al igual que pasaba con los getters de un ResultSet, un driver de base de datos podría extender los tipos de datos soportados. Por ejemplo, PostgreSQL te permite asignar un LocalDate en un prepared statement usando el método setObject():

LocalDate fechaNac = obtenerFecha(); // como sea.
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM pacientes WHERE fecha_nac < ?");
stmt.setObject(1, fechaNac);
stmt.executeQuery();
// ...
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