martes, 7 de junio de 2011

Ejecutar un archivo .sql desde PHP

Hace poco debí enfrentarme a este problema en el que debía ejecutar una actualización de un sistema, dentro de la actualización se hacían modificaciones a la estructura de la base de datos MySQL y no necesariamente se ejecutaba una consulta por actualización, sino que las consultas podían ser muchas.

Otro punto a detallar es que todas estas consultas venían dadas en un archivo .sql, porque podía ser usado además desde el CLI de MySQL o interfaces gráficas como MySQL Workbench o PHPMyAdmin. Y esto nos lleva a un problema, porque no podemos sólo leer todo el archivo y pasar el string obtenido como parametro a las funciones query de mysq, mysqli, o PDO ya que estos módulos ejecutan una consulta por vez.

Si el caso no hubiese sido el uso de .sql, una buena solución sería, por ejemplo, tener un array de PHP conteniendo todas las consultas separadas, incluir el archivo y luego iterar sobre el array ejecutando las consultas una a una.

Pero en el caso del .sql necesitamos separar las consultas que hay en el archivo para poder ejecutarlas una a una.
Una primera solución podría ser, sabiendo que en MySQL las consultas se separan por (;), leer el archivo a una variable, utilizar la función explode para separarlas por (;) y luego iterar sobre el arreglo resultante ejecutando las consultas una a una.

Ejemplo:



function ejecutarSQL($_rutaArchivo, $_conexionDB)
{
$queries = explode(';', file_get_contents($_rutaArchivo));
foreach($queries as $query)
{
if($query != '')
{
$_conexionDB->query($query): // Asumo un objeto conexión que ejecuta consultas
}
}
}


Esta aproximación sería correcta para archivos .sql como el siguiente:



ALTER TABLE tabla_a ADD COLUMN column_z TINYTEXT NOT NULL;
INSERT INTO tabla_b VALUES (1, 'a', 'valor');
CREATE TABLE tabla_c ( id INT auto_increment PRIMARY KEY, name VARCHAR(50) NOT NULL);

¿Pero qué pasa si el DBA que creo el archivo .sql correctamente agregó comentarios al archivo?



-- Agrego una columna para texto en tabla_a
ALTER TABLE tabla_a ADD COLUMN column_z TINYTEXT NOT NULL;
INSERT INTO tabla_b VALUES (1, 'a', 'valor');
-- Creo una tabla para mantener una lista de nombres
CREATE TABLE tabla_c ( id INT auto_increment PRIMARY KEY, nombre VARCHAR(50) NOT NULL);

Esto provocará que nuestro script falle, ya que encontrará en el string $query tokens que no pertenecen a una consulta correcta.
No es tan grave, se nos puede ocurrir utilizar expresiones regulares para identificar los comentarios y eliminarlos, y luego hacemos el explode. Solucionado.

Pero nada es fácil en la vida, porque: ¿qué sucede si en el archivo .sql nuestro querido amigo el DBA incluyó la creación de un stored procedure?
Esto nos quita toda posibilidad de usar nuestro script anterior, porque al crear un stored procedure necesitamos cambiar temporalmente el delimitador de las consultas, ya que las consultas dentro del stored procedure se delimitarán por (;).


Luego de darle unas vueltas a la idea llegué a la siguiente función, que quizás no sea la más óptima pero hasta ahora me funcionó en las situaciones en que debí usarlo.




function executeSqlScript($_db, $_fileName) {
$sql = file_get_contents($_fileName); // Leo el archivo
// Lo siguiente hace gran parte de la magia, nos devuelve todos los tokens no vacíos del archivo
$tokens = preg_split("/(--.*\s+|\s+|\/\*.*\*\/)/", $sql, null, PREG_SPLIT_NO_EMPTY);
$length = count($tokens);

$query = '';
$inSentence = false;
$curDelimiter = ";";
// Comienzo a recorrer el string
for($i = 0; $i < $length; $i++) {
$lower = strtolower($tokens[$i]);
$isStarter = in_array($lower, array( // Chequeo si el token actual es el comienzo de una consulta
'select', 'update', 'delete', 'insert',
'delimiter', 'create', 'alter', 'drop',
'call', 'set', 'use'
));

if($inSentence) { // Si estoy parseando una sentencia me fijo si lo que viene es un delimitador para terminar la consulta
if($tokens[$i] == $curDelimiter || substr(trim($tokens[$i]), -1*(strlen($curDelimiter))) == $curDelimiter) {
// Si terminamos el parseo ejecuto la consulta
$query .= str_replace($curDelimiter, '', $tokens[$i]); // Elimino el delimitador
$_db->query($query);
$query = ""; // Preparo la consulta para continuar con la siguiente sentencia
$tokens[$i] = '';
$inSentence = false;
}
}
else if($isStarter) { // Si hay que comenzar una consulta, verifico qué tipo de consulta es
// Si es delimitador, cambio el delimitador usado. No marco comienzo de secuencia porque el delimitador se encarga de eso en la próxima iteración
if($lower == 'delimiter' && isset($tokens[$i+1]))
$curDelimiter = $tokens[$i+1];
else
$inSentence = true; // Si no, comienzo una consulta
$query = "";
}
$query .= "{$tokens[$i]} "; // Voy acumulando los tokens en el string que contiene la consulta
}
}


La idea es separar el script sql en tokens utilizando una expresión regular que, además, está optimizada para que elimine los comentarios y utiliza un flag para descartar tokens vacios.
Luego se van recorriendo los tokens uno a uno identificando cuando hay un comienzo de sentencia sql y cuándo un final. Cuando la secuencia termina, se ejecuta y se vacia la cadena para recomenzar el ciclo

jueves, 7 de abril de 2011

Consultando mysql desde bash

No es muy común que necesitemos consultar bases de datos mysql desde un shellscript, pero en ciertas ocasiones (por ejemplo en un cron), podemos desear hacerlo. Ya sea para registrar un backup en una tabla de logs o tomar algún dato necesario para nuestro script desde una tabla, puede suceder que desde bash necesitemos consultar a mysql sin querer usar un script intermediario hecho en otro lenguaje, como perl o php.


¿Cómo accedemos?


Para acceder a la base de datos utilizaremos el CLI de mysql y lo iniciaremos con dos parámetros:

  • -B Hace que el cliente CLI de mysql trabaje en modo batch entonces, en lugar de devolver los datos en tablas dibujadas en ascii, devolverá los resultados separando los campos por tabuladores y los registros por nuevas líneas. Además no muestra prompt.

  • -N Evita que el CLI de mysql devuelva los headers con los nombres de los campos



Ejemplo



mysql -N -B

Nota:Me centraré en las consultas select, las consultas de modificación se ejecutan fácilmente utilizando el parámetro -e de mysql CLI y especificando la consulta de modificación.

Luego debemos asegurarnos que el CLI de mysql reciba nuestras consultas, pero como no las tipearemos nosotros en el prompt, tenemos que redirigir nuestras consultas a su entrada estándar para que las reciba, esto se puede hacer fácilmente mediante echo y haciendo entubamiento hacia mysql. Además le indico a mysql el nombre de la base de datos en la que debe consultar.



Ejemplo



echo "SELECT * FROM mi_tabla WHERE 1" | mysql -N -B mi_base_de_datos

El siguiente paso es hacer que el resultado quede guardado en una variable, para esto utilizamos las comillas invertidas.



Ejemplo



RESULTADOS=`echo "SELECT * FROM mi_tabla WHERE 1" | mysql -N -B mi_base_de_datos`

Ahora ya tenemos los resultados de nuestra consulta en una variable y lo único que debemos hacer es parsearlos. Para esto debemos hacer uso de la variable especial IFS de bash, la cual indica el separador que se utiliza para delimitar tokens al usar programas como for o awk, seteándole qué caracter debe usar para dividir las cadenas. En nuestro caso '\n' para separar registros y '\t' para separar campos.


En este post intentaré hacer todo lo más simplificado posible para que sea claro y entendible, por lo que indicaremos en la consulta qué campos deseamos de la tabla para saber su orden.



El ejemplo completo


Los comentarios explican lo que hace el script.

DBUSER=mi_user
DBPASS=mi_pass
RES=`echo "SELECT id, nombre FROM mi_tabla WHERE 1;" | mysql -N -B -u $DBUSER -p$DBPASS mi_db` # Ejecuto la consulta y la guardo en RES
OLDIFS="$IFS" # Respaldo el valor de IFS, no olvidar las comillas dobles
IFS=$'\n' # Seteo el valor de IFS que me sirve
for row in $RES ; # Itero por los registros porque for separará por \n
do
if [ ${#row} -gt 0 ]; then # Chequeo si tengo un registro
INNERIFS="$IFS" # Hago un respaldo temporal de IFS, no olvidar las comillas dobles
IFS="$OLDIFS" # Ahora separaré los campos

id=$(echo $row | awk '{print $1}') # Utilizo awk porque sé el orden de los campos: id es el 1 y nombre es el 2
nombre=$(echo $row | awk '{print $2}')

echo "El id es $id y el nombre es $nombre"
IFS="$INNERIFS" # Vuelvo a hacer que IFS separe por \n para la próxima iteración
fi
done

IFS="$OLDIFS" # Dejo IFS en su estado original.

Como se ve es bastante fácil acceder a los datos, utilizando este esquema se pueden parsear resultados aun más complejos, es cuestión de experimentar un poco.

sábado, 2 de abril de 2011

Javascript: Usar métodos de clases con setInterval y setTimeout

El objeto Window de Javascript posee dos métodos que nos permiten lanzar una acción luego de cierto tiempo:



  • setTimeout lanza la acción una vez luego del tiempo especificado

  • setInterval lanza la acción en intervalos regulares de duración especificada


Por ejemplo:



<script type="text/javascript">
setTimeout('alert("Hola mundo")'. 5000); // Muestra la alerta hola mundo luego de 5000 milisegundos (5 segundos)
setTimeout('alert("Hola mundo")'. 3000); // Muestra la alerta hola mundo cada 3000 milisegundos (3 segundos)
</script>


Pero un problema clásico que sucede cuando trabajamos con clases en Javascript es podemos querer usar setInterval o setTimeout en un método de nuestra clase que ejecute como acción un llamado a un método de su misma instancia. Una primera solución que se nos ocurriría podría ser esta:


<script type="text/javascript">
function MiClase()
{
this.miIntervalo = null;
}

MiClase.prototype.miMetodo = function()
{
alert("Hola Mundo");
}

MiClase.prototype.iniciarIntervalo = function()
{
this.intervalo = setInterval('this.miMetodo()', 3000); // Esto no funcionará: utilizo 'this' (local) en el global scope
}

var miInstancia = new MiClase();
miInstancia.iniciarIntervalo();
</script>


Sin embargo el código anterior no funcionará, generando un error.


¿Por qué?

Como setTimeout y setInterval son métodos de la clase window, ejecutan sus acciones en el scope global, pero 'this' no pertenece al scope global, sino que es local a la instancia de la clase. Al tratar de ejecutar la acción setInterval no encontrará el objeto llamado this y devolverá un error.




Una solución


Una forma de solucionar esto es mantener una colección de objetos por fuera de la clase y asignar a cada instancia una key, en el constructor hacemos que el objeto se guarde a sí mismo en la colección y luego, cuando le indiquemos a setInterval o setTimeout cuál es la acción a ejecutar, utilizamos la colección en lugar de 'this'



Ejemplo de la solución



<script type="text/javascript">
var objetosMiClase = new Object(); // Mi colección de instancias de MiClase

function MiClase()
{
this.miIntervalo = null;
var date = new Date();
this.key = date.getTime() + date.getMilliseconds(); // Genero una key, se pueden usar otras técnicas
objetosMiClase[this.key] = this; // La instancia se guarda a sí misma en la colección
}

MiClase.prototype.miMetodo = function()
{
alert("Hola Mundo");
}

MiClase.prototype.iniciarIntervalo = function()
{
this.intervalo = setInterval('objetosMiClase[' + this.key + '].miMetodo()', 3000); // Funciona! utilizo un objeto del global scope en lugar de 'this'
}

var miInstancia = new MiClase();
miInstancia.iniciarIntervalo();
</script>


Esta es una solución rápida al problema de intentar llamar un objeto de la misma instancia utilizanso setInterval o setTimeout

Epistemomaniáticos ©Template Blogger Green by Dicas Blogger.

TOPO