Mostrando entradas con la etiqueta DBExpress. Mostrar todas las entradas
Mostrando entradas con la etiqueta DBExpress. Mostrar todas las entradas

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.





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.