Los componentes ClientDataSet y DataSetProvider simplifican enormemente el desarrollo de aplicaciones de bases de datos cliente/servidor al introducir una capa de abstracción entre la interface de usuario y los componentes de acceso a datos.
El ClientDataSet es como una pequeña base de datos relacional que mantiene los datos en memoria. Estos datos los obtiene, en la mayoría de los casos, de un DataSetProvider. El DataSetProvider cumple dos funciones principales: proveer datos y resolver actualizaciones. Casi siempre los datos que provee los obtiene de un DataSet, el cual debe implementar la interface IProviderSupport. El ClientDataSet mantiene en memoria no sólo los datos que obtiene del DataSetProvider sino también las modificaciones realizadas por el usuario. El método ApplyUpdates del ClientDataSet inicia el proceso de actualización el cual es llevado a cabo por el DataSetProvider.
Muchas veces es necesario personalizar el proceso de actualización para realizar tareas adicionales necesarias para implementar la lógica de negocio de nuestra aplicación. En este artículo veremos cómo personalizar el proceso de actualización.
Nuestro ejemplo
Antes de comenzar a añadir componentes y escribir código veamos de qué se trata el proyecto que desarrollaremos a modo de ejemplo. El código fuente puede descargarse aquí.
Vamos a utilizar las tablas Orders.DB, Items.DB y Parts.DB del alias DBDemos del BDE para ingresar órdenes. Los campos OnHand y OnOrder de la tabla Parts mantienen, para cada producto, la cantidad disponible y la cantidad comprometida respectivamente. Cada vez que el usuario inserta, modifica o elimina una orden debemos actualizar el campo OnOrder de la tabla Parts para cada una de las líneas que componen la orden. También debemos verificar que la cantidad remanente (OnHand - OnOrder) sea suficiente para satisfacer la cantidad ordenada. En caso de que no lo sea, debemos generar una excepción y la orden debe ser rechazada.
Para simplificar el proyecto actualizaremos sólo los campos requeridos de la tabla Orders, es decir, los campos OrderNo, CustNo y EmpNo. Para facilitar las cosas mostraremos los datos de las tablas Customer, Employee y Parts así podremos, por ejemplo, consultar un número de cliente valido o verificar la cantidad disponible de un producto antes y después de insertar una orden.
Manos a la obra
Vamos a comenzar por crear un proyecto nuevo. Seleccionar del menú File | New | Application. Colocaremos los componentes de datos en un Data Module, así que seleccionar del menú File | New | Data Module. Guardar el proyecto seleccionando del menú File | Save All.
El Data Module
Añadir los siguientes componentes al Data Module:
- De la página BDE, un Database, tres Table y tres Query.
- De la página Data Access, un DataSource, tres ClientDataSet y dos DataSetProvider.
Nombrar los componentes como lo muestra la siguiente imagen de pantalla del Data Module.
Como dije antes, vamos a utilizar el alias DBDemos del BDE. Asignarle a la propiedad DatabaseName del componente DB_DBDemos el valor "_DBDemos" y a la propiedad LoginPrompt el valor "False". Asignarle a la propiedad DatabaseName de los componentes Table y Query el valor "_DBDemos".
Utilizaremos los componentes Table para acceder a las tablas Customer, Employee y Parts respectivamente. Por lo tanto, asignarle a la propiedad TableName del componente TBLCustomer el valor "Customer.db", a TBLEmployee "Employee.db" y a TBLParts "Parts.db".
La sentencia SQL para acceder a la tabla Orders, que corresponde al valor de la propiedad SQL del componente QRYOrders, es la siguiente:
SELECT OrderNo, CustNo, EmpNoFROM OrdersORDER BY OrderNo DESC
Sólo vamos a actualizar los campos requeridos. La sentencia SQL para acceder a la tabla Items, que corresponde al valor de la propiedad SQL del componente QRYItems, es la siguiente:
SELECT OrderNo, ItemNo, PartNo, QtyFROM ItemsWHERE OrderNo = :OrderNoORDER BY ItemNo
Asignarle a la propiedad DataSet del componente DSOrders el valor "QRYOrders" y a la propiedad DataSource del componente QRYItems el valor "DSOrders". Esto nos permite establecer la relación maestro/detalle entre las tablas Orders e Items. También tenemos que configurar el parámetro OrderNo de la sentencia SQL para acceder a la tabla Items. Es posible acceder a los parámetros de un componente Query por medio de su propiedad Params. Asignarle a la propiedad DataType del parámetro OrderNo el valor "ftFloat" y a la propiedad ParamType el valor "ptInput".
La sentencia SQL para acceder a la tabla Parts, que corresponde al valor de la propiedad SQL del componente QRYParts, es la siguiente:
SELECT PartNo, Description, OnHand, OnOrderFROM PartsWHERE PartNo = :PartNo
Tenemos que configurar el parámetro PartNo de la sentencia SQL para acceder a la tabla Parts. Asignarle a la propiedad DataType del parámetro PartNo el valor "ftFloat" y a la propiedad ParamType el valor "ptInput".
Asignarle a la propiedad DataSet del componente DSPOrders el valor "QRYOrders" y a la misma propiedad del componente DSPParts el valor "QRYParts". Asignarle a la propiedad ProviderName del componente CDSOrders el valor "DSPOrders". El DataSetProvider detecta la relación maestro/detalle entre QRYOrders y QRYItems y crea una estructura en la cual añade un campo al maestro que contiene los registros del detalle. El nombre de este campo lo obtiene del nombre del DataSet detalle. Crear campos persistentes para el componente CDSOrders. Además de los campos de la tabla Orders aparece el campo QRYItems. Este es un campo especial de tipo TDataSetField. Asignarle a la propiedad DataSetField del componente CDSItems el valor "QRYItems". Crear también campos persistentes para el componente CDSItems (es posible que esto demore más de lo normal). Por último, asignarle a la propiedad ProviderName del componente CDSParts el valor "DSPParts" y crear campos persistentes para el componente CDSParts.
El siguiente grafico muestra todas las relaciones que hemos establecido.
Asegurarse que la propiedad Active de todos los DataSet y del Database tengan el valor "False". Guardar el proyecto. Ahora pasemos al formulario principal.
El formulario principal
Añadir a la cláusula uses de la unidad del formulario principal la unidad del Data Module. Añadir los siguientes componentes al formulario principal:
- De la página Win32, un PageControl.
Asignarle a la propiedad Align del PageControl el valor "alClient". Crear 4 páginas para el PageControl haciendo clic con el botón derecho del ratón sobre el PageControl y seleccionando del menú contextual la opción New Page. Asignarle a la propiedad Caption de cada una de ellas los valores "Orders", "Customer", "Employee" y "Parts" respectivamente. Ahora, añadir los siguientes componentes:
- En la primera página:
- De la página Additional, un ScrollBox y un Splitter.
- De la página Data Controls, un DBGrid y un DBNavigator.
- En cada una de las tres páginas restantes:
- De la página Data Controls, un DBGrid y un DBNavigator.
Vamos a configurar los componentes que hemos añadido en cada página. Comencemos por lo más fácil. En cada una de las últimas tres páginas hemos añadido un DBGrid y un DBNavigator. Pues bien, asignarle a la propiedad Align del DBNavigator el valor "alTop" y a la misma propiedad del DBGrid el valor "alClient".
En la primera página hemos añadido más componentes. Asignarle a la propiedad Align del ScrollBox y del Splitter el valor "alTop". Asignarle a la propiedad Align del DBNavigator el valor "alBottom" y a la misma propiedad del DBGrid el valor "alClient". Añadir dentro del ScrollBox los siguientes componentes:
- De la página Standard, un Button.
- De la página Data Controls, un DBNavigator.
- Arrastrar y soltar dentro del ScrollBox los campos del CDSOrders del Data Module.
Asignarle a la propiedad Align del DBNavigator que hemos añadido dentro del ScrollBox el valor "alBottom" y a la propiedad Caption del Button el valor "ApplyUpdates". Al haber arrastrado y soltado los campos del CDSOrders, Delphi ha añadido por nosotros un componente DataSource. Nombrar este componente como "DSOrders". Finalmente añadir los siguientes componentes:
- De la página Data Access, cuatro DataSource.
Nombrarlos "DSItems", "DSCustomer", "DSEmployee" y "DSParts" respectivamente. Relacionar el componente DSItems con el componente CDSItems por medio de la propiedad DataSet del primero. Relacionar DSCustomer con TBLCustomer, DSEmployee con TBLEmployee y DSParts con TBLparts.
Por último, relacionar el DBGrid y el DBNavigator de la primera página con el componente DSItems por medio de la propiedad DataSource de los dos primeros. Relacionar de la misma forma los componentes DBGrid y DBNavigator de las tres páginas restantes con su correspondiente DataSource.
El formulario principal, con la primera página seleccionada, debería verse así.
Guardar el proyecto. Escribir el siguiente código para el evento OnCreate del formulario principal:
procedure TForm1.FormCreate(Sender: TObject);begin DataModule2 := TDataModule2.Create(Self); with DataModule2 do begin CDSOrders.Open; CDSItems.Open; TBLCustomer.Open; TBLEmployee.Open; TBLParts.Open; end;end;
En el código para este evento simplemente creamos una instancia del Data Module y abrimos todos los DataSet que necesitamos. Asegurarse que el Data Module no esté en la lista Auto-create forms en las opciones del proyecto (Project | Options y luego la página Forms). Escribir el siguiente código para el evento OnDestroy del formulario principal:
procedure TForm1.FormDestroy(Sender: TObject);begin with DataModule2 do begin CDSOrders.Close; CDSItems.Close; TBLCustomer.Close; TBLEmployee.Close; TBLParts.Close; end;end;
El código de este evento es también muy simple. Sólo cerramos todos los DataSet que abrimos en el código del evento OnCreate.
Guardar el proyecto, compilarlo y ejecutarlo. Lo primero que observamos (al menos en mi caso es así) es que el formulario principal tarda mucho en aparecer. Esto se debe a que el CDSOrders está obteniendo todos los registros disponibles incluyendo los detalles. Podemos obtener un tiempo de respuesta mejor asignándole a la propiedad PacketRecords del componente CDSOrders el valor "2". De esta forma obtendrá de a 2 registros cada vez hasta que no hayan más registros disponibles.
Escribir el siguiente código para el evento OnClick del Button:
procedure TForm1.Button1Click(Sender: TObject);begin (DSOrders.DataSet as TClientDataSet).ApplyUpdates(0);end;
Para que este código compile es necesario añadir la unidad DBClient a la cláusula uses.
Hasta aquí hemos resuelto la lógica visual. Pasemos ahora a resolver la lógica de datos en el Data Module. Debemos personalizar el proceso de actualización del CDSOrders para actualizar CDSParts. Escribir el siguiente código para el evento AfterUpdateRecord del DSPOrders:
procedure TDataModule2.DSPOrdersAfterUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind);var oPartNo, oQty: TField;begin if SourceDS = QRYItems then begin oPartNo := DeltaDS.FieldByName('PartNo'); oQty := DeltaDS.FieldByName('Qty'); case UpdateKind of ukInsert: ActualizarParts(oPartNo.NewValue, oQty.NewValue); ukDelete: ActualizarParts(oPartNo.OldValue, oQty.OldValue * -1); ukModify: // El usuario puede modificar el producto y la cantidad begin if oPartNo.NewValue = Unassigned then begin if not (oQty.NewValue = Unassigned) then ActualizarParts(oPartNo.OldValue, oQty.NewValue - oQty.OldValue); end else begin ActualizarParts(oPartNo.OldValue, oQty.OldValue * -1); if VarIsNull(oQty.Value) then ActualizarParts(oPartNo.NewValue, oQty.OldValue) else ActualizarParts(oPartNo.NewValue, oQty.NewValue); end; end; end; end;end;
En primer lugar, verificamos que la tabla para la cual se ha actualizado un registro sea la que necesitamos, en este caso, QRYItems. Luego, dependiendo del tipo de actualización, llamamos al método ActualizarParts para que se encargue de actualizar la tabla Parts. Este método está declarado como private en la clase del Data Module y su implementación es la siguiente:
procedure TDataModule2.ActualizarParts(APartNo, AQty: double);begin CDSParts.Close; CDSParts.FetchParams; // Obtener parametros CDSParts.Params.ParamByName('PartNo').AsFloat := APartNo; CDSParts.Open; try if (CDSPartsOnHand.Value - (CDSPartsOnOrder.Value + AQty)) < 0 then raise Exception.CreateFmt('No es posible procesar esta orden' + #13#10 + 'Part: %.0f / %s' + #13#10 + 'OnHand: %f - OnOrder: %f = %f' + #13#10 + 'Cantidad ordenada: %f', [CDSPartsPartNo.Value, CDSPartsDescription.Value, CDSPartsOnHand.Value, CDSPartsOnOrder.Value, CDSPartsOnHand.Value - CDSPartsOnOrder.Value, AQty]); CDSParts.Edit; CDSPartsOnOrder.Value := CDSPartsOnOrder.Value + AQty; // Actualizar OnOrder CDSParts.Post; if CDSParts.ApplyUpdates(0) > 0 then raise Exception.CreateFmt('No es posible procesar esta orden' + #13#10 + 'Error al actualizar Part.OnOrder para %.0f / %s', [CDSPartsPartNo.Value, CDSPartsDescription.Value]); finally CDSParts.Close; end;end;
En este código hacemos lo siguiente. Primero cerramos CDSParts, luego obtenemos los parámetros (en este caso es sólo uno, PartNo), le asignamos el valor correspondiente y abrimos CDSParts. Inmediatamente después de esto verificamos la cantidad remanente. Aquí no es necesario verificar si el producto existe ya que la tabla Items tiene esta imposición y en este punto ya se ha actualizado. Si el producto no existiera entonces este código nunca sería ejecutado. Si la cantidad remanente no es suficiente, generamos una excepción. Si lo es, actualizamos el campo OnOrder y aplicamos las actualizaciones de CDSParts, luego de lo cual lo cerramos. Si por algún motivo ocurre un error al actualizar CDSParts el resultado del método ApplyUpdates sería mayor a cero. En ese caso generamos una excepción.
Debemos hacer algo más, aunque sólo sea para nuestros ojos. A pesar de que estamos generando una excepción nunca veremos el mensaje correspondiente ya que los componentes DataSerProvider y ClientDataSet involucrados se encargan de capturarla y transformarla en eventos. Escribir el siguiente código para el evento OnReconcileError de CDSOrders:
procedure TDataModule2.CDSOrdersReconcileError( DataSet: TCustomClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);begin Action := HandleReconcileError(DataSet, UpdateKind, E);end;
Para que este código compile es necesario añadir a la cláusula uses la unidad RecError. Ahora sí estamos en condiciones de hacer pruebas y ver lo que pasa. Guardar el proyecto, compilarlo y ejecutarlo.
Las pruebas
Según los datos de mis tablas, para el producto "900 / Dive kayak" el valor para el campo OnHand es "24" y para el campo OnOrder "16". Vamos a insertar una orden para pedir 9 unidades de este producto. La siguiente imagen de pantalla muestra la orden insertada antes de aplicar las actualizaciones.
Estos valores son válidos para los datos de mis tablas. Hemos añadido la posibilidad de consultar todos los valores que necesitamos por lo que para vosotros debería ser muy fácil encontrar valores validos según los datos de vuestras tablas. Al aplicar las actualizaciones obtengo el siguiente mensaje de error:
Si hacemos clic en el botón "OK", cerramos la aplicación y volvemos a ejecutarla podremos ver como los datos no han cambiado, es decir, las tablas Orders, Items y Parts no han sido modificadas. Esto se debe a que el componente DataSetProvider aplica las actualizaciones en el contexto de una transacción y si algo sale mal cancela todos los cambios realizados. En nuestro ejemplo, la línea de código en la que generamos la excepción es posterior a la grabación de las tablas Orders e Items, pero todo ello ocurre en el contexto de una transacción que, al generar una excepción, es finalizada rechazando todos los cambios.
Podemos hacer todo tipo de pruebas para verificar el correcto funcionamiento del proceso de actualización. Por ejemplo, podemos insertar una orden para un cliente o vendedor inexistente, para un producto inexistente, añadir varios ítems que no generen ningún error y un último ítem que sí lo haga, etc. En cualquier caso veremos que la operación de actualización es atómica, es decir, o se graba todo o no se graba nada.
Conclusiones
Debido a que el componente ClientDataSet almacena los cambios en memoria, desarrollar aplicaciones como las de este ejemplo es muy fácil. No tenemos que preocuparnos por las reglas de integridad referencial de la base de datos mientras el usuario inserta una orden. Eso sí, tenemos que controlar la concurrencia al momento de actualizar los cambios.
Por su parte, el componente DataSetProvider se encarga de resolver las actualizaciones generando automáticamente sentencias SQL para INSERT, DELETE y UPDATE. También es lo suficientemente listo como para resolver las actualizaciones en el orden correcto, es decir, si insertamos, primero el maestro y luego el detalle, y si eliminamos, primero el detalle y luego el maestro. Además, nos olvidamos completamente de gestionar transacciones ya que el DataSetProvider lo hace por nosotros.
El proceso de actualización del DataSetProvider puede ser totalmente personalizado a través de sus propiedades y eventos. Esta personalización puede ser tan simple como indicarle al DataSetProvider que no actualice un campo, parcial, como la de este ejemplo, o completa, al punto de implementar nuestro propio proceso de actualización.
No hay comentarios:
Publicar un comentario