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:
- 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.
- 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 JavaDate(de nuevo, puede que este método se te quede antiguo si quieres pasarle unLocalDateo unLocalTime, 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();
// ...