13 de noviembre de 2012

Migrar Vistas Remotas a CursorAdapter

Para los que hemos desarrollado aplicaciones Cliente-Servidor, con nuestro querido Zorro, el uso de la tecnología de Vistas Remotas ha sido sin duda la primera opción. Bajo esta suposición, puede que nos hayamos sentido a gusto desarrollando aplicaciones hasta la versión 7, de allí en adelante sin embargo, VFP incorporó otros cambios sustanciales y específicamente en la administración y gestión de datos y que podríamos usar como alternativa: La tecnología CursorAdapter. Ahora toca contarles por que decidí cambiarme a esta última.

Una de las mayores dificultades que encontré en la versión 8 y 9 al trabajar con Vistas Remotas , fue la pésima gestión que tiene VFP al momento de gestionar la concurrencia y las conexiones en el servidor, hablando un poco más claro: Me refiero a que la propiedad Shareconection de las vistas remotas. Esta propiedad controla la apertura de una nueva conexión cada que se abre una vista remota en una determinada sesión de datos. Es decir si esta propiedad se establece como verdadera (.T.) , nos aseguraremos de que al abrir el primer cursor o vista remota generaremos un numero de conexión determinado para ella, y las vistas que se abran posteriormente compartirán este mismo hilo de conexión (Siempre y cuando que estas tengan la propiedad Shareconecction establecida a TRUE) , de esta manera tendremos varias vistas abiertas con una sola conexión. Al hacer esto tenemos asegurado lo siguiente:
  • Manejo eficiente de las conexiones al servidor
  • Mejor manejo de las Transacciones, en especial al actualizar las vistas con las funciones TABLEUPDATE(), SQLCOMMIT() y SQLROLLBACK()
  • Mejor administración de código
La mala noticia es que en la versión 9, no respeta el valor de la propiedad ShareConection, por lo que se abrirán tantas conexiones como vistas se abran.

Por esta razón, decido cambiarme a la tecnología CursorAdapter , pero esto significa cambiar algunas cosas en el código de las aplicaciones que hayamos escrito anteriormente, y ese el principal motivo de este artículo. En primer lugar debemos de tener en cuenta que CursorAdapter Tiene las propiedades y comportamientos que cualquier cursor o vista remota y aun más todavía. De modo que si logramos abrir Cursores derivados de CursorAdapter con las mismas propiedades que nuestras vistas en el entorno de datos, no tendremos que cambiar nuestro código. Tendremos que preocuparnos entonces de crear y configurar los objetos CursorAdapter de tal manera que se comporten tal y como nuestras vistas remotas, el mejor lugar y el momento de hacerlo para cumplir el objetivo propuesto es sin dudas el evento BeforeOpenTables del Entorno de datos de nuestro formulario:

En primer lugar vamos a definir una clase heredada de la Clase CursorAdapter para inicializar algunas propiedades y agregar unos métodos a nuestra conveniencia. Lo podemos hacer dentro de una archivo Prg.
DEFINE CLASS CursorVista AS CURSORADAPTER

  && Estas 2 siguientes propiedades pueden establecerse
  && dinamicamente de acuerdo a las
  && Técnicas que el programador este usando

  DATASOURCETYPE="ODBC"
  DATASOURCE=SQLSTRINGCONNECT("DSN=PostgreSQL31W;DATABASE=guia;SERVER=localhost;PORT=5432;UID=miusuario;PWD=miclave" + ;
    ";CA=d;A6=;A7=100;A8=4096;B0=255;B1=8190;BI=0;C2=dd_;CX=1b502bb;A1=7.4-1")
  ALLOWDELETE = .T.
  ALLOWINSERT = .T.
  ALLOWUPDATE = .T.
  NODATA = .T.
  PREPARED = .T.
  SENDUPDATES = .T.
  WHERETYPE = 1

  && Este método toma como parámetro el nombre de la base de datos y el
  && nombre de la vista que está contenida en la misma, luego clona
  && todos las propiedades de la vista y las traslada al Objeto Cursor
  && Adapter Correspondiente

  PROCEDURE atrapavista
    LPARAMETERS Pnombredb,Pnombrevista
    && veriifcando que este abierta la base de datos
    IF OCCURS(ALLTRIM(UPPER(PnombreDB)) , ALLTRIM(UPPER(DBC()))) > 0 THEN
      THIS.ALIAS=Pnombrevista
      THIS.SELECTCMD=DBGETPROP(Pnombrevista,"VIEW","SQL")
      THIS.TABLES=DBGETPROP(Pnombrevista,"VIEW","Tables")
      DIMENSION papedazos(1)
      THIS.explotacadena(',',ALLTRIM(UPPER(STREXTRACT(THIS.SELECTCMD,'SELECT ',' FROM '))),@papedazos)
      FOR K=1 TO ALEN(papedazos)
        STORE THIS.aclaracampo(papedazos(k)) TO papedazos(k)
        THIS.CURSORSCHEMA=THIS.CURSORSCHEMA+papedazos(k)+SPACE(2)+DBGETPROP(Pnombrevista+'.'+papedazos(k),"FIELD","DataType")+ ','
        THIS.UPDATENAMELIST=THIS.UPDATENAMELIST+IIF(DBGETPROP(Pnombrevista+'.'+papedazos(k),"FIELD","Updatable"),papedazos(k)+ ;
          SPACE(2)+DBGETPROP(Pnombrevista+'.'+papedazos(k),"FIELD","Updatename")+',',"")
        THIS.UPDATABLEFIELDLIST=THIS.UPDATABLEFIELDLIST+IIF(DBGETPROP(Pnombrevista+'.'+Papedazos(k),"FIELD","Updatable"),Papedazos(k)+',',"")
        THIS.KEYFIELDLIST=THIS.KEYFIELDLIST+IIF(DBGETPROP(Pnombrevista+'.'+papedazos(k),"FIELD","KeyField"),Papedazos(k)+',',"")
      ENDFOR
      THIS.CURSORSCHEMA=SUBSTR( THIS.CURSORSCHEMA,1,LEN(THIS.CURSORSCHEMA)-1)
      THIS.UPDATABLEFIELDLIST=SUBSTR(THIS.UPDATABLEFIELDLIST,1,LEN(THIS.UPDATABLEFIELDLIST)-1)
      THIS.KEYFIELDLIST=SUBSTR(THIS.KEYFIELDLIST,1, LEN(THIS.KEYFIELDLIST)-1)
      RETURN 1
    ELSE
      MESSAGEBOX("No esta abierta la base de datos ","")
    ENDIF
  ENDPROC

  PROCEDURE explotacadena
    LPARAMETERS PCARACTER, PCADENA, apedazos
    PCARACTER=ALLTRIM(UPPER(PCARACTER))
    PCADENA=ALLTRIM(UPPER(PCADENA))
    LOCAL NCANTIDADEVECES

    NCANTIDADEVECES=OCCURS(PCARACTER,PCADENA)
    DIMENSION APEDAZOS  (ncantidadeveces+1)

    FOR I=1 TO NCANTIDADEVECES+1
      IF I=1
        pedazo=SUBSTR(PCADENA,1,AT(PCARACTER,PCADENA,I)-1)
      ELSE
        IF I=NCANTIDADEVECES +1
          pedazo=SUBSTR(PCADENA,RAT(PCARACTER,PCADENA,1)+1)
        ELSE
          pedazo=SUBSTR(PCADENA,AT(PCARACTER,PCADENA,I-1)+1,AT(PCARACTER,PCADENA,I)-AT(PCARACTER,PCADENA,I-1)-1)
        ENDIF
      ENDIF
      STORE pedazo TO Apedazos(i)
    ENDFOR
    RETURN @APEDAZOS
  ENDPROC

  PROTECTED PROCEDURE aclaracampo
    LPARAMETERS Pnombrecampo
    LOCAL auxcampo AS STRING
    PNOMBRECAMPO=ALLTRIM(UPPER(PNOMBRECAMPO))
    IF AT(' AS ',pnombrecampo) > 0 THEN
      IF AT(' AS ',pnombrecampo,2) > 0 THEN
        auxcampo=STREXTRACT(pnombrecampo,' AS ','',2)
      ELSE
        auxcampo=STREXTRACT(pnombrecampo,' AS ','')
      ENDIF
    ELSE
      IF AT('.',pnombrecampo) > 0 THEN
        auxcampo=STREXTRACT(pnombrecampo,'.','')
      ELSE
        auxcampo=pnombrecampo
      ENDIF
    ENDIF
    RETURN AUXCAMPO
  ENDPROC

ENDDEFINE
Nuestro formulario de ejemplo simula una guía de remisión electrónica, con datos del encabezado y una grilla con el detalle de los ítems del documento usaba originalmente 4 vistas remotas de las cuales:

Vw_guia: Vista con los datos del encabezado, con campos de varias tablas remotas, es actualizable y esta parametrizada, almacenamiento de optimista de fila. (Buffering=3)
VW_detgui: Detalle del documento, actualizable y parametrizada, almacenamiento optimista de tabla (Buffering=5)
Vw_embarcaciones: Vista no parametrizada y abre datos maestros de una tabla remota. No es actualizable
vw_documentos_correo: Vista parametrizada que contiene datos maestros de una tabla remota, no es actualizable.

Luego en el evento BeforeOpenTables del entorno de datos del formulario, debemos de crear los objetos y agregarlos como cursores miembros, de la siguiente manera según las características de cada vista:
&& Objeto que clona a la vista vw_guia
guia=CREATEOBJECT("CursorVista")
THIS.ADDOBJECT("guia","CursorVista")
THIS.guia.atrapavista("geus","vw_guia")
THIS.guia.ALIAS="vw_guia"
THIS.guia.NODATA=.T.
IF !THIS.guia.CURSORFILL()
  AERROR(nn)
  MESSAGEBOX(nn(1,2),"")
ELSE
  * this.guia.cursorrefresh()
ENDIF

&& Objeto que clona a la vista vw_detgui
detalle=CREATEOBJECT("CursorVista")
THIS.ADDOBJECT("detalle","CursorVista")
THIS.detalle.NODATA=.T.
THIS.detalle.atrapavista("geus","vw_detgui")
THIS.detalle.ALIAS="vw_detgui"
THIS.DETALLE.BUFFERMODEOVERRIDE=5 &&Almacenamiento de tabla
IF !THIS.detalle.CURSORFILL()
  AERROR(nn)
  MESSAGEBOX(nn(1,2),"")
ELSE
  *this.detalle.cursorrefresh()
ENDIF

&& Objeto que clona a la vista vw_embarcaciones
embarcaciones=CREATEOBJECT("CursorVista")
THIS.ADDOBJECT("embarcaciones","CursorVista")
THIS.embarcaciones.NODATA=.F. &&No esta parametrizada, cargar los datos sin mas
THIS.embarcaciones.atrapavista("geus","vw_embarcaciones")
THIS.embarcaciones.ALIAS="vw_embarcaciones"
IF !THIS.embarcaciones.CURSORFILL()
  AERROR(nn)
  MESSAGEBOX(nn(1,2),"")
ELSE
  THIS.embarcaciones.CURSORREFRESH() &&Cargar los datos sin mas
ENDIF

&& Objeto que clona a la vista vw_documentos_correo
documentos_correo=CREATEOBJECT("CursorVista")
THIS.ADDOBJECT("documentos_correo","CursorVista")
THIS.documentos_correo.atrapavista("geus","vw_documentos_correo")
THIS.documentos_correo.ALIAS="vw_documentos_correo"
IF !THIS.documentos_correo.CURSORFILL()
  AERROR(nn)
  MESSAGEBOX(nn(1,2),"")
ELSE
  THIS.documentos_correo.NODATA=.T.
  *this.documentos_correo.cursorrefresh()
ENDIF
En los dos primeros casos como ambos Objetos se derivan de vistas parametrizadas, se configura la propiedad NoData a FALSE con lo que nos aseguramos que el método CursorFill() no desencadene el cuadro de dialogo solicitando el valor del parámetro, en el tercer caso a diferencia de los anteriores al tratarse de una vista sin parámetros se cargan los datos sin restricciones, en el cuarto caso al igual que los dos primeros el método CursosFill() solo abrirá el cursor vacio en la sesión, quedando a nuestra cuenta invocar a los datos posteriormente con el método CursorRefresh(), esto puede hacerse en el evento Init o load del formulario en cuestión suministrando para ello el valor del parámetro que filtrara los datos.

A partir de entonces ya se puede trabajar con los Cursores CursorAdapter como si se trataran de Vistas Remotas, y lo mejor de todo esto es que solo se ha abierto un solo hilo de conexión.

Julián [HIPOGEA]

9 de noviembre de 2012

BLOQUEOS VIRTUALES EN MySQL

De todos es bien sabido la imposibilidad de bloquear registros individuales en MySQL u otras bases de datos de arquitectura Cliente-Servidor. Pero en MySQL, con un poco de imaginación, es posible realizar un bloqueo "virtual"simulando un bloqueo de registro...

BLOQUEOS "VIRTUALES" CON MySQL

De todos es bien sabido la imposibilidad de bloquear registros individuales en MySQL u otras bases de datos de arquitectura Cliente-Servidor. No existe la función RLOCK() como la conocemos para tablas DBF, que permite bloquear un registro o varios en el area de trabajo que le designamos, o que estamos posicionados en ese momento.

Pero en MySQL, con un poco de imaginación, Si es posible realizar un bloqueo "virtual", simulando un bloqueo de registro. Ahora vamos a ver como.

Para ello se utilizan tres funciones propias de la base de datos MySQL:

GET_LOCK(str,timeout)

Intenta obtener un bloqueo con el nombre dado por la cadena str, con un tiempo límite de timeout segundos.  Devuelve 1 si se ha obtenido el bloqueo, 0 si no se ha obtenido en el tiempo indicado (por ejemplo, porque otro cliente ha bloqueado ya el nombre), o NULL si ha habido un error (como falta de memoria o si el hilo fue matado por mysqladmin kill). Un bloqueo se libera cuando se ejecuta la función , se ejecuta un nuevo GET_LOCK() o el hilo termina (ya sea normal o anormalmente). Esta función se puede usar para implementar bloqueos de aplicación o para simular bloqueos de registro. Los nombres se bloquean en bases del servidor. Si un nombre ha sido bloqueado por un cliente, GET_LOCK() bloquea cualquier petición de otro cliente para bloquear el mismo nombre. Esto permite que clientes que se ponen de acuerdo para bloquear un nombre dado usar el mismo nombre para realizar un bloqueo coordinado.

IS_FREE_LOCK(str)

Verifica si el nombre de bloqueo str está libre para usarse (es decir, no está bloqueado). Devuelve 1 si el bloqueo está libre (nadie está usando el bloqueo), 0 si el bloqueo está en uso y NULL si hay errores (como argumentos incorrectos).

RELEASE_LOCK(str)

Libera el bloqueo con el nombre str que se obtuvo mediante. Devuelve 1 si el bloqueo fue liberado, 0 si no fue bloqueado por este hilo (en cuyo caso no fue liberado), y NULL si el nombre de bloqueo no existe. (El bloqueo no existirá si nunca fue obtenido por una llamada a o si ya ha sido liberado.) Es conveniente usar la sentencia con RELEASE_LOCK().

Vamos a ver un ejemplo de su utilización con VFP y a través de SQL Pass Through.
El ejemplo va a consistir en el bloqueo de un código determinado, es indiferente que sea un código de Cliente, de Proveedor, de un Artículo, o un número de Factura. Para el ejemplo vamos a seguir tres pasos:
  • Conectarnos a la base de datos MySQL
  • Realizar un bloqueo virtual a un código determinado (LOCK)
  • Liberar dicho bloqueo (UNLOCK)
Conexión a una base de datos MySQL.
cSQL=  "DRIVER={MySQL ODBC 5.1 Driver};" + ;
       "SERVER=localhost" + ;
       "PORT=3306" + ;
       "UID=xxxx" + ;
       "PWD=xxxx" + ;
       "DATABASE=MyBaseDatos" + ;
       "OPTION=2049"
 
SQLSETPROP(00,"Transactions", 1) 
SQLSETPROP(00,"DispLogin", 3)          
 
NH = SQLSTRINGCONNECT(""+cSQL, .T.)
IF NH < 0
MESSAGEBOX("Error de CONEXIÓN : " + ALLT(STR(NH)),48,"Atencion")
ENDIF

Realizar un bloqueo virtual 

El ejemplo muestra el intento de bloqueo de un código almacenado en la variable cKey . La función GET_LOCK reintenta automáticamente el bloqueo durante 10 segundos. Si consigue el bloqueo exitosamente devuelve el valor ‘1’, y salimos del bucle DO WHILE. Si al cabo de 10 segundos no es así, el valor devuelto será ‘0’ o Nulo con lo cual mostraremos un mensaje indicando que el código ya se encuentra bloqueado, o que su bloqueo es imposible.

También se puede utilizar la función IS_FREE_LOCK, para comprobar si el código en cuestión se encuentra bloqueado.
cKey="000001" && Código a Bloquear

DO WHILE .T.

     cSQL="SELECT IFNULL(GET_LOCK('"+cKey+"',10),'0') AS cRESULT"   
     SQLEXEC(NH,""+cSQL,"CURSOR")
     SELECT CURSOR
     GO TOP
     IF ALLTRIM(CURVAL(""+FIELD(1),"CURSOR"))="1"
          EXIT     && Código Bloqueado
     ENDIF

     OPB=MESSAGEBOX("Error de BLOQUEO   Reintentar",5+48+0,"Atencion")
     IF OPB <> 04
          EXIT
     ENDIF

ENDDO

Liberar Bloqueo