Saltar a contenido

🧠 Mejora del rendimiento

Otro aspecto importante que mide la calidad de las aplicaciones es la eficiencia con la que se consigue comunicar con el SGBD. Para optimizar la conexión es importante reconocer qué procesos pueden actuar de cuello de botella y bajo qué circunstancias o qué otras agilizan las respuestas de los SGBD.

  1. Analizar la petición de conexión a un SGBD porque se trata de un proceso costoso pero inevitable que hay que considerar.

  2. Estudiar las sentencias predefinidas (PreparedStatement), porqué su uso facilita la creación de datos clave e índices temporales de modo que sea posible anticiparse a la demanda o disponer de los datos de forma mucho más rápida.

🪐 Ciclo de vida de una conexión

El establecimiento de una conexión es un procedimiento bastante lento, tanto en la parte cliente como la parte servidor. En la parte cliente, DriverManager debe descubrir el controlador correcto de entre todos los que tenga que gestionar. La mayoría de veces las aplicaciones trabajarán sólo con un único controlador, pero hay que tener en cuenta que DriverManager no conoce a priori qué URL de conexión corresponde a cada controlador, y para averiguarlo envía una petición de conexión a cada controlador que tenga registrado, el controlador que no le devuelve error será el correcto.

En el lado servidor, se creará un contexto específico y se habilitarán un conjunto de recursos para cada cliente conectado. Es decir, que durante la petición de conexión del SGBD debe gastar un tiempo considerable antes de no dejar operativa la comunicación cliente-servidor.

Este elevado gasto de tiempo concentrado en el momento de la petición de conexión nos hace plantear si podemos considerar ineficiente abrir y cerrar la conexión cada vez que tengamos que ejecutar una sentencia SQL, como hemos hecho hasta ahora. Desafortunadamente no hay una única respuesta, sino que depende de la frecuencia de uso de la conexión y el número de conexiones contra un mismo SGBD coexistiendo al mismo tiempo.

jdbc

Como en todo, se trata de encontrar {==el punto de equilibrio entre la cantidad de recursos empleados por conexión y la rentabilidad que se saca en mantenerlas abiertas==}.

  • Si el número de clientes, y por tanto de conexiones, es bajo y la frecuencia de uso es alta, será preferible mantener las conexiones abiertas mucho tiempo.

  • Por el contrario, si el número de conexiones es muy alto y el uso infrecuente, lo que será preferible será abrir y cerrar la conexión cada vez que se necesite.

Mientras tanto, habrá una multitud de casos en que la solución consistirá en mantener las conexiones abiertas, pero no permanentemente. Se puede dar un tiempo de vida a cada conexión, o bien cerrarlas después de restar inactiva una cantidad determinada de tiempo, o se puede usar el criterio de mantener un número máximo de conexiones abiertas, cerrando las más antiguas o las más inactivas cuando se sobrepase el límite.

😶‍🌫️ Nota

Por otra parte, hay que tener en cuenta también que una misma aplicación puede trabajar con varias conexiones simultáneamente para incrementar la eficiencia. Cada conexión abre un hilo de ejecución independiente, por lo que es posible el envío simultáneo de peticiones.

⚡ Sentencias predefinidas

PreparedStatement presenta ventajas sobre su antecesor Statement cuando tengamos que trabajar con sentencias que haya que ejecutar varias veces.

⭐Statement⭐ ⭐PreparedStatement⭐
Statement normalmente se analiza (parsea) y se ejecuta cada vez que se usa. PreparedStatement se analiza una sola vez y se ejecuta repetidamente con diferentes parámetros.
Un Statement es una sentencia SQL estática. No soporta parámetros. Un PreparedStatement es una sentencia SQL dinámica. Sí soporta parámetros.
Es más lento porque cada vez la sentencia se analiza y se ejecuta de nuevo. Es más rápido porque se precompila una vez y se ejecuta con diferentes parámetros.
Statement verifica los metadatos contra la base de datos cada vez. PreparedStatement verifica los metadatos contra la base de datos solo una vez.
Si queremos ejecutar una sentencia SQL una sola vez, se recomienda usar Statement. Si queremos ejecutar sentencias SQL repetidamente, se recomienda usar PreparedStatement.

La razón es que cualquier sentencia SQL, cuando se envía el SGBD será compilada antes de ser ejecutada. Usando un objeto Statement, cada vez que hacemos una ejecución de una sentencia, ya sea vía executeUpdate o bien vía executeQuery, el SGBD la compilará, ya que le llegará en forma de cadena de caracteres.

En cambio, al PreparedStament la sentencia nunca varía y por lo tanto se puede compilar y almacenar dentro del mismo objeto, por lo que las siguientes veces que se ejecute no habrá que compilarla. Esto reducirá sensiblemente el tiempo de ejecución. La parametrización, además, ayuda a crear sentencias muy genéricas que se puedan reutilizar fácilmente.

En algunos sistemas gestores, además, usar PreparedStatement puede llegar a suponer más ventajas, ya que utilizan la secuencia de bytes de la sentencia para detectar si se trata de una sentencia nueva o ya se ha servido con anterioridad. De esta manera se propicia que el sistema almacene las respuestas en la caché, de manera que se puedan entregar de forma más rápida.

Importante 🤔

El uso de Statement en JDBC debería limitarse exclusivamente a sentencias DDL (ALTER, CREATE, GRANT, etc.), ya que estos son los únicos tipos de sentencias que no pueden aceptar variables enlazadas (bind variables).

Para todos los demás tipos de sentencias (DML y consultas), se deberían utilizar PreparedStatement, porque son los tipos de sentencias que sí aceptan variables enlazadas.

Esto es un hecho, una regla, una ley: usa PreparedStatement en todas partes. Usa Statement casi en ningún sitio.

📝 Ejemplos de problemas con Statement

  • 1. Statement acepta cadenas como consultas SQL. Por lo tanto, el código se vuelve menos legible cuando concatenamos cadenas SQL:
public void insert(PersonEntity personEntity) {
    String query = "INSERT INTO persons(id, name, age, email) VALUES(" + personEntity.getId() 
                    + ", '" + personEntity.getName() + ", '" + personEntity.getAge() 
                    + ", '" + personEntity.getEmail() + "')";

    Statement statement = connection.createStatement();
    statement.executeUpdate(query);
}
  • 2. Es vulnerable a la inyección de SQL.
public void check(String name) {
    String query = "SELECT * FROM users WHERE name = '" + name + "';";

    Statement statement = connection.createStatement();
    statement.executeUpdate(query);
}

Si un usuario malintencionado escribe como nombre de usuario a consultar:

Alicia'; DROP TABLE usuarios; SELECT * FROM datos WHERE nombre LIKE '%

Se generaría la siguiente consulta SQL, (el color verde es lo que pretende el programador, el azul es el dato, y el rojo, el código SQL inyectado):

jdbc

En la base de datos se ejecutaría la consulta en el orden dado, se seleccionarían todos los registros con el nombre 'Alicia', se borraría la tabla 'usuarios' y finalmente se seleccionaría toda la tabla "datos", que no debería estar disponible para los usuarios web comunes.'