Archivo Mensual de Mayo, 2008

Simulando DLookup fuera de Access (III).

En los dos posts anteriores he mostrado maneras de simular un DLookup en una base de datos MySQL mediante stored procedures y stored functions (primera aproximación y versión mejorada). No obstante, ha quedado claro que hacerlo así aunque puede resultar útil tiene sus limitaciones. Es por ello que normalmente prefiero utilizar un DLookup llevado a cabo desde la parte de la aplicación. En mi caso esto equivale a realizarlo en VisualBasic.NET. Y sin más preámbulos veamos el código de la función:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080530
' Description: Function to simulate Microsoft Access DLookup. 
'--------------------------------------------------------------------
Public Function DLookup(ByVal Field As String, _
                        ByVal Table As String, _
               Optional ByVal Condition As String = "TRUE") _
                        As Object
    Try
        'Creating SQL string.
        Dim SQL As String
        SQL = "SELECT " & Field _
            & " FROM " & Table _
            & " WHERE " & Condition
        'Filling dataset with desired value. 
        Dim DS As New DataSet
        Dim DA As New MySqlDataAdapter(SQL, CONNECTION_STRING)
        DA.Fill(DS, "anyname")
        'Returning value or Null.
        If Not IsDBNull(DS.Tables("anyname").Rows(0).Item(0)) Then
            Return DS.Tables("anyname").Rows(0).Item(0)
        Else
            Dim X As Object = Convert.DBNull
            Return X
        End If
    Catch
        'If error happens, returning Null.
        Dim X As Object = Convert.DBNull
        Return X
    End Try
End Function

Cabe aclarar que CONNECTION_STRING será una constante definida en algún punto de la aplicación que recogerá la cadena de conexión para nuestra base de datos. En mi caso es algo como:

Database = nombre_bbdd; _
Data Source = localhost; _
User ID = root; _
Password = mi_password

Relacionado con cadenas de conexión, recomendado darse un paseo por ConnectionStrings.com.

Yo estoy trabajando con una base de datos MySQL, por eso el DataAdapter es en realidad un MySqlDataAdapter, pero el funcionamiento sería análogo con cualquier otra base de datos y utilizando un objeto DataAdapter convencional. Lo único es que si se trabaja con MySQL es necesario previamente haber agregado la referencia correspondiente al proyecto, y para mayor comodidad en temas de nomenclatura importar el espacio de nombres correspondiente en la parte superior de la clase:

Imports MySql.Data.MySqlClient

Respecto a la función, poco que explicar. Pasarle el campo y la tabla deseados y de manera opcional una condición en formato SQL sin el WHERE. Si no se desea condición se pueden informar sólo dos argumentos, en esta ocasión no es necesario enviar una cadena vacía ni nada por el estilo. Además, el valor que nos devuelve la función es de tipo Object, así que no tendremos ningún problema con el tipo de dato devuelto, nos admitirá cualquier tipo de campo que quiera que sea el campo en cuestión de la tabla en cuestión. En caso de producirse algún error (nombre de campo o tabla mal escritos, conexión a base de datos fallida…) o si no se halla ningún registro, la función nos devolverá un valor nulo, con lo cual desde código podremos recogerlo sin problemas y actuar en consecuencia. :-)

Simulando DLookup fuera de Access (II).

En el anterior post expuse una manera de realizar un DLookup en MySQL mediante stored procedures y stored functions y comenté que para el siguiente post me reservaba un modo de hacer lo mismo desde .NET. No obstante me permito intercalar otra manera de realizar otra vez el DLookup desde MySQL algo más sencilla que la primera aunque para recuperar el valor necesitaremos igualmente una doble consulta. Veamos cómo queda el código en esta ocasión:

#--------------------------------------------------------------------
# Author:      Albert Mata (www.albertmata.net)
# Date:        20080527
# Description: MySQL procedure to simulate Microsoft Access 
#              function DLookup. 
#--------------------------------------------------------------------

#--------------------------------------------------------------------
# Returns DLookup value using fourth parameter.
#--------------------------------------------------------------------
DROP PROCEDURE IF EXISTS DLookup;
DELIMITER //
CREATE PROCEDURE DLookup(IN campo VARCHAR(7),
                         IN tabla VARCHAR(14),
                         IN condicion VARCHAR(250),
                         OUT valor VARCHAR(250))
BEGIN

DROP TABLE IF EXISTS tbl_dlookupaux;
CREATE TABLE tbl_dlookupaux (tbl_res VARCHAR(250));
SET @query = CONCAT('INSERT INTO tbl_dlookupaux SELECT ',
                    campo, ' FROM ', tabla, ' WHERE ',
                    IF(ISNULL(condicion),'TRUE',condicion),
                    ' LIMIT 1');

PREPARE running FROM @query;
EXECUTE running;
DEALLOCATE PREPARE running;
SELECT tbl_res INTO valor FROM tbl_dlookupaux;
DROP TABLE IF EXISTS tbl_dlookupaux;

END
//
DELIMITER ;

Esta vez utilizo únicamente una stored procedure, pero lo hago tomando parámetros tanto de entrada (campo, tabla y condicion) como de salida (valor). Con esto lo que hago es almacenar el resultado en el parámetro valor en lugar de almacenarlo provisionalmente en una tabla para luego recuperarlo de allí (bueno, internamente la stored procedure sí almacena en una tabla temporal, pero en tanto que la misma stored procedure la borra este proceso es transparente al usuario).

Recuerdo que partíamos de una tabla cty_nicecities con esta estructura:

+--------+-----------+---------+
| cty_id | cty_nam   | cty_cou |
+--------+-----------+---------+
|      1 | Paris     | France  | 
|      2 | Rome      | Italy   | 
|      3 | Frankfurt | Germany | 
|      4 | Dortmund  | Germany | 
|      5 | Milan     | Italy   | 
+--------+-----------+---------+

La llamada debe ahora realizarse así:

CALL DLookup ('cty_nam', 'cty_nicecities', 'cty_id = 4', @my_city);
SELECT @my_city;

Almacenamos pues el valor en una variable (@my_city) y después lo rescatamos:

+----------+
| @my_city |
+----------+
| Dortmund |
+----------+

Tal vez este método es más limpio que el anterior. De todos modos para la próxima entrega, esta vez sí, postearé el DLookup mediante código en .NET, que será bastante más potente al incorporar gestión de errores y no tener limitaciones fruto de tener que especificar el tipo de datos concreto que esperamos obtener. No obstante un DLookup en MySQL se ejecutará íntegramente en el servidor, mientras que uno en .NET tendrá una parte ejecutándose en la máquina cliente, así que dependerá del gusto de cada cual escoger uno u otro. Yo me inclino por la versión en .NET…

Simulando DLookup fuera de Access (I).

Para todos aquellos que empezamos en bases de datos con Access y que programamos aplicaciones con VBA, las funciones DCount, DMax y similares nos resultan de lo más útiles para obtener valores puntuales de una base de datos sin tener que recuperar explícitamente un recordset. La más útil de todas probablemente sea DLookup. La sintaxis resumida de DLookup es la siguiente (para una más detallada explicación se puede consultar la propia ayuda de Access):

DLookup (nombre_campo, nombre_tabla, condicion_opcional)

Y nos devuelve el valor del campo correspondiente de la tabla indicada, en el primer registro que cumpla la condición opcionalmente pasada como parámetro.

Pues bien, como ya he comentado en posts anteriores, la base de datos con la que actualmente estoy trabajando es MySQL, y por desgracia esta función tan útil no existe de manera predeterminada en MySQL. No obstante, ello no quiere decir que no podamos utilizar algo similar, sí que podemos, aunque vamos a tener que programar un poco antes. He aquí el código:

#--------------------------------------------------------------------
# Author:      Albert Mata (www.albertmata.net)
# Date:        20080527
# Description: MySQL procedure and function to simulate Microsoft
#              Access function DLookup. 
#--------------------------------------------------------------------

#--------------------------------------------------------------------
# Creates an auxiliar table to store the value.
#--------------------------------------------------------------------
DROP PROCEDURE IF EXISTS PreDLookupAux;
DELIMITER //
CREATE PROCEDURE PreDLookup(campo VARCHAR(7), 
                            tabla VARCHAR(14), 
                            condicion VARCHAR(250))
BEGIN

DROP TABLE IF EXISTS tbl_dlookupaux;
CREATE TABLE tbl_dlookupaux (tbl_res VARCHAR(250));
SET @query = CONCAT('INSERT INTO tbl_dlookupaux SELECT ', 
                    campo, ' FROM ', tabla, ' WHERE ', 
                    IF(ISNULL(condicion),'TRUE',condicion), 
                    ' LIMIT 1');
PREPARE running FROM @query;
EXECUTE running;
DEALLOCATE PREPARE running;

END
//
DELIMITER ; 

#--------------------------------------------------------------------
# Gets the value from auxiliar table created before.
#--------------------------------------------------------------------
DROP FUNCTION IF EXISTS DLookup;
DELIMITER //
CREATE FUNCTION DLookup() RETURNS VARCHAR(250)
BEGIN

DECLARE aux VARCHAR(250); 
SET aux := (SELECT tbl_res FROM tbl_dlookupaux);
RETURN aux;

END
//
DELIMITER ;

Hay unas cuantas cosas que explicar:

1) Hasta la versión 5.0.45 de MySQL (la última GA cuando escribo esto y por tanto la de uso recomendado) las stored functions no permiten hacer ciertas cosas que harían mucho más fácil crear el DLookup en un solo paso, por eso se tiene que crear en un doble paso.

2) En el primer paso creamos una tabla auxiliar con un único campo y le insertamos un único registro con el valor que sale de la combinación campo-tabla-condicion que le hemos pasado. En el segundo paso nos devuelve ese valor que hemos almacenado temporalmente en la tabla auxiliar.

3) He puesto una longitud de 7 caracteres para el nombre del campo y de 14 para el de la tabla porque son las longitudes que tienen siempre mis nombres de campos y tablas, no obstante se puede modificar al gusto.

4) En cambio en tipo de campo de salida he puesto VARCHAR(250) por intentar ser algo genérico. Con esta definición nos devolverá sin problemas fechas y números, pero hay que tener en cuenta esta definición por si se utiliza DLookup con tipos de datos que no tengan cabida en un genérico VARCHAR(250).

5) Si no se desea pasar ningúna condición se puede establecer a cadena vacía ('') o bien a NULL, pero una stored function de MySQL no permite poner argumentos como de introducción opcional.

Veámoslo en funcionamiento. Tengo una tabla cty_nicecities con esta estructura:

+--------+-----------+---------+
| cty_id | cty_nam   | cty_cou |
+--------+-----------+---------+
|      1 | Paris     | France  | 
|      2 | Rome      | Italy   | 
|      3 | Frankfurt | Germany | 
|      4 | Dortmund  | Germany | 
|      5 | Milan     | Italy   | 
+--------+-----------+---------+

Y puedo utilizar mi DLookup particular de esta manera:

CALL PreDLookup ('cty_nam', 'cty_nicecities', 'cty_id = 4');
SELECT DLookUp();

Para obtener:

+-----------+
| DLookup() |
+-----------+
| Dortmund  | 
+-----------+

Y otro ejemplo, si pongo:

CALL PreDLookup ('cty_nam', 'cty_nicecities', 'cty_cou = \'Italy\'');
SELECT DLookUp();

Obtengo:

+-----------+
| DLookup() |
+-----------+
| Rome      | 
+-----------+

Funciona aceptablemente bien y nos puede resultar muy útil cuando queramos utilizar la función DLookup en MySQL, sin embargo no es una solución idonea. Quizá cuando MySQL elimine las restricciones comentadas anteriormente se pueda crear una mejor solución. Por lo pronto en el próximo post subiré una solución infinitamente más elegante y potente para hacer lo mismo sólo que en lugar de hacerlo desde el lado del servidor MySQL lo haremos desde el lado de una aplicación programada en .NET. ;-)

Actualización.

En el siguiente post hay una versión distinta más breve y elegante del DLookup en MySQL.

Servidor de bolsillo con WOS Portable.

En el proyecto en el que actualmente estoy trabajando, decidimos utilizar una base de datos MySQL por su bajo coste y excelente rendimiento. De hecho fue una recomendación mía que la Dirección aceptó, por lo cual celebro que de momento nos esté dando el resultado tan bueno que nos está dando.

Así pues en mi portátil de trabajo tengo instalado un servidor MySQL en local. En él hago toda la fase de desarrollo antes de pasar tablas, triggers, procedimientos y demás al servidor MySQL del servidor de la empresa. Pero en ocasiones no tengo mi portátil a mano y me gustaría poder probar algunas cosas en otro PC. Hasta ahora tenía que desestimar la idea porque andar instalando el servidor MySQL en ordenadores ajenos es inviable. Pero hace poco he descubierto WOS Portable. Una maravilla. WOS Portable es un software que se instala en un pendrive cualquiera (bueno, dicen que algunos modelos dan problemas, pero parecen ser los menos y en cualquier caso a mí no me ha ocurrido) y nos permite que al conectar después ese pendrive en cualquier PC con Windows y ejecutar el archivo wos.exe automáticamente dispondremos de un servidor Apache + PHP + MySQL funcionando perfectamente, de tal manera que si en un navegador escribimos:

http://localhost

Obtendremos la página index.php que tenemos alojada en el pendrive (en el directorio www). Y si en una consola escribimos:

mysql -u root

Tendremos un servidor MySQL a nuestra entera disposición. (ver nota 1)

Personalmente este software me ha parecido una maravilla y creo que es muy útil para desarrolladores que solemos trabajar con más de un PC, ya que así nuestros datos pueden estar siempre en el pendrive y no tenemos que andar backupeándolos y restaurándolos constantemente. También se me ha ocurrido que puede resultar extremadamente útil como sistema de seguridad. Me explico: en mi empresa las aplicaciones funcionan como he dicho contra una base de datos MySQL alojada en el servidor. Si éste sufriera un accidente o cualquier imprevisto, la actividad se paralizaría y hasta que se restaurara el correcto funcionamiento del servidor podrían pasar horas o incluso días. En cambio ahora tenemos un pendrive con WOS Portable y cada X días le restauramos una copia de seguridad de la base de datos productiva. Con ello, ante cualquier eventualidad nos basta con conectar este pendrive, hacer un doble clic y ya tenemos de nuevo las aplicaciones funcionando. Realmente genial y recomendable.

Pues bien, WOS Portable se puede descargar libremente (es GNU-GPL) desde su sitio web. Para ello la única peculiaridad es que en lugar de bajarnos un archivo estándar podemos configurar qué aplicaciones extras (Joomla, Mambo, Wordpress, etc) queremos incorporar al paquete de instalación. Lo básico es Apache + PHP + MySQL, y personalmente no recomiendo añadirle mucha cosa más desde un inicio, puesto que siempre estamos a tiempo de hacer actualizaciones con nuevos paquetes. No obstante, yo sí le añadí el phpMyAdmin porque considero que en ocasiones resulta muy útil.

Una vez seleccionados los componentes nos generarán un archivo comprimido personalizado que sólo tendremos que bajarnos, descomprimirlo en el pendrive y ejecutar el fichero wos.exe. Con eso se llevará a cabo un proceso de configuración que tarda un ratito pero que no tiene complicación alguna. ;-)

Nota 1.

Para que esta sentencia funcione tenemos que tener un cliente MySQL (un archivo mysql.exe) en el PC y la ruta donde éste se encuentra guardada en la variable del sistema Path. Si no es así, cosa probable si no estamos en nuestro PC, siempre podemos ir primero a buscar el cliente:

F:
cd mysql/bin
mysql -u root

Copias de seguridad con ROBOCOPY.

Imagino que a todos los que nos dedicamos a esto nos ocurre que constantemente estamos creando nuevos archivos, nuevas versiones del mismo código, nuevos ficheros que sustituyen al anterior… y vamos haciendo copias de seguridad aquí y allá. Al menos a mí me pasa. Y me pasa también que termino teniendo versiones distintas de una misma cosa en el ordenador portátil y en el de sobremesa, en el pendrive, en el disco duro externo, en el servidor de la empresa… ¡no fuera caso que se perdiera el trabajo!

Pues hace unos días me pregunté si no habría alguna manera fácil y rápida de mantener todo eso un poco más sincronizado. Y sí, claro, la hay. De buenas a primeras seguro que hay un montón de aplicaciones en Softonic que nos permiten hacer copias de seguridad incrementales de manera superprofesional. Pero yo buscaba algo más simple, algo más rápido. Y encontré Robocopy.

Robocopy es una herramienta para copiado de archivos y directorios que nos viene incorporada con Windows Vista (¡algo bueno tenía que tener Vista!). Hace cosas parecidas a las que se podrían hacer con Xcopy, pero si en una linea de comando escribimos:

xcopy /?

El propio sistema nos informa que Xcopy es obsoleto y que utilicemos Robocopy en su lugar. Probemos ahora a escribir:

robocopy /?

Recibimos una explicación de todos los posibles argumentos que podemos utilizar con Robocopy, algo que con alguna información extra añadida también podemos encontrar en la Wikipedia. Sin embargo para un uso simple, rápido y funcional de Robocopy nos basta con crear un archivo .bat tal que así:

@echo off
robocopy C:\Proyectos \\99.34.63.19\AM_Proyectos /E /B
robocopy C:\VBnet \\99.34.63.19\AM_VBnet /E /B
robocopy C:\2006 F:\2006 /E /B
robocopy C:\2007 F:\2007 /E /B
robocopy C:\2008 F:\2008 /E /B

Después basta con ejecutar el .bat y directamente se nos crea una copia incremental de los cinco directorios de la unidad C que hemos especificado en las cinco rutas destino que hemos indicado. Así, los dos primeros se copiarán en sus respectivas carpetas en el servidor, mientras que los otros tres irán a parar a mi pendrive USB. Los parámetros /E y /B nos permiten precisamente hacer eso.

Insisto en que es una copia incremental. Esto significa que la primera vez que se ejecuta se demora unos minutos en función de la cantidad de información a respaldar, pero las siguientes sólo copiará los ficheros modificados, con lo que en principio es bastante rápido. No borra de destino los elementos que ya no forman parte de los directorios de origen, pero es algo que también se puede configurar con el parámetro /PURGE si se desea.

Nota.

Aunque Robocopy viene con Windows Vista, se puede también añadir a sistemas Windows anteriores mediante las Windows Server 2003 Resource Kit Tools que funcionan como su propio nombre indica para Windows Server 2003 y también para Windows XP.

MySQL o el misterio de los panes y los peces.

Recientemente perdí más de media hora intentando descubrir porque MySQL me devolvía un resultado a priori carente de sentido al efectuar una simple diferencia entre dos números enteros. Para reproducir el problema utilizaré una tabla simple reducida únicamente a los campos imprescindibles para mostrar el problema. Creo pues la tabla stk_stockmonth con esta estructura:

+---------+----------------------+------+-----+---------+-------+
| Field   | Type                 | Null | Key | Default | Extra |
+---------+----------------------+------+-----+---------+-------+
| stk_ini | smallint(5) unsigned | YES  |     | NULL    |       | 
| stk_inc | smallint(5) unsigned | YES  |     | NULL    |       | 
| stk_out | smallint(5) unsigned | YES  |     | NULL    |       | 
+---------+----------------------+------+-----+---------+-------+

Se trata de una tabla donde guardo el stock de un material. En el campo stk_ini recojo el stock inicial, en stk_inc las entradas de stock que preveo para un período (incomings) y en stk_out las salidas de stock para ese mismo período (outgoings). Después ejecuto la consulta con el fin de obtener el stock final previsto para ese período como diferencia del inicial más las entradas menos las salidas previstas:

SELECT 
    stk_ini, 
    stk_inc, 
    stk_out, 
    (stk_ini + stk_inc - stk_out) AS stk_fin 
FROM 
    stk_stockmonth;

Pues bien, aquí es donde obtenía un resultado inesperado:

+---------+---------+---------+----------------------+
| stk_ini | stk_inc | stk_out | stk_fin              |
+---------+---------+---------+----------------------+
|     100 |      50 |     180 | 18446744073709551586 | 
+---------+---------+---------+----------------------+

¿Cómo que 18446…? Simplemente tenía que devolverme -30 (entraría en rotura de stock, sí, pero ese es otro tema ;-) ), ¿dónde estaba el problema pues? ¿Cómo demonios hemos obtenido este astronómico stock digno del más puro milagro al estilo de la multiplicación de los panes y los peces? Fácil. El motivo reside en los tipos de datos. Como los tres campos con los que está operando son smallint(5) unsigned, MySQL asume que el resultado también será un unsigned, cuando en este caso no tiene por qué serlo. La solución pasaría pues por cambiar los tipos de datos y dejarlos en signed, pero no me convence porque realmente para mí esos datos tienen que ser unsigned y no quiero reducir a la mitad el número máximo que puedo almacenar en esos campos por añadirle un signo que no necesito.

Pues aquí está la solución que encontré:

SELECT 
    stk_ini, 
    stk_inc, 
    stk_out, 
    CAST((stk_ini + stk_inc - stk_out) AS SIGNED) AS stk_fin 
FROM 
    stk_stockmonth;

Haciendo un casting a número con signo, MySQL ya nos muestra el resultado esperado:

+---------+---------+---------+---------+
| stk_ini | stk_inc | stk_out | stk_fin |
+---------+---------+---------+---------+
|     100 |      50 |     180 |     -30 | 
+---------+---------+---------+---------+

En principio es lógico que MySQL asuma que si en la operación sólo participan números unsigned el resultado también lo vaya a ser, pero me pregunto si no podría estar algo más optimizado para prever que pudiera no ser así y efectuar un casting de manera automática.




Creative Commons License
El blog de Albert Mata by Albert Mata is licensed under a Creative Commons Reconocimiento-Compartir bajo la misma licencia 2.5 España License.