jueves, 1 de septiembre de 2011

Mi aplicación en Internet

Con Delphi tenemos la posibilidad de meter casi todo los componentes y otros recursos dentro del ejecutable. Una aplicacion delphi tradicional no requiere de mucho mas que el ejecutable mismo. Esto facilitaría el deploy.
En el caso de trabajar con datasnap podriamos publicar nuestra aplicacion en una pequeña aplicacion web y podria ser ejecutada desde cualquier lugar siempre y cuando el servidor datasnap este publico.
Hasta aca todo parece ir sobre ruedas pero aparecen algunas cuestiones, que casi todo pueda embeberse en el ejecutable no significa que absolutamente todo este incluido, pueden quedarnos cosas afuera.
Podemos poner como ejemplo alguna DLL imprescindible con algún driver que no puede faltar, incluso algún archivo ini para guardar configuraciones básicas, previas a todo, IP/Puerto etc y  no puedo exigirle al usuario que descargue ademas de un ejecutable un archivo ini, un driver de base de datos etc, esto tiene que funcionar con un click
Solución
Arrancamos con el ejemplo mas sencillo, supongamos un archivo ini, donde vamos a guardar dirección ip de mi servidor datasnap y alguna que otra cosa que necesite.
Cuando alguien descargue nuestra aplicación puede ponerla en alguna carpeta o ejecutarla directamente
Si decide ejecutar la aplicación correrá en algún directorio que el sistema operativo determine, si se selecciona guardar se pedirá donde colocar el ejecutable. Vamos a tomar como general el caso mas problemático que es cuando seleccionan directamente "Ejecutar". La solución pensada es colocar un ini inicial en alguna carpeta propietaria del usuario o de la aplicación (Para evitar problemas de permisos entre otras cosas), pero hacerlo si y solo si este archivo aun no existe. Entonces primer problema preguntarle al sistema operativo por la carpeta del usuario, aquí el código con algunos comentarios (Solo coloco la implementación, son métodos de clase) :
....uses SHFolder, windows, Classes,   SysUtils;
{Opciones posibles
  CSIDL_DESKTOP
  CSIDL_INTERNET
  CSIDL_PROGRAMS
  CSIDL_CONTROLS
  CSIDL_PRINTERS
  CSIDL_PERSONAL
  CSIDL_FAVORITES
  CSIDL_STARTUP
  CSIDL_RECENT
  CSIDL_SENDTO
  CSIDL_BITBUCKET
  CSIDL_STARTMENU
  CSIDL_DESKTOPDIRECTORY
  CSIDL_DRIVES
  CSIDL_NETWORK
  CSIDL_NETHOOD
  CSIDL_FONTS
  CSIDL_TEMPLATES
  CSIDL_COMMON_STARTMENU
  CSIDL_COMMON_PROGRAMS
  CSIDL_COMMON_STARTUP
  CSIDL_COMMON_DESKTOPDIRECTORY
  CSIDL_APPDATA
  CSIDL_PRINTHOOD
  CSIDL_ALTSTARTUP
  CSIDL_COMMON_ALTSTARTUP
  CSIDL_COMMON_FAVORITES
  CSIDL_INTERNET_CACHE
  CSIDL_COOKIES
  CSIDL_HISTORY
  CSIDL_SYSTEM
}
class function TGenericsUtils.GetUserIniFile(ApplicationExeName:String):String;
begin
  result := ChangeFileExt(IncludeTrailingBackslash( TGenericsUtils.GetSpecialFolderPath(CSIDL_APPDATA))+ExtractFileName(ApplicationExeName), '.ini');
end;


class function TGenericsUtils.GetSpecialFolderPath(folder : integer) : string;
const
 SHGFP_TYPE_CURRENT = 0;
var
 path: array [0..MAX_PATH] of char;
begin
   //[Current User]\My Documents
   //CSIDL_PERSONAL;
   //All Users\Application Data
   //CSIDL_COMMON_APPDATA;
   //[User Specific]\Application Data
   //CSIDL_LOCAL_APPDATA;
   //Program Files
   //CSIDL_PROGRAM_FILES;
   //All Users\Documents
   //CSIDL_COMMON_DOCUMENTS;


 if SUCCEEDED(SHGetFolderPath(0,folder,0,SHGFP_TYPE_CURRENT,@path[0])) then
   Result := path
 else
   Result := '';
end;

Aquí el método GetUserIniFile me retornara el nombre del ini de mi aplicación, ademas del nombre del archivo esta la ruta completa a el, ruta solicitada al sistema operativo por medio del método GetSpecialFolderPath.


fn := TGenericsUtils.GetUserIniFile(Application.ExeName);
  if not FileExists(fn) then begin
    //Si no existe genero un ini basico.....
    TGenericsUtils.ExtractResource('ARCHIVOINI', fn);
  end;

El código anterior pregunta por la existencia del ini, en caso de no existir llama a una método "ExtractResource" que extrae un recurso del ejecutable, un recurso llamada "ARCHIVOINI" porque yo previamente embebí un ini de arranque en el ejecutable, tema que veremos mas adelante, para no tener que tomarme el trabajo de crear todas las secciones y claves que en un archivo ini grande puede exigir varias lineas de código.
Aquí el código de "ExtractResource":


class procedure TGenericsUtils.ExtractResource(ResName: String; Filename: String);
var
  ResStream: TResourceStream;
begin
  ResStream:= TResourceStream.Create(HInstance, ResName, RT_RCDATA);
  try
    ResStream.Position:= 0;
    ResStream.SaveToFile(Filename);
  finally
    ResStream.Free;
  end;
end;


Ademas esto me sirve para cuando tenga que embeber una dll y otro elemento que no puedo generar por código.
¿Como meto recursos en el ejecutable?
El delphi XE es muy sencillo, seleccione el item del menu como indica la figura

Luego añada los recursos colocando le tipo correspondiente y un nombre que luego sera usado para pedirle al método ExtractResource que realice la extracción deseada



En la próxima entrada extendemos la solución a modulos, dll, bpls etc

miércoles, 3 de agosto de 2011

DBExpress - Many to Many (Relaciones muchos a muchos) Parte 2

En la ultima entrada vimos como trabajar relaciones muchos a muchos con dbexpress. Analizamos como relacionar los datasets y demás componentes de tal forma que casi sin escribir código logramos que moviéndome sobre un registro me muestre los elementos incluidos en el y los disponibles a ser incluidos. Hasta acá funciono todo sin problemas, ahora llega el momento de meter elementos dentro de los asignados, es decir, tomar elementos disponibles y meterlos en el lote de asignados para dejar luego que dbexpress se encargue de los inserts/deletes relacionados. Usamos un ejemplo de usuarios-permisos por ser sencillo y descriptivo y armamos un form con una grilla superior que muestra los usuarios y dos grillas inferiores mostrando permisos asignados y disponibles.
Pongamos ahora dos botones para que se puedan enviar los permisos de disponibles a asignados y viceversa.


Hagamos luego también una primera aproximación a la solución en términos de código

procedure TFrmUsuario.btnAsignarClick(Sender: TObject);
begin
  cdsPermisosAsignados.Insert;
  cdsPermisosAsignadosIdPermiso.Value := cdsPermisosDisponiblesIdPermiso.Value;
  cdsPermisosAsignados.Post;
  cdsPermisosDisponibles.Delete;

end;

procedure TFrmUsuario.Button1Click(Sender: TObject);
begin
  cdsUsuario.ApplyUpdates(-1);
end;

procedure TFrmUsuario.Button2Click(Sender: TObject);
begin
  cdsUsuario.CancelUpdates;
end;

Esto funciona y funciona bien si hablamos solo de asignación de permisos.
Asigna correctamente los permisos y se encarga de actualizar la nueva realidad en la tabla de relación, pero solo funciona correcta y completamente hacia un lado, si intentara la inversa, quitar un permiso asignado tendría el problema de que no tengo el campo nombre para llenar el clientDataSet del los permisos disponibles. Algunas modificaciones en el dataset que provee los datos podrían ayudar. 
Lo que originalmente fue:

select *
from UsuarioPermiso
Where Login=:Login

Ahora es:

select UsuarioPermiso.*, Permiso.Nombre
from UsuarioPermiso inner join Permiso
On UsuarioPermiso.IdPermiso=Permiso.IdPermiso
Where Login=:Login

Muy importante ver que el campo Nombre no pertenece a la tabla relación y por tanto no debe ser parte de los inserts y delete, mediante los providerflags de este campo desactivar su participacion en pfInUpdate y en pfInWhere,  (En ClientDataSet y DataSet), es un campo invitado en la consulta simplemente para mejorar aspectos de interfaz de usuario.


Ahora la interfaz muestra el nombre de los permisos





Finalmente el código quedaría así:

procedure TFrmUsuario.btnAsignarClick(Sender: TObject);
begin

  cdsPermisosAsignados.Insert;
  cdsPermisosAsignadosIdPermiso.Value := cdsPermisosDisponiblesIdPermiso.Value;
  cdsPermisosAsignadosNombre.Value := cdsPermisosDisponiblesNombre.Value;
  cdsPermisosAsignados.Post;
  cdsPermisosDisponibles.Delete;

end;

procedure TFrmUsuario.btnDesasignarClick(Sender: TObject);
begin
  cdsPermisosDisponibles.Insert;
  cdsPermisosDisponiblesIdPermiso.Value := cdsPermisosAsignadosIdPermiso.Value;
  cdsPermisosDisponiblesNombre.Value := cdsPermisosAsignadosNombre.Value;
  cdsPermisosDisponibles.Post;
  cdsPermisosAsignados.Delete;

end;

procedure TFrmUsuario.Button1Click(Sender: TObject);
begin
  cdsPermisosDisponibles.CancelUpdates;
  cdsUsuario.ApplyUpdates(-1);
   cdsUsuario.Refresh;
end;

procedure TFrmUsuario.Button2Click(Sender: TObject);
begin
  cdsUsuario.CancelUpdates;
end;

procedure TFrmUsuario.cdsUsuarioBeforeScroll(DataSet: TDataSet);
begin
  if cdsUsuario.ChangeCount>0 then begin
    ShowMessage('Hay cambios pendientes en el registro actual, los perdera');
    cdsUsuario.CancelUpdates;
    cdsUsuario.Refresh;
  end;
end;

Observe que antes de aplicar los cambios cancelo los cambios referentes los registros disponibles, los mismos son resultado del calculo de la consulta sobre lo que esta en la base y no deben tener injerencia sobre la misma. Observe también que esta consulta debe actualizarse constantemente, si aplique un cambio a la base necesito actualizar esto, si me intentan cambiar de registro en usuario y no guardaron los cambios debe preguntar que se debe hacer y guardo y refresco o cancelo todo.
El objetivo de la entrada es entender la forma el concepto principal, el lector luego puede mejorarlo, incluso se pueden activar/desactivar los botones de asignación y desasignación segun disponibilidad, ese tipo de detalles no serán contemplados aquí.







miércoles, 20 de julio de 2011

DBExpress - Many to Many (Relaciones muchos a muchos) Parte 1

Así como en la entrada http://pablosoligo.blogspot.com/2011/07/dbexpressmaster-detail.html se presento el tema de las relaciones master-details aparece ahora el interrogante de si podemos tambien manejar las relaciones muchos a muchos de una manera similar.
Yo digo que si, lo he probado, podemos manejar casi de manera completamente automática las relaciones muchos a muchos, lo único que haremos es ayudarnos con comandos sql para producir el resultado esperado.
Para empezar pensemos una situación de relación muchos a muchos, propongo la siguiente, didáctica y bien sencilla


Creo se sobreentiende un usuario tiene muchos permisos y un permiso puede estar en muchos usuarios.
Para plantear esto como un maestro detalle o mejor dicho como un maestro y doble detalle pongamos a cada elemento en su posición

Usuario->Maestro
Permisos asignados->Detalle 1
Permisos no asignados->Detalle 2

¿Como quedan las consultas?

sqlUsuario
select *
from Usuario

sqlPermisosAsignados
select *
from UsuarioPermiso
Where Login=:Login

sqlPermisosDisponibles
select *
from Permiso as p
Where p.IdPermiso not in
(Select IdPermiso
from UsuarioPermiso as up
Where up.Login=:Login)

Bueno ahí esta la primera aproximación, luego veremos si es suficiente o tenemos que mejorar algún aspecto,
sqlUsuario no merece explicación, sqlPermisoAsignados casi que tampoco, son los permisos que le usuario ya tiene asignado y podemos considerarlo como un detalle del usuario, sqlPermisosDisponibles son todos los permisos que no tiene el usuario tema resuelto con un "Not In" el lector puede usar un Exists si prefiere y también lo podemos considerar un detalle del usuario, de hecho como esta planteado el único parámetros de las consultas a las que consideramos detalle es el login del usuario, dato presente en el maestro, condición necesaria y suficiente para armar nuestros master-details en DBExpress
Así quedaría la cosa, recordar enlazar los dataset detalle por su propiedad DataSource al DataSource llamado en este caso "datUsuario"




Llegado este punto terminamos con el servidor, llega la hora del cliente.
Siguiente lo mostrado en la entrada http://pablosoligo.blogspot.com/2011/07/dbexpressmaster-detail.html la cosa quedaría así, tanto los permisos disponibles como los permisos asignados son detalles del usuario




Relaciono las grillas los datasources etc, pongo algunos datos y abro solo el clientedataset maestro al abrir la ventana

procedure TFrmUsuario.FormCreate(Sender: TObject);
begin
  cdsUsuario.Open;
end;


y lo pruebo a ver que tal funciona.



Perfecto, me muevo entre usuarios y sus permisos disponibles y asignados se actualizan automáticamente.
Claro, la critica acá puede ser de que debo mostrar las descripciones de los permisos en permisos asignados, es un tema que dejamos para mas adelante ya que no hace a la diferencia en cuanto a programación, lo importante es entender la idea. Con esto tenemos el 70% del problema resuelto y solo escribimos una linea de código.

En la próxima entrada vemos la actualización de los permisos con este mismo esquema.

jueves, 14 de julio de 2011

Master-Details y Autoincrementales

Ahora si, este tema no puedo decir que no esta documentado, he encontrado tutoriales y papers que describen el problema, eso si, siempre en Ingles y tampoco son tantos los lugares donde uno pueda encontrar una solución a este asunto.
Como vimos en la entrada

http://pablosoligo.blogspot.com/2011/07/dbexpressmaster-detail.html

DBExpress maneja muy bien la idea de maestro detalle, lo maneja como un elemento único simplificando el desarrollo, es mas, solo se encarga de llenar el campo de relación en el detalle y lo llena correctamente con el identificado del maestro.
Rapidamente aparece un problema con los autoincrementales, el valor clave del maestro lo pone la base, cuando dbexpress intenta insertar los registros detalles no sabe la clave del maestro, de hecho el problema aparece antes ¿Que clave lleva el detalle en el ClientDataSet cuando siquiera se la clave que va a tener el maestro?

Primero ataquemos el problema que acabamos de mencionar, voy a cambiar la base de datos de ejemplo vista en las entradas anteriores para indicar que ahora el idPedido es autoincremental, en una oracle sera colocar un trigger, en Interbase un generator o como sea según motor, la idea es que el valor lo pone la base y no yo.






Como ven, el clientDataSet(Maestro) tiene un evento que se ejecuta cuando hay un nuevo registro, tomemos esto para colocar un IdPedido temporal o comodín si así lo quieren llamar. Luego los detalles se generaran con este IdPedido comodín.

procedure TFrmPedido.cdsPedidoNewRecord(DataSet: TDataSet);
begin
  DataSet.FieldByName('IdPedido').Value := -1;
end;




Esto es una simplificación, si yo intentara ingresar mas registros maestros fallaría porque crearía dos registros con IdPedido=-1, solución y truco para esto.


procedure TFrmPedido.cdsPedidoNewRecord(DataSet: TDataSet);
begin
  DataSet.FieldByName('IdPedido').Value := DataSet.FieldByName('IdPedido').Tag - 1;
  DataSet.FieldByName('IdPedido').Tag := DataSet.FieldByName('IdPedido').Value;
end;

Ayudándonos con la propiedad tag y sin declarar mas variables hacemos que nuestro IdPedido comodín se incremente hacia el negativo. Vamos a usar es breve este valor comodín.

Ahora el siguiente problema, si el valor lo IdPedido lo pone la base, entonces no lo pongo yo.
¿Como le digo a DBExpress insertame todos los campos menos este que lo hace la base de datos?
La respuesta es simple, del lado servidor (Suponiendo trabajo con DataSnap) buscamos el DataSet, seleccionamos en campo y en providerflags ponemos el pfInUpdate en false, con esto cuando se genere el insert DBExpress dejara fuera a este campo. La figura muestra como hacerlo.






En esta entrada http://pablosoligo.blogspot.com/2011/07/logica-del-negocio.html  se demuestra que puedo tener injerencia en los datos justo antes de ser insertados efectivamente, ¿Como usar esto a mi favor y poner en orden el tema de las claves en este maestro-detalle?

El SqlServer se encargara de poner el valor de IdPedido al maestro, es mi responsabilidad ponerlos en el detalle. Como vimos en la entrada anterior puedo meterme justo antes de que se inserte el registro e interferir en la operación, veamos como:

procedure TSMPedido.dspPedidoBeforeUpdateRecord(Sender: TObject;
  SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
  var Applied: Boolean);
begin
  inherited;
  if (UpdateKind=ukInsert) and (SourceDS=sqlPedido) then begin
    DeltaDs.FieldByName('FechaHora').NewValue := Now();
  end;

  if (UpdateKind=ukInsert) and (SourceDS=sqlItemPedido) and (DeltaDS.FieldByName('IdPedido').NewValue<0) then begin
      sqlTableIdentity.Close;
      sqlTableIdentity.ParamByName('TableName').Value := 'Pedido';
      sqlTableIdentity.Open;
      DeltaDS.FieldByName('IdPedido').NewValue  := sqlTableIdentity.FieldByName('Identidad').Value;
  end;
end;


sqlTablaIdenty es un TSqlDataSet con el siguiente CommandText

SELECT IDENT_CURRENT(:TableName) as Identidad


En sqlServer retorna la identidad para una tabla en una conexión determinada, el lector sabrá como hacerlo en su motor de base de datos favorito, si utilizo un esquema de herencia para la creacion de servermodules mucho mejor porque puede poner este sqldataset en el servermodule padre y tenerlo listo en cualquier server module donde lo necesite usar.
Por lo demas lo que se hace es preguntar si es una inserción, si el dataset origen es el del detalle y si el valor de idPedido del detalle es negativo, pensemos que me puede estar insertando un detalle mas en un pedido previamente creado que ya tiene su IdPedido correctamente generado, ahí no necesito preguntar a la base y asignarlo por la fuerza.





domingo, 10 de julio de 2011

Lógica del negocio

Es sabido, y existen tutoriales y vídeos que explican como crear métodos en datasnap y consumirlos desde una aplicación cliente, ya sea Delphi, ASP.NET, PHP etc. Existe también muchos tutoriales y vídeos que explican como utilizar la interfaz IAppServer.
Muchos nos sentimos muy tentados a utilizar intensivamente la interfaz IAppServer, principalmente por su facilidad y rapidez. En entradas anteriores dejamos pendiente el tema de los autoincrementales en relaciones maestro-detalle con DataSnap

http://pablosoligo.blogspot.com/2011/07/dbexpressmaster-detail.html

lo que voy a mostrar es por un lado la puerta de entrada es a resolver el problema mencionado, pero es mucho mas que eso.

¿Si utilizo la interfaz IAppServer como y donde genero mi lógica del negocio?

La tentación de usar esta interfaz es muy grande, pero si los componentes generar automáticamente todo, ¿Como puedo yo meter mi lógica en todo este automatismo?
Existen varios lugares pero quiero proponer el que viene acompañado de mas información para la aplicación efectiva


Como muestra la figura el TDataSetProvider tiene dos eventos que nos serán muy útiles, BeforeUpdateRecord y AfterUpdateRecord. Tomemos el primero, generemos el código del evento y veamos que podemos hacer.



procedure TSMPedido.dspPedidoBeforeUpdateRecord(Sender: TObject;
  SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
  var Applied: Boolean);
begin
  inherited;



  if (UpdateKind=ukInsert) and (SourceDS=sqlPedido) then begin
    DeltaDs.FieldByName('FechaHora').NewValue := Now();
  end;




end;



Aunque muy simple el código de arriba nos presenta una idea de lo que podemos hacer. El evento como su nombre lo indica se ejecuta justo antes de insertar/modifica/borrar el registro.
En el ejemplo, muy simple, lo único que hacemos es preguntar si es una inserción, de ser así ponemos la fecha hora que nos de el sistema. DeltaDS contiene los registros que cambiaron, la diferencia entre los datos que salieron del servidor y los que volvieron del cliente.
Parándonos sobre un registro y un campo con OldValue, NewValue podemos acceder a esa diferencias, incluso en este caso pisamos la fecha con el valor que nos retorne el sistema. Podemos incluso liberar a la aplicación cliente de esta responsabilidad y dejar que el servidor de aplicación lo haga.

Esto que ponemos aquí es didáctico, pero ya nos da la pauta de que es posible justo antes o justo después de actualizar un registro poner nuestra lógica, modificar valores, remplazar, e incluso tocar otras tablas que estén relacionadas. A modo de ejemplo luego de insertar/modificar/borrar un ítem de un pedido debo actualizar los stocks, luego de actualizar la temperatura de algún sensor puedo generar alguna alarma etc.
Para mejor, los parámetros me dan acceso a los elementos involucrados en la operación e incluso me dejan ver si es un insert, un update o un delete.

lunes, 4 de julio de 2011

DBExpress - Master-Detail

Es posible anidar dataset en dbexpress, utilidad muy interesante para casos de tablas relacionadas tipo master-detail. Existe buena documentación sobre esto en internet y quiero usar esta entrada como introducción a las próximas donde se ve un tema menos documentado que es el de los autoincrementales en casos de maestro-detalle.
DBExpress permite anidar DataSet para tablas de tipo maestro-detalle hasta un limite cercano a los 15 o 16 dataset, quien escribe nunca supero los 5 o 6 y en este sentido nunca tuve mayores problemas.

¿Por que yo querría trabajar con dataset anidados?

Bueno las ventajas son varias, en principio manejar todos los elementos de manera única, cosa que es una realidad, si el maestro fuera una tabla de pedidos y me paro sobre un pedido particular quiero ver, borrar, editar y añadir items de ese pedido y no de otros.
Para mejor cuando actualizo quiero una actualización en un bloque transaccional, para todo lo relacionado, maestro y detalle.

¿Como genero un conjunto de dataset maestro-detalle en delphi?

Es fácil, con un poco de practica sale como pan caliente, pero obviamente hay que relacionar varias cosas de manera ordenada así que naturalmente puede aparecer alguna complicación las primeras veces.
Una imagen, mil palabras.



La imagen anterior muestra los diferentes componentes y sus conexiones, figura en el texto de la propiedad a conectar y la flecha la dirección de conexión. Las consultas en cada uno de los datasets serian las siguientes.

SqlPedido: Select * from Pedido

SqlItemPedido: Select * from ItemPedido Where IdPedido=:IdPedido

Se observa en la consulta de SqlItemPedido un parametro, IdPedido, el parametro debe tener el mismo nombre que la columna de la tabla maestra.
Añadiendo un y solo un datasetProvider conectado al dataset maestro (En este caso sqlPedido) tenemos solucionada “la parte servidora” independientemente de si es una aplicación multicapa o no. Si fuera una simple aplicación cliente/servidor entendamos que la parte servidora son las conexiones a la base de datos, los datasets y los providers, la parte cliente son los clientsdataset involucrados.
La "Parte servidora" quedaría entonces así


Ahora llega el momento de configurar la parte cliente, aca la cosa puede ser muy simple o se puede complicar un poco depende del diseño de base de datos.
Esta seria la forma del lado cliente, dos clientdatasets uno para la tabla maestra y otro para la tabla detalle, dos datasources enlazados y dos grillas, completando dos navigators por si son necesarios y un boton para efectuar el applyUpdate que llevara los cambios a la base de datos.


Muy importante, el ApplyUpdate solo debe ejecutarse sobre el clientdataset maestro, DBExpress se encargara del detalla en el orden correspondiente según sea inserción, edición o borrado.


procedure TFrmPedido.btnApplyUpdateClick(Sender: TObject);
begin
  cdsPedido.ApplyUpdates(-1);
end;

Para saber si todo va bien basta con generar los campos en el clientdataset maestro como se indica abajo.





Si se observa un campo mas con el nombre del dataset detalle del lado servidor nos quedamos tranquilos, conectamos todo bien y estamos listos para dar el toque final.
Lo que nos queda por hacer ahora es hacer apuntar en el clientDataSet detalle el DataSetField al campo sqlItemPedido del clientDataSet maestro.



Con esto es suficiente para el clientDataSet detalle, todo lo va a manejar el maestro y en el detalle solo veremos los registros correspondiente al registro maestro actual. Incluso podemos ver que dbexpress genera automáticamente en el detalle el id del maestro correspondiente.
Podemos añadir, borrar, editar y el applyUpdate que usaremos es el del clientDataSet maestro que se encargara de todo.




Hilando un poco mas fino


En aplicaciones reales algunos cosas que no debemos dejar pasar.

¿Traemos todos los detalles juntos o los traemos a medida que nos vamos desplazando por el maestro?

Esto en realidad depende de la cantidad de datos, del tamaño etc, lo importante es saber donde determinar esto y yo he encontrado sumamente útil en variedad de casos retrasar el envío del detalle en el momento que se necesite
Como muestra la figura una de las opciones del datasetProvider es poFetchDetailsOnDemand, esta propiedad determina si los detalle se envían sobre demanda o todos al mismo tiempo.


Ahora sabemos crear master-detail condición necesaria para encarar un problema un poco mas dificil y menos documentado de como combinar esto con los autoincrementales que tanto nos gustan, incluso si no fueran autoincrementales es muy probable que el IdPedido sea responsabilidad de la base de datos y no nuestra.
La manera de resolver esto y otros temas quedan para próximas entradas.










lunes, 20 de junio de 2011

¿Y cuanto duran tus transacciones?

Mientras miraba de reojo un servidor de aplicación funcionando observo el mensaje "Begin transaction module bla bla..", le presto un poco de atención y me pongo en espera  del "Commit transaction.....". (Este servidor de aplicación va logueando en pantalla algunas de las tareas que va realizando).
Sucede entonces que los segundos pasan y nada....y pasan y nada, y de repente "Commit transaction". ¿Que paso acá? La base de datos no esta sobrecargada, yo la monitoreo, ¿Como puede tardar tanto una transacción ? Nos han enseñado una y otra vez que las transacciones deben ser cortas, tan cortas como sea posible. Cuando profundizo en el tema veo con horror los siguiente, esta transacción abarca entre 6 y 10 tablas y ante una situación anormal (Bastante improbable por suerte) la aplicación comienza a preguntar al usuario si quiere resolverlo de tal o cual manera, y mientras tanto la transacción abierta. Problema encontrado, problema solucionado y ahora acá viene el tema ¿Esta esto pasando en varios lugares o fue esta la única "patinada" de toda la aplicación? ¿Como saberlo?

En la entrada
http://pablosoligo.blogspot.com/2011/06/dbexpress-trace.html
se explico como llevar a una tabla de log las operaciones que realiza DBExpress con la base de datos, entre esas operaciones están los begin transaction/commit/rollback entre otras muchas cosas. ¿Y si sacamos la diferencia de tiempo entre un begin transaction y su correspondiente commit/rollback? podriamos ver si hay irregularidades, incluso podemos ordenar la cosa y saber cuales son las que mas demoran.
Desgraciadamente quien les escribe no sabe ni investigo como pedirle al DBExpress o al sqlserver el numero de transaction si es que existiera, tampoco tenemos en la tabla un campo que una un begin transaction con su commit/rollback, por tanto tendremos que usar las neuronas un poco mas.
La siguiente consulta es la base sql que podría resolver el misterio,



select DateDiff(millisecond, s1.FechaHora, s2.FechaHora) as 'Demora', *
from SqlLog as s1 inner join SqlLog as s2
On s1.Module=s2.Module and s1.Login=s2.Login
Where (s1.Message like 'OLEDB - StartTransaction') and
  (s2.Message like 'OLEDB - Abort' or s2.Message like 'OLEDB - Commit')and
  (s2.IdSqlLog>s1.IdSqlLog) and
  (s2.IdSqlLog<(
   Select top 1 IdSqlLog
   from SqlLog as i
   Where i.Login=s1.Login and
     i.Module=s1.Module and
     i.IdSqlLog>s1.IdSqlLog and
     i.Message like 'OLEDB - StartTransaction'
   Order by i.IdSqlLog))


Traducido lo que hace es un join de la tabla de log con sigo misma juntando por Module y login, esto es porque es una aplicación multicapa, en una aplicación Cliente/Servidor una campo hostname en la tabla de log soluciona el problema del join.
Hecho el join busca un begin transaction y para ese busca o un commit o un rollback cuidando de que el idLog sea mayor y que no exista ningún otro begin transaction en el medio (Begin transaction del mismo Login/Module o hostname según corresponda).
Mañana estaré probando esto en un entorno de producción, con datos reales
WARNING
No hace falta pedir el plan de consulta para ver que esta consulta es explosiva especialmente en una tabla que crece sin descanso y que se puede volver gigante en un par de días










Así que mucho cuidado, ejecutarla sobre un set de datos pequeño o en un servidor de testing.

jueves, 16 de junio de 2011

DBExpress & Trace


Bueno, en esta oportunidad quiero comentarles una técnica que me ha dado excelentes resultados con muy poco trabajo. Se trata de dedicar algunos minutos, máximo alguna hora a la creación de un esquema de logueo de comandos de base de datos ejecutados por dbexpress que puede ser de suma utilidad en desarrollo e incluso en producción.

El Problema

Muchas veces en el día a día del desarrollo nos encontramos con alguna error de base de datos, o una operación que no se realizo como esperábamos, en definitiva una situación desconocida y no tenemos muchas herramientas para hacer un diagnostico rápido. De nada valen breakpoints porque no es un error de programa sino propiamente SQL.

La solución

La solución que quiero presentar es muy simple de implementar gracias el componente TsqlMonitor de DBExpress, no cuesta mucho trabajo y siempre es útil tener a mano algo mas de información para un rápido diagnostico. El Componente TsqlMonitor nos dispara una evento con cada interacción entre nuestra conexión y la base de datos, este evento viene acompañado de una buena cantidad de información sobre el comando que se acaba de ejecutar, existen una forma rapida mediante el componente de guardar esta información en archivo pero yo quiero ir un poco mas allá.

La implementación

Lo que propongo es guardar también esta información en la base de datos, esto nos permite rastrear y encontrar mejor lo que buscamos, permite una persistencia mas seria de la historia. Un diseño de tabla mas o menos completo seria el siguiente:

CREATE TABLE [dbo].[SqlLog](
[IdSqlLog] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,--Clave principal autoincremental
[Module] [varchar](50) NULL,--Modulo que ejecuto el comando, muy util en DataSnap
[FechaHora] [datetime] NOT NULL,--FechaHora de ejecucion
[Message] [text] NOT NULL,--Mensaje, DDL DML correspondiente
[TraceFlag] [int] NOT NULL,--Informacion adicion DBExpress
[TraceLevel] [int] NOT NULL, --Informacion adicion DBExpress
[CustomCategory] [varchar](256) NOT NULL, --Informacion adicion DBExpress
[Login] [varchar](50) NULL, --Usuario logueado
[InTransaction] [varchar](50) NOT NULL, --¿El comando se ejecuto dentro de una transaccion?
CONSTRAINT [PK_idSqlLog] PRIMARY KEY CLUSTERED
(
[IdSqlLog] ASC
) )Etc....

Esto esta extraido de un sqlserver 2008 pero es mas o menos lo mismo en cualquier motor. Comento en cada linea cual es la idea del campo.
Campo mas campo menos la información esta, abajo un ejemplo real de mis aplicaciones


TSqlMonitor
Lo primero que hay que hacer luego de crear la tabla es drag and drop de un TsqlMonitor a nuestro DataModule o donde tengamos acceso a nuestro componente de conexión, mediante la propiedad SqlConnection enlazamos el Monitor a la conexión.


Si tenemos una datamodule o servermodule abstracto del cual heredamos mucho mejor, mas información sobre esto aquí


Ahora hay que tener mucho cuidado, no podemos usar la misma conexión para loguear lo que sucede, seria como verse entre dos espejos, la situación se extendería hasta el infinito en el caso de dbexpress hasta el access violation :)
La razón es simple, si usamos la misma conexión al tener que llevar una operación al log estaríamos generando otro trace (El insert al log) que también generaría otro comando para un nuevo trace de este comando y así hasta el desastre.
De esto se desprende que necesitamos otra conexión para cumplir con el objetivo, podemos usar directamente le componente de conexión o ayudarnos con un TsqlQuery con la siguiente inserción:

INSERT INTO SqlLog
(Module, Message, TraceFlag, TraceLevel, CustomCategory, Login, InTransaction)
VALUES (:Module,:Message,:TraceFlag,:TraceLevel,:CustomCategory,:Login,:InTransaction)

(Las fechas las pone la base), y un metodo que hago uso del comando mas o menos así:


function TDMGlobalConnection.eLogTrace(AModule:String; Msg:String; TraceFlag:Integer; TraceLevel:Integer; CustomCategory:String; UserName:String; InTransaction:boolean):boolean;
begin
Result := false;

//OLEDB - Commit
//OLEDB - StartTransaction


//256=Consulta
//512=Parametros

if ((TraceFlag=256) or (TraceFlag=512) and ((TraceFlag=32))) and
(
(Msg<>'OLEDB - Release')and
(Msg<>'OLEDB - ReleaseAccessor')and
(Msg<>'OLEDB - GetNextRows')and
(Msg<>'OLEDB - ReleaseRows')and
(Msg<>'OLEDB - GetData')and
(Msg<>'OLEDB - Execute')and
(Msg<>'OLEDB - GetResult')and
(Msg<>'OLEDB - CreateAccessor')and
(Msg<>'OLEDB - IAccessor')and
(Msg<>'OLEDB - GetColumnsInfo')and
(Msg<>'OLEDB - GetColumnInfo')and
(Msg<>'OLEDB - IColumnsInfo')and
(Msg<>'OLEDB - ICommandText')and
(Msg<>'OLEDB - SetCommandText')and
(Msg<>'OLEDB - GetResult')and
(Msg<>'OLEDB - ReleaseAccessor')and
(Pos('SELECT DISTINCT DB_NAME(), SCHEMA_NAME(O.uid)', Msg)=0)and
(AModule<>'TDSInterface')
) then begin

InsertLog.ParamByName('Module').Value := AModule;
InsertLog.ParamByName('Message').Value := Msg;
InsertLog.ParamByName('TraceFlag').Value := TraceFlag;
InsertLog.ParamByName('TraceLevel').Value := TraceLevel;
InsertLog.ParamByName('CustomCategory').Value := CustomCategory;
InsertLog.ParamByName('Login').Value := UserName;
InsertLog.ParamByName('InTransaction').Value := BoolToStr(InTransaction, true);
InsertLog.ExecSQL();

Result := true;
end;

end;

Se puede observar que no se esta logueando todo, se esta haciendo un filtro y esto es muy dependiente del motor, el lector puede loguear todo y luego ir quitando mediante filtros elementos que no lo interesan o no aportan a la causa.

¿Y con aplicaciones multithread?¿Y con DataSnap?

Bueno es básicamente lo mismo pero añadimos un complejidad mas, podemos tener las dos conexiones en cada servermodule, una para su uso regular y otra para log.
Sin embargo la cantidad de conexiones puede ser demasiada si operamos asi, la solucion tener un DataModule global, a este dataModule lo van a usar todos los server modules que necesite loguear. ¡Pero cuidado con la concurrencia! Debemos utilizar seccion criticas para asegurarnos que lo hagan de a uno a la vez, véase


Finalmente el evento quedaria así

procedure TAbstractDSServerModule.SQLMonitorTrace(Sender: TObject;
TraceInfo: TDBXTraceInfo; var LogTrace: Boolean);
begin

//Puede poner flag para desactivar log en producción o cuando no se desee
CSConnection.Enter;
try
DMGlobalConnection.eLogTrace(Self.ClassName, TraceInfo.Message, TraceInfo.TraceFlag, TraceInfo.TraceLevel, TraceInfo.CustomCategory, 'Proxima entrada', TSqlMonitor(Sender).SQLConnection.InTransaction);
finally
CSConnection.Leave;
end;


end;

end;


Prometo subir el código de todas las entradas en breve.

martes, 14 de junio de 2011

Threads y Secciones Criticas

Cuando se necesita asegurar el acceso atómico a un bloque de código determinado tenemos que utilizar alguna forma de bloqueo y sincronizacion. Delphi dispone de varias herramientas para esto (Semáforos, monitores, secciones criticas)
Vamos a ver una de ellas, probablemente la mas sencilla y en mi caso la única que he usado intensivamente.

Una aplicación VCL tradicional no usa mas que un thread o hilo, el hilo principal, por lo tanto no tenemos acceso simultaneo a un recurso. Esto hace que el tema de sincronizacion no nos toque directamente, cuando uno desarrolla una aplicación/servidor de aplicación o servicio multihilo la cosa cambia y hay que tener especial atención al acceso simultaneo a los recursos.

La sección critica
Como dijimos en delphi tenemos una herramienta muy sencilla que es la seccion critica, esta no es mas que una clase de la cual necesitamos solo 2 metodos, uno para entrar a la seccion critica o otro para abandonarla

CS.Enter;
try
 //mi código protegido de acceso simultaneo
finally
 CS.Leave;
end;

Observe que el código protegido esta dentro de un bloque try-finally, esto no es solo prolijidad o una buena practica, en este caso particular es mandatario. Si una excepción ocurre en “mi código protegido” y no se ejecuta el método Leave nunca se abandona la sección critica, por tanto se bloquean todos los otros threads que intenten ejecutar el código protegido.

Ejemplo para el uso de TcriticalSection en DataSnap

Dependiendo del modelo de lifeCycle seleccionado en cada uno de los ServerClass es muy probable que tengamos varios Threads funcionando en paralelo. ¿Y si quiero que mis ServerModules escriban operaciones, logs de seguimiento o lo que sea en un visor en la pantalla principal? 
Acá aparece el problema, supogamos ponemos un memo para loguear actividades de los ServerModules, varios intentaran usar el memo al mismo tiempo produciendo los problemas de concurrencia mencionados. 
Ponemos el ejemplo de escribir sobre un memo en la pantalla principal porque es el ejemplo mas sencillo, pero aplica a cualquier uso de un recurso compartido, en este caso el memo.


TCriticalSection

Antes de usar una sección critica hay que crearla, una unidad global puede ser una solución. Probablemente no es la mas elegante ni ortodoxa pero es la que aprendí y no he probado otra por tanto no me voy a arriesgar a proponer nada nuevo para mi.

unit uGlobal;
interface
uses SyncObjs;
var
  CSLog:TCriticalSection;
implementation
initialization
  CSLog := TCriticalSection.Create;
finalization
  CSLog.Free;
end.

Creada la sección critica esta lista para ser usada justo antes de acceder al recurso compartido...

procedure TAbstractServerModule.LogVisor(s: string);
begin
  CSLog.Enter;
  try
    FrmMain.LogVisor(s);
  finally
    CSLog.Leave;
  end;
end;

Ya que estamos, aprovecho mi ServerModule abstracto (ver http://pablosoligo.blogspot.com/2011/06/datasnap-dbexpress-y-threads.html)
para crear la función.Justo antes de usar el recurso compartido entro en la sección critica, justo luego de salir salgo de la misma.
Ahora todos los server modules puede escribir sobre el log, no importa que estén en distintos threads y que lo quieran hacer simultáneamente.

Recordar
  1. Try-Finally. No podemos dejar una sección critica sin salida, entonces si entro y siempre salgo
  2. Hacer la sección critica lo mas corta posible para favorecer paralelismo y multitarea 
En la medida que se avance en mas aspectos de desarrollo datasnap se daran mas usos a las secciones criticas. En breve el código disponible aquí









domingo, 12 de junio de 2011

DataSnap, DBExpress y Threads

El primer problema al que se enfrenta el desarrollador DataSnap principiante es el problema de las conexiones a la base de datos. La costumbre le hace rápidamente cometer un error. En su afán por ahorrar conexiones es sumamente probable que cree un datamodule y redireccione todas sus datasets a la conexión disponible en el mismo. Acá es donde aparece el problema, dependiendo del lifecycle seleccionado en el componente TDSServerClass es altamente probable que varios threads intenten usar esta conexión, dbexpress no es thread safe y para peor podemos generar una buena cantidad de problemas si todos los thread manejan las mismas conexiones, active recordset etc.

Solución:

Una conexión por ServerModule.
Esto abre un interrogante, ¿Tengo que tomarme el trabajo de meter una conexión y configurara directamente en cada uno de mis servermodules?¿Tengo que generar el código para tomar los parámetros del conexión en cada uno de mis servermodules?
La respuesta es no, podemos usar la herencia que tan bien funciona en Delphi para codificar una vez y reutilizar cuando se necesite.De hecho esta saludable practica no solo va a aportar soluciones a este problema, nos va a ayudar en otras problemáticas que aparecerán en escenarios futuros cuando profundicemos mas en esta tecnología. En futuras entradas podremos sacar mas provecho de lo expuesto aquí.
Vamos paso paso como hacer esto.

Crear el servidor datasnap
Si lo desea puede utilizar el wizard de delphi














Seleccione la opción VCL Forms Application, Habilite TCP/IP y HTTP (Por las dudas si lo necesita en el futuro, para esto no es necesario), y en la ultima acción del wizard seleccione TDSServerModule.
El servermodule creado automáticamente por el wizard solo servirá a los fines de heredar de el los servermodules que verdaderamente ofrecerán la funcionalidad, coloque un nombre que demuestre su condicion por ejemplo
uAbstractServerModule para el archivo.
TAbstractServerModule para la clase.

Verifique que dicho servermodule no figure como auto creado (Project->Options->Forms)

Vale aclarar que este server module "abstracto" en realidad no lo es tal, el nombre indica su condicion donde del software pero no es una clase abstracta.

Conexión en el ServerModule Abstracto
Lo llamamos abstracto por nunca se va a crear una instancia directamente sino que sera por medio de servermodules hijos. A pesar de esto tenemos cosas que hacer en modulo, comenzamos configurando una conexión en el dataexplorer y haciendo drag and drop












































Hagamos uso del evento beforeconnect de la conexión para levantar los parámetros de la misma en tiempo de ejecución















Usemos también los eventos de creación y destrucción del servermodule para abrir y cerrar conexión respectivamente















Ahora si, casi estamos, nos quedan algunos detalles pero vamos a crear nuestro primer servermodule "real", para ello seleccionamos File->New->Other->Inheritable items

















Seleccionado claro, como base nuestro servermodule abstracto.
Para ir terminando, creamos una archivo ini en la misma carpeta donde esta nuestros servidor DataSnap, copiamos los parametros de conexión de dbxConnections.ini como indica la figura.














Si se trabaja con SqlServer no olvidar el parametro MARS_Connection para habilitar "multiple active record set"















Ahora creamos el cliente, verificamos que los métodos en la clase abstracta están presentes en la clase proxy generada, el sistema de herencia funciona.


















Como dijimos en próximas entradas veremos mas ventajas de operar con herencia en servermodules