05 octubre 2007

Creando consultas SQL con parámetros

En el artículo anterior vimos como realizar consultas SQL para INSERT, DELETE, UPDATE y SELECT utilizando el componente IBSQL que forma parte de la paleta de componentes IBExpress.

También quedó muy claro que la velocidad de ejecución de consultas con este componente respecto a otros como IBQuery es muy superior. Todo lo que hemos visto esta bien para hacer consultas esporádicas sobre alguna tabla que otra, pero ¿que ocurre si tenemos que realizar miles de consultas SQL de una sola vez?

UTILIZANDO UNA TRANSACCION POR CONSULTA

Supongamos que tenemos que modificar el nombre de 1000 registros de la tabla CLIENTES:

var
i: Integer;
dwTiempo: DWord;
begin
with Consulta do
begin
////////////// METODO LENTO ////////////////

dwTiempo := TimeGetTime;

for i := 1 to 1000 do
begin
SQL.Clear;
SQL.Add( 'UPDATE CLIENTES' );
SQL.Add( 'SET NOMBRE = ' + QuotedStr( 'NOMBRE CLIENTE Nº ' + IntToStr( i ) ) );
SQL.Add( 'WHERE ID = ' + IntToStr( i ) );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

ShowMessage( 'Tiempo: ' + IntToStr( TimeGetTime - dwTiempo ) + ' milisegundos' );
end;
end;

Como puede verse arriba, por cada cliente actualizado he generado una SQL distinta abriendo y cerrando una transacción para cada registro. He utilizado la función TimeGetTime que se encuentra en la unidad MMSystem para calcular el tiempo que tarda en actualizarme el nombre de los 1000 clientes. En un PC con Pentium 4 a 3 Ghz, 1 GB de RAM y utilizando el motor de bases de datos Firebird 2.0 me ha tardado 4167 milisegundos.

Aunque las consultas SQL van muy rápidas con los componentes IBSQL aquí el fallo que cometemos es que por cada registro actualizado se abre y se cierra una transacción. En una base de datos local no se nota mucho pero en una red local con muchos usuarios trabajando a la vez le puede pegar fuego al concentrador.

Lo ideal sería poder modificar la SQL pero sin tener que cerrar la transacción. Como eso no se puede hacer en una consulta que esta abierta entonces hay que utilizar los parámetros. Los parámetros (Params) nos permiten enviar y recoger información de una consulta SQL que se esta ejecutando sin tener que cerrarla y abrila.

UTILIZANDO PARAMETROS EN LA CONSULTA

Para introducir parámetros en una consulta SQL hay que añadir dos puntos delante del parámetro. Por ejemplo:

UPDATE CLIENTES
SET NOMBRE = :NOMBRE
WHERE ID = :ID

Esta consulta tiene dos parámetros: ID y NOMBRE. Los nombres de los parámetros no tienen porque coincidir con el nombre del campo. Bien podrían ser así:

UPDATE CLIENTES
SET NOMBRE = :NUEVONOMBRE
WHERE ID = :IDACTUAL

De este modo se pueden modificar las condiciones de la consulta SQL sin tener que cerrar la transacción. Después de crear la consulta SQL hay que llamar al método Prepare para que prepare la consulta con los futuros parámetros que se le van a suministrar (no es obligatorio pero si recomendable). Veamos el ejemplo anterior utilizando parámetros y una sóla transacción para los 1000 registros:

var
i: Integer;
dwTiempo: DWord;
begin
with Consulta do
begin
////////////// METODO RÁPIDO ////////////////

dwTiempo := TimeGetTime;

Transaction.StartTransaction;

SQL.Clear;
SQL.Add( 'UPDATE CLIENTES' );
SQL.Add( 'SET NOMBRE = :NOMBRE' );
SQL.Add( 'WHERE ID = :ID' );
Prepare;

for i := 1 to 1000 do
begin
Params.ByName( 'NOMBRE' ).AsString := 'NOMBRE CLIENTE Nº '+ IntToStr( i );
Params.ByName( 'ID' ).AsInteger := i;
ExecQuery;
end;

try
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;

ShowMessage( 'Tiempo: ' + IntToStr( TimeGetTime - dwTiempo ) + ' milisegundos' );
end;
end;

En esta ocasión me ha tardado sólo 214 milisegundos, es decir, se ha reducido al 5% del tiempo anterior sin saturar al motor de bases de datos abriendo y cerrando transacciones sin parar.

Este método puede aplicarse también para consultas con INSERT, SELECT y DELETE. En lo único en lo que hay que tener precaución es en no acumular muchos datos en la transacción, ya que podría ser peor el remedio que la enfermedad.

Si teneis que actualizar cientos de miles de registros de una sola vez, recomiendo realizar un Commit cada 1000 registros para no saturar la memoria caché de la transacción. Todo depende del número de campos que tengan las tablas así como el número de registros a modificar. Utilizad la función TimeGetTime para medir tiempos y sacar conclusiones.

Y si el proceso a realizar va a tardar más de 2 o 3 segundos utilizar barras de progreso e hilos de ejecución, ya que algunos usuarios neuróticos podrían creer que nuestro programa se ha colgado y empezarían ha hacer clic como posesos (os aseguro que existe gente así, antes de que termine la consulta SQL ya te están llamando por teléfono echándote los perros).

Pruebas realizadas con Firebird 2.0 y Dephi 7.

04 octubre 2007

Creando consultas SQL rápidas con IBSQL

Cuando se crea un programa para el mantenimiento de tablas de bases de datos (clientes, artículos, etc.) el método ideal es utilizar componentes ClientDataSet como vimos anteriormente.

Pero hay ocasiones en las que es necesario realizar consultas rápidas en el servidor tales como incrementar existencias en almacén, generar recibos automáticamente o incluso incrementar nuestros contadores de facturas sin utilizar generadores.

En ese caso el componente más rápido para bases de datos Interbase/Firebird es IBSQL el cual permite realizar consultas SQL sin que estén vinculadas a ningún componente visual. Vamos a ver unos ejemplos de inserción, modificación y eliminación de registros utilizando este componente.

INSERTANDO REGISTROS EN UNA TABLA

Aquí tenemos un ejemplo de insertar un registro en una tabla llamada CLIENTES utilizando un objeto IBSQL llamado Consulta:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'INSERT INTO CLIENTES' );
SQL.Add( '( NOMBRE, NIF, IMPORTEPTE )' );
SQL.Add( 'VALUES' );
SQL.Add( '( ''ANTONIO GARCIA LOPEZ'', ''46876283D'', 140.23 )' );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

Como puede apreciarse hemos tenido que abrir nosotros a mano la transacción antes de ejecutar la consulta ya que el objeto IBSQL no la abre automáticamente tal como ocurre en los componentes IBQuery.

Una vez ejecutada la consulta, si todo ha funcionado correctamente enviamos la transacción al servidor mediante el método Commit. En el caso de que falle mostramos el error y cancelamos la transacción utilizando el método RollBack.

MODIFICANDO LOS REGISTROS DE UNA TABLA

El método para modificar los registros es el mismo que para insertarlos:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'UPDATE CLIENTES' );
SQL.Add( 'SET NOMBRE = ''MARIA GUILLEN ROJO'',' );
SQL.Add( 'NIF = ''69236724W'', ' );
SQL.Add( 'IMPORTEPTE = 80.65' );
SQL.Add( 'WHERE ID=21963' );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

ELIMINANDO REGISTROS DE LA TABLA

Al igual que para las SQL para INSERT y UPDATE el procedimiento es el mismo:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'DELETE FROM CLIENTES' );
SQL.Add( 'WHERE ID=21964' );

Transaction.StartTransaction;

try
ExecQuery;
Transaction.Commit;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;
end;

CONSULTANDO LOS REGISTROS DE UNA TABLA

El método de consultar los registros de una tabla mediante SELECT difiere de los que hemos utilizado anteriormente ya que tenemos que dejar la transacción abierta hasta que terminemos de recorrer todos los registros:

with Consulta do
begin
SQL.Clear;
SQL.Add( 'SELECT * FROM CLIENTES' );
SQL.Add( 'ORDER BY ID' );

Transaction.StartTransaction;

// Ejecutarmos consulta
try
ExecQuery;
except
on E: Exception do
begin
Application.MessageBox( PChar( E.Message ), 'Error de SQL', MB_ICONSTOP );
Transaccion.Rollback;
end;
end;

// Recorremos los registros
while not Eof do
begin
Memo.Lines.Add( FieldByName( 'NOMBRE' ).AsString );
Next;
end;

// Cerramos la consulta y la transacción
Close;
Transaction.Active := False;
end;

Aunque pueda parecer un coñazo el componente de la clase TIBSQL respesto a los componentes TIBTable o TIBQuery, su velocidad es muy superior a ambos componentes, sobre todo cuando se ejecutan las consultas sucesivamente.

En el próximo artículo veremos cómo utilizar parámetros en las consultas.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

03 octubre 2007

Como poner una imagen de fondo en una aplicación MDI

En un artículo anterior vimos como crear aplicaciones MDI gestionando múltiples ventanas hijas dentro de la ventana padre.

Una de las cosas que más dan vistosidad a una aplicación comercial es tener un fondo con nuestra marca de fondo de la aplicación (al estilo Contaplus o Facturaplus).

Para introducir una imagen de fondo en la ventana padre MDI hay que hacer lo siguiente:

- Introducir en la ventana padre (la que tiene la propiedad FormStyle a MDIForm) un componente de la clase TImage situado en la pestaña Additional. Al componenente lo vamos a llamar Fondo.

- En dicha imagen vamos a cambiar la propidedad Align a alClient para que ocupe todo el fondo del formulario padre.

- Ahora sólo falta cargar la imagen directamente:

Fondo.Picture.LoadFromFile( 'c:\imagenes\fondo.bmp' );

El único inconveniente que tiene esto es que no podemos utilizar los eventos del formulario al estar la imagen encima (Drag and Drop, etc).

UTILIZANDO EL CANVAS

Otra forma de hacerlo sería poniendo el objeto TImage en medio del formulario pero de manera invisible (sin alClient). Después en el evento OnPaint del formulario copiamos el contenido de la imagen TImage al fondo del formulario:

procedure TFormulario.FormPaint( Sender: TObject );
var R: TRect;
begin
R.Left := 0;
R.Top := 0;
R.Right := Fondo.Width;
R.Bottom := Fondo.Height;
Canvas.CopyRect( R, Fondo.Canvas, R );
end;

Así podemos tener igualmente una imagen de fondo sin renunciar a los eventos del formulario (OnMouseMove, OnClick, etc.).

Pruebas realizadas en Dephi 7.

02 octubre 2007

La potencia de los ClientDataSet (y V)

Antes de proceder a crear el código para el botón Completar vamos a hacer que la conexión del programa con la base de datos sea algo más flexible.

Hasta ahora nuestro objeto IBDatabase está conectado directamente a una ruta fija donde tenemos el archivo BASEDATOS.FDB. Pero si cambiamos de ruta el programa dejará de funcionar provocando un fallo de conexión.

Para evitar esto vamos a hacer que el programa conecte con la base de datos al mostrar el formulario principal FClientes. Para ello en el evento OnShow de dicho formulario ponemos lo siguiente:

procedure TFClientes.FormShow( Sender: TObject );
begin
AccesoDatos.BaseDatos.DatabaseName := '127.0.0.1:' + ExtractFilePath( Application.ExeName ) + 'BASEDATOS.FDB';

try
AccesoDatos.BaseDatos.Open;
except
raise;
end;

LogicaNegocio.TLstClientes.Active := True;
end;

Esto hace que conecte con la base de datos que está al lado de nuestro ejecutable. Así hacemos que nuestro programa sea portable (a falta de instalarle el motor de bases de datos correspondiente). Si la conexión con la base de datos es correcta entonces abrimos la tabla del listado de clientes (TLstClientes).

Cuando se trata de una base de datos Interbase la conexión puede ser local o remota. Si es local no es necesario poner la IP:

AccesoDatos.BaseDatos.DatabaseName := 'C:\MiPrograma\BaseDatos.gdb';

Aunque viene a ser lo mismo que hacer esto:

AccesoDatos.BaseDatos.DatabaseName := '127.0.0.1:C:\MiPrograma\BaseDatos.gdb';

Se trata de una conexión remota aunque estemos accediendo a nuestro mismo equipo. Si se tratara de otro equipo de la red habría que hacer lo mismo:

AccesoDatos.BaseDatos.DatabaseName := '192.168.0.1:C:\MiPrograma\BaseDatos.gdb';

Para bases de datos Firebird no existe la conexión local, siempre es remota. Así que si vamos a conectar con nuestro equipo en local habría que hacerlo así:

AccesoDatos.BaseDatos.DatabaseName := '127.0.0.1:C:\MiPrograma\BaseDatos.fdb';

Yo recomiendo utilizar siempre la conexión remota utilizando para ello un archivo INI al lado de nuestro programa que contenga la IP del servidor. Por ejemplo:

[CONEXION]
IP=127.0.0.1

Así podemos hacer que nuestro programa se conecte en local o remoto sin tener que volver a compilarlo. Pero que no se os olvide dejar desconectado el componente IBDatabase en el módulo de acceso a datos AccesoDatos porque si no lo primero que ha a hacer es conectarse al arrancar el programa provocando un error.

DANDO DE ALTA CLIENTES DE FORMA MASIVA

Para probar el rendimiento de un programa no hay nada mejor que darle caña metiendo miles y miles de registros a la base de datos. Para ello voy a crear un procedimiento dentro del botón Completar que le preguntará al usuario cuandos clientes desea crear. Al pulsar Aceptar dará de alta todos esos registros mostrando el progreso en la barra de progreso que pusimos anteriormente:

procedure TFClientes.BCompletarClick( Sender: TObject );
var
sNumero: string;
i, iInventado: Integer;
begin
sNumero := InputBox( 'Nº de clientes', 'Rellenando clientes', '' );
Randomize; // Inicializamos el generador de números aleatorios

if sNumero <> '' then
begin
with LogicaNegocio do
begin
TClientes.Open;
Progreso.Max := StrToInt( sNumero );
Progreso.Visible := True;
TLstClientes.DisableControls;

for i := 1 to StrToInt( sNumero ) do
begin
iInventado := Random( 99999999 ) + 1000000; // Nos invertamos un número identificador de cliente
TClientes.Insert;
TClientesNOMBRE.AsString := 'CLIENTE Nº ' + IntToStr( iInventado );
TClientesNIF.AsString := IntToStr( iInventado );
TClientesDIRECCION.AsString := 'CALLE Nº ' + IntToStr( iInventado );
TClientesPOBLACION.AsString := 'POBLACION Nº ' + IntToStr( iInventado );
TClientesPROVINCIA.AsString := 'PROVINCIA Nº ' + IntToStr( iInventado );
iInventado := Random( 79999 ) + 10000; // Nos inventamos el código postal
TClientesCP.AsString := IntToStr( iInventado );
TClientesIMPORTEPTE.AsFloat := 0;
TClientes.Post;
Progreso.Position := i;
Application.ProcessMessages;
end;

TClientes.Close;
Progreso.Visible := False;
TLstClientes.EnableControls;
end;
end;
end;

Lo que hemos hecho es inventar el nombre de usuario, dirección, NIF, etc. También he desconectado y he vuelto a conectar la tabla TLstClientes de la rejilla utilizando los métodos DisableControls y EnableControls para ganar más velocidad. La barra de progreso llamada Progreso mostrará la evolución del alta de registros.

Realmente es una burrada dar de alta masivamente registros utilizando objetos ClientDataSet ya que están pensados para utilizarlos para altas, modificaciones y eliminación de registros de uno a uno y sin mucha velocidad. Si quereis hacer una inserción masiva de registros hay que realizar consultas SQL con INSERT utilizando el objetos de la clase TIBSQL que es mucho más rápido que los ClientDataSet. Ya explicaré en otro momento la ventaja de utilizar dichos componentes.

Con esto finalizamos la introducción a los componentes ClientDataSet.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

01 octubre 2007

La potencia de los ClientDataSet (IV)

Ya tenemos todo lo necesario para realizar el programa:

- La base de datos firebird: BASEDATOS.FDB

- La capa de acceso a datos en el módulo de datos: AccesoDatos.

- La lógica de negocio en el módulo de datos: LogicaNegocio.

CREANDO EL FICHERO GENERAL DE CLIENTES

El primer formulario que vamos a crear se va a llamar FClientes y va a contener el listado general de clientes:


Va a contener los siguientes componentes:

- 4 botones de la clase TButton para dar de alta clientes, modificarlos y eliminarlos. Habrá otro llamado BCompletar que utilizaremos más adelante para dar de alta clientes de forma masiva.

- Una rejilla de datos de la clase TDBGrid que va a contener 3 columnas para el listado del cliente: ID, NOMBRE y NIF. Su nombre va a ser ListadoClientes.

- Un componente DataSource (pestaña Data Access) llamado DSLstClientes que va a encargarse de suministrar datos a la rejilla.

- Un componente DBNavitator (pestaña Data Controls) para hacer pruebas con los registros. Lo llamaremos Navegador.

- Una barra de progreso a la izquierda del DBNavigator de la clase TProgressBar que mostrará el progreso de los clientes dados de alta automáticamente a través del botón BCompletar.

Ahora hay que vincular los componentes como corresponde:

- Lo primero es añadir en la sección uses el módulo de datos LogicaNegocio para poder vincular el listado de clientes a la rejilla.

- Vinculamos el componente DSLstClientes con el ClientDataSet llamado TLstClientes a través de su propiedad DataSet.

- En la propiedad DataSource de la rejilla de datos ListadoClientes asignamos el componente DSLstClientes.

- Vinculamos el componente Navegador al componente DSLstClientes mediante su propiedad DataSource.

Más adelante escribiremos código para cada uno de los botones.

CREANDO EL FORMULARIO DEL CLIENTE

Vamos a crear el siguiente formulario llamado FCliente:


Va a constar de tantos componentes de la clase TLabel y TDBEdit como campos tenga la tabla clientes. Todos los campos son modificables a excepción del ID que lo he puesto con su propiedad Enabled a False. También lo he oscurecido podiendo de Color el valor clBtnFace.

Para vincular los campos a la tabla real de clientes introducimos un componente DataSource llamado DSClientes, pero antes hay que añadir en la sección uses el módulo de datos LogicaNegocio.

Ahora se vincula el componente DSClientes con el ClientDataSet llamado TClientes situado en el módulo de datos LogicaNegocio a través de su propiedad Dataset. Con esto ya podemos vincular cada campo TDBEdit con el DataSource DSClientes y con su campo correspondiente especificado en la propiedad DataField.

Cuando se pulse el botón Aceptar ejecutamos el código:

procedure TFCliente.BAceptarClick( Sender: TObject );
begin
LogicaNegocio.TClientes.Post;
ModalResult := mrOk;
end;

Al botón BAceptar le ponemos en su propiedad ModalResult el valor mrNone. Esto tiene su explicación. Cuando pulsemos Aceptar, si el usuario no ha rellenado correctamente algún dato saltará un error definido en los Constraint y no hará nada, evitamos que se cierre el formulario. Así obligamos al usuario a introducir los datos correctamente. Sólo cuando el Post se realice correctamente le diremos al formulario que el resultado es mrOk para que pueda cerrarse.

En el botón BCancelar con sólo asignar en su propiedad ModalResult el valor mrCancel no será necesario hacer nada más.

Sólo una cosa más: si utilizais el teclado numérico en los campos de tipo Float vereis que la tecla decimal no funciona (aquí en España). Nos obliga a utilizar la coma que está encima de la barra de espacio. Para transformar el punto en coma yo lo que suelo hacer es lo siguiente (para el campo IMPORTEPTE):

procedure TFCliente.IMPORTEPTEKeyPress( Sender: TObject; var Key: Char );
begin
if key = '.' then
key := ',';
end;

Es decir, en el evento OnKeyPress cuando el usuario pulse el punto lo transformo en una coma. Si tuvieramos más campos float sólo habría que reasignar el mismo eventos al resto de campos.

Con esto ya tenemos terminado el formulario del cliente.

TERMINANDO EL FICHERO GENERAL DE CLIENTES

Una vez que tenemos la ficha del cliente vamos a terminar el mantenimiento de clientes asignado el código a cada botón. Comencemos con el botón Nuevo:

procedure TFClientes.BNuevoClick( Sender: TObject );
begin
LogicaNegocio.TClientes.Open;
LogicaNegocio.TClientes.Insert;
Application.CreateForm( TFCliente, FCliente );
FCliente.ShowModal;
LogicaNegocio.TClientes.Close;
end;

Como puede apreciarse abrimos la tabla TClientes sólo cuando hay que dar de alta un cliente o modificarlo. Después la cerramos ya que para ver el listado de clientes tenemos nuestra tabla TLstClientes.

Para el botón Modificar es muy parecido:

procedure TFClientes.BModificarClick( Sender: TObject );
begin
LogicaNegocio.TClientes.Params.ParamByName( 'ID' ).AsString := LogicaNegocio.TLstClientesID.AsString;
LogicaNegocio.TClientes.Open;
LogicaNegocio.TClientes.Edit;
Application.CreateForm( TFCliente, FCliente );
FCliente.ShowModal;
LogicaNegocio.TClientes.Close;
ListadoClientes.SetFocus;
end;

Aquí hay que detenerse para hablar de algo importante. Cuando abrimos una tabla por primera vez el cursor SQL dentro del motor de bases de datos se va al primer registro. ¿Cómo hacemos para ir al registro seleccionado por TLstClientes? Pues le tenemos que pasar el ID del cliente que queremos editar.

Esto se hace utilizando parámetros, los cuales hay que definirlos dentro del componente ClientDataSet llamado TClientes que está en el módulo de datos LogicaNegocio. Generalmente cuando se pulsa CTRL + A para traernos los campos de la tabla al ClientDataSet se dan de alta los parámetros. Como el componente TClientes está vinculado al IBQuery Clientes que tiene la SQL:

SELECT * FROM CLIENTES
WHERE ID=:ID

entonces al pulsar CTRL + A en el ClientDataSet nos da de alta automáticamente el parámetro ID. Si no fuera así, tendríamos que ir al componente TClientes, pulsar el botón [...] en su propiedad Params y dar de alta un parámetro con las propiedades:

DataType: ftInteger
Name: ID
ParamType: ptInput

Los parámetros dan mucha velocidad a un programa porque permiten modificar las opciones de la SQL sin tener que cerrar y abrir de nuevo la consulta, permitiendo saltar de un registro a otro dentro de una misma tabla a una velocidad impresionante.

Y por último introducimos el código correspondiente al botón Eliminar:

procedure TFClientes.BEliminarClick( Sender: TObject );
begin
with LogicaNegocio.TLstClientes do
if RecordCount > 0 then
if Application.MessageBox( '¿Desea eliminar este cliente?', 'Atención',
MB_ICONQUESTION or MB_YESNO ) = ID_YES then
Delete;

ListadoClientes.SetFocus;
end;

Antes de eliminar el registro nos aseguramos que de tenga algún dato y preguntamos al usuario si esta seguro de eliminarlo.

Con esto finalizamos nuestro mantenimiento de clientes a falta de utilizar el botón Completar donde crearemos un bucle que dará de alta tantos clientes como deseemos para probar la velocidad de la base de datos. Esto lo haremos en el próximo artículo.

Pruebas realizadas en Firebird 2.0 y Delphi 7.

Publicidad