Saltar la navegación

3.1.5.- Consultas preparadas.

Imagen de un logo conteniendo una comilla simple, un punt y coma y dos guiones en un fondo azul.
Troy Hunt (Dominio público)


Cada vez que se envía una consulta al servidor, éste debe analizarla antes de ejecutarla. Algunas sentencias SQL, como las que insertan valores en una tabla, deben repetirse de forma habitual en un programa. Para acelerar este proceso, MySQL admite consultas preparadas. Estas consultas se almacenan en el servidor listas para ser ejecutadas cuando sea necesario.

Por otra parte existe un riesgo de seguridad muy importante al usar formularios para insertar, consultar, modificar, borrar datos en una base de datos, la "inyección SQL" . Unos de los métodos que se recomiendan para evitar este tipo de ataques es precisamente usar consultas parametrizadas ya que los valores de los parámetros, son transmitidos después, usando un protocolo diferente y no necesitan ser escapados.

Para trabajar con consultas preparadas con la extensión MySQLi de PHP, debes utilizar la clase mysqli_stmt. Utilizando el método stmt_init de la clase mysqli (o la función mysqli_stmt_init) obtienes un objeto de dicha clase.

$conProyecto = new mysqli('localhost', 'gestor', 'secreto', 'proyecto');

$stmt = $conProyecto->stmt_init();

Los pasos que debes seguir para ejecutar una consulta preparada son:

  • Preparar la consulta en el servidor MySQL utilizando el método prepare (función mysqli_stmt_prepare).
  • Ejecutar la consulta, tantas veces como sea necesario, con el método execute (función mysqli_stmt_execute).
  • Una vez que ya no se necesita más, se debe ejecutar el método close (función mysqli_stmt_close).

Por ejemplo, para preparar y ejecutar una consulta que inserta un nuevo registro en la tabla familia:

$stmt = $conProyecto->stmt_init();
$stmt->prepare('INSERT INTO familias (cod, nombre) VALUES ("TABLET", "Tablet PC")');
$stmt->execute();
$stmt->close();
$conProyecto->close();

El problema que ya habrás observado, es que de poco sirve preparar una consulta de inserción de datos como la anterior, si los valores que inserta son siempre los mismos. Por este motivo las consultas preparadas admiten parámetros. Para preparar una consulta con parámetros, en lugar de poner los valores debes indicar con un signo de interrogación su posición dentro de la sentencia SQL.

$stmt->prepare('INSERT INTO familias (cod, nombre) VALUES (?, ?)');


Y antes de ejecutar la consulta tienes que utilizar el método bind_param (o la función mysqli_stmt_bind_param) para sustituir cada parámetro por su valor. El primer parámetro del método bind_param es una cadena de texto en la que cada carácter indica el tipo de un parámetro, según la siguiente tabla.

Caracteres indicativos del tipo de los parámetros en una consulta preparada.
Carácter. Tipo del parámetro.
i. Número entero.
i. Número real (doble precisión).
s. Cadena de texto.
b. Contenido en formato binario (BLOB).

En el caso anterior, si almacenas los valores a insertar en sendas variables, puedes hacer:

$stmt = $conProyecto->stmt_init();
$stmt->prepare('INSERT INTO familias (cod, nombre) VALUES (?, ?)');
$cod_producto = "TABLET";
$nombre_producto = "Tablet PC";
$stmt->bind_param('ss', $cod_producto, $nombre_producto);
$stmt->execute();
$stmt->close();
$conProyecto->close();

Cuando uses bind_param para enlazar los parámetros de una consulta preparada con sus respectivos valores, deberás usar siempre variables como en el ejemplo anterior. Si intentas utilizar literales, por ejemplo:

$stmt->bind_param('ss', 'TABLET', 'Tablet PC');  // Genera un error

Obtendrás un error. El motivo es que los parámetros del método bind_param se pasan por referencia. Aprenderás a usar paso de parámetros por referencia en una unidad posterior.

El método bind_param permite tener una consulta preparada en el servidor MySQL y ejecutarla tantas veces como quieras cambiando ciertos valores cada vez. Además, en el caso de las consultas que devuelven valores, se puede utilizar el método bind_result (función mysqli_stmt_bind_result) para asignar a variables los campos que se obtienen tras la ejecución. Utilizando el método fetch (mysqli_stmt_fetch) se recorren los registros devueltos. Observa el siguiente código:

$stmt = $conProyecto->stmt_init();
$stmt->prepare('SELECT producto, unidades FROM stocks WHERE unidades<2');
$stmt->execute();
$stmt->bind_result($producto, $unidades);
while($stmt->fetch()) {
    echo "<p>Producto $producto: $unidades unidades.</p>";
}
$stmt->close();
$conProyecto->close();

Recomendación

Tanto $stmt->prepare() como $stmt->execute() devuelven un dato de tipo booleano, podemos usar esto para controlar errores, fíjate en el ejemplo siguiente:

$stmt=$conProyecto->stmt_init();
$cod=1;
$consulta="select nombre from productos where id=?";
if(!($stmt->prepare($consulta))){
    echo "Se ha producido un error: " . $conProyecto->error();
    die();
}
$stmt->bind_param('i', $cod);
if(!$stmt->execute()){
     //error
}
. . .

Ejercicio resuelto

A partir de la página web obtenida en el ejercicio anterior, añade la opción de modificar el número de unidades del producto en cada una de las tiendas. Utiliza una consulta preparada para la actualización de registros en la tabla stocks. No es necesario tener en cuenta las tareas de inserción (no existían unidades anteriormente) y borrado (si el número final de unidades es cero).