Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Vistas y triggers

... o triggers y vistas. ¿Y qué tiene que ver la gimnasia con la magnesia? Tiene que ver, y mucho, con la única condición de que estemos trabajando con Oracle o InterBase. La técnica que le voy a explicar es my sencilla, y pudiera resumirla en un párrafo y un diagrama de sintaxis. Pero quiero también que comprenda todo el partido que se le puede sacar a estos recursos. Tendremos que rebobinar la película y comenzar desde la primera escena.

TODO COMENZO POR...

...las actualizaciones en caché. Muchos programadores piensan que la principal utilidad de las actualizaciones en caché es agrupar determinado número de actualizaciones y enviarlas de golpe al servidor. Los manuales de Borland nos explican que de este modo ahorramos espacio en la transmisión de datos al servidor, y que así podemos disminuir el tráfico en red. Todo esto es verdad, pero no toda la verdad; ni siquiera es la parte más importante de la verdad.

Probablemente, el mayor atractivo de las actualizaciones en caché para el programador experto sea el uso del componente TUpdateSQL y del evento OnUpdateRecord. Estoy seguro de que alguna vez se habrá tropezado con uno de esos profetas que predican: "No hay más Conjunto de Datos que TQuery". Casi siempre, sin embargo, a estos personajes se les olvida aclarar qué condiciones son necesarias para que un componente TQuery se comporte mejor que un TTable.

Supongamos que estamos programando el mantenimiento de una tabla. Traemos una consulta Query1 al formulario, le asignamos un "select * from Tabla" en su propiedad SQL, asignamos True a la propiedad RequestLive y le enchufamos los correspondientes controles visuales: rejillas, cuadros de edición o lo que se le antoje. ¿Estamos haciéndolo bien? No: estamos haciéndolo fatal. Si la consulta no tuviese RequestLive activa, su apertura sería realmente más rápida que la de un TTable. Pero al querer que su resultado sea actualizable, hemos instruido al BDE para que pida información a la base de datos acerca de qué columnas contiene el resultado, de qué tipos, cuál es la clave primaria de la tabla subyacente, etc, etc. La información se solicita a las tablas del catálogo de la base de datos, y es lo que habitualmente hacen los componentes TTable. Puede encontrar más información en mi artículo Religion Wars in Lilliput. Entonces, las consultas actualizables tardarán tanto en abrirse como las tablas, y tendrán encima inconvenientes añadidos:

  • La navegación irá mal. Para llegar al registro 1.000 habrá que pasar por los 999 anteriores. Las operaciones Last, Locate y todas las que utilizan filtros serán lentas.
  • Se presentarán problemas con la cantidad de registros en el lado cliente. Usted tiene una consulta con cinco registros, digamos por simplificar que en una rejilla. Añade entonces un nuevo registro. ¿Se verán los seis registros en la pantalla? No: se seguirán mostrando cinco registros. Puede que desaparezca el nuevo, o alguno de los anteriores.
  • El método Refresh no funciona con las consultas. En caso contrario, el problema anterior no sería tan grave. Si queremos "refrescar" una consulta, debemos cerrarla y volverla a abrir. Como perdemos la posición original dentro del cursor, tenemos que regresar a la fila activa mediante un bookmark o una llamada a Locate. Y ya sabemos que ambas operaciones son costosas cuando se aplican a una consulta.
  • Una transacción confirmada mientras una consulta está abierta obligará a que todos los registros de la consulta sean leídos por el cliente (FetchAll). Da lo mismo si se trata de una transacción implícita o explícita. Hay un par de trucos para evitar este problema, pero solamente funcionan para algunos controladores y en algunos casos.

ACTUALIZACIONES EN CACHE, ¡AL RESCATE!

Si las consultas dan tantos dolores de cabeza, ¿cómo es posible que algún programador siga utilizándolas? Es aquí donde comenzamos a aclarar las condiciones necesarias para trabajar con estos componentes. En primer lugar, la mayoría de los problemas enumerados dejan de serlo si la consulta devuelve pocos registros. Ahí va entonces la primera regla:

Las consultas que utilicemos para navegación y mantenimiento deben limitarse a pocos registros.

Pero seguimos sin resolver el problema de la lectura del catálogo para que el BDE genere las actualizaciones, y el mal comportamiento del cursor cuando se inserta un registro. Para evitar el largo protocolo inicial de consultas al catálogo podemos activar la opción ENABLE SCHEMA CACHE del Motor de Datos. Dicho sea de paso: así también se resuelve el problema similar de las tablas. Sin embargo, se trata de una solución con sus propios límites, pues por motivos para mí desconocidos, el BDE solamente puede manejar hasta 32 tablas en la caché de esquemas. Un diseño típico de bases de datos tiene muchas más tablas que esta cantidad. ¿Y ahora qué, listillo?

Hagamos una reflexión: ¿por qué el BDE hace preguntas a la base de datos sobre las tablas, en vez de preguntarnos a nosotros? No es una idea descabellada. Puedo imaginar un componente derivado de TDataSet, que tuviera propiedades en tiempo de diseño como PrimaryKey, para especificar la clave primaria, InsertSQL, para indicar la instrucción SQL que queremos lanzar cuando se realiza una inserción, ModifySQL, DeleteSQL, etc. De hecho, ¡existen componentes en este estilo! ¿Ha oído hablar sobre los componentes de acceso directo a InterBase FreeIBComponents? El componente que sustituye a los conjuntos de datos confía en que el programador le indique cómo quiere actualizar cada registro leído por una consulta. Así se ahorra la interrogación inicial de la base de datos.

Utilice FreeIBComponents, y otros componentes similares, solamente si su aplicación está completamente basada en consultas, en vez de tablas. Los conjuntos de datos de FIB implementan la navegación con el mismo sistema de TQuery. Así que no se le ocurra abrir de golpe una tabla de 10.000 registros con este componente.

Para poder tener este grado de control con los conjuntos de datos del BDE es necesario seguir estos dos pasos:

  • Activar las actualizaciones en caché (CachedUpdates := True).
  • Para quitarle el control al BDE sobre las instrucciones de actualización generadas, hay que asociar un componente TUpdateSQL a la propiedad UpdateObject del conjunto de datos; esto, si basta con una sola instrucción SQL para las actualizaciones. Si el algoritmo de actualización es más complejo, debemos interceptar el evento OnUpdateRecord.

En "La Cara Oculta de Delphi 4" (capítulo 32) y en la de "... C++ Builder" (cap. 31), hay unos cuantos ejemplos de esta técnica. Lo que nos interesa en este momento es que, si el conjunto de datos manipulado de este modo es un TQuery, no se produce la larga consulta inicial sobre el catálogo de la bases de datos. Desgraciadamente, esta optimización no se produce con TTable, pues el BDE sigue necesitando la información de catálogo para implementar eficientemente la navegación.

Los tipos duros no dejan a una consulta que genere sus propias instrucciones de actualización. En vez de eso, activan las actualizaciones en caché y suministran componentes TUpdateSQL, o interceptan el evento OnUpdateRecord.

CONTROL EN EL LADO SQL DE LA VIDA

Realmente, estamos usurpando en Delphi algunas atribuciones más apropiadas para que ser desempeñadas por el servidor SQL.

create view PedidoCliente as
    select P.Numero, P.Fecha, P.Enviado, C.Nombre
    from   Pedidos P inner join Clientes C
           on (P.Cliente = C.Codigo)

Esta consulta no es actualizable, al menos en InterBase. Sin embargo, la columna Nombre solamente cumple una función decorativa y podemos exigir que no se pueda modificar su valor. En tal caso, existe una correspondencia biunívoca entre las filas de PedidoCliente y de la tabla base Pedidos, considerando que todo pedido tiene un cliente asociado. Entonces, una modificación sobre un registro de PedidoCliente puede traducirse automáticamente en una modificación sobre Pedidos. ¿Cómo le indicamos a InterBase nuestras "ideas" acerca de las actualizaciones sobre PedidoCliente? Sencillamente creando triggers para la vista en cuestión:

set term !;

create trigger UpdPedidoCliente for PedidoCliente
    active before update as
begin
    update Pedidos
    set    Numero = new.Numero, Fecha = new.Fecha,
                  Enviado = new.Enviado
    where  Numero = old.Numero and Fecha = old.Fecha and
            Enviado = old.Enviado;  
end!

create trigger UpdPedidoCliente for PedidoCliente
    active before delete as
begin
    delete from Pedidos
    where  Numero = old.Numero and Fecha = old.Fecha and
            Enviado = old.Enviado;  
end!

create exception InsercionPedidoCliente
    "No se puede insertar en esta vista"!

create trigger UpdPedidoCliente for PedidoCliente
    active before insert as
begin
    exception InsercionPedidoCliente;
end!

Observe que un intento de inserción debe generar una excepción. También observe que, por simplicidad, los borrados y modificaciones utilizan todos los valores anteriores de los campos, como si se tratase de un TDataSet con el valor upWhereAll en su propiedad UpdateMode. Esta ha sido una decisión arbitraria, pues el programador tiene la libertad de programar el trigger según considere pertinente. Ya puestos en el asunto, podíamos haber programado el trigger de modificación así:

set term !;

create exception NoPermitida "Operación no permitida"!

create trigger UpdPedidoCliente for PedidoCliente
    active before update as
begin
    if (old.Numero <> new.Numero) then
        exception NoPermitida;
    update Pedidos
    set    Fecha = new.Fecha, Enviado = new.Enviado
    where  Numero = old.Numero;
    if (old.Nombre <> new.Nombre) then
        update Clientes
        set    Nombre = new.Nombre
        where  Nombre = old.Nombre;
end!

Con esto hemos permitido que el usuario pueda corregir el nombre del cliente si detecta, por ejemplo, una falta de ortografía. Sin embargo, del mismo modo se puede establecer que una modificación en el nombre del cliente constituya una asignación del pedido a otro cliente diferente:

create trigger UpdPedidoCliente for PedidoCliente
    active before update as
declare variable NuevoCliente integer;
begin
    if (old.Numero <> new.Numero) then
        exception NoPermitida;
    update Pedidos
    set    Fecha = new.Fecha, Enviado = new.Enviado
    where  Numero = old.Numero;
    if (old.Nombre <> new.Nombre) then
    begin
        select Codigo
        from   Clientes
        where  Nombre = new.Nombre
        into   :NuevoCliente;
        if (NuevoCliente is null) then
            exception ClienteNoExiste;
        update Pedidos
        set    Cliente = :NuevoCliente
        where  Numero = old.Numero;
    end
end!
Esta técnica no nos evita, de todos modos, el uso de actualizaciones en caché y objetos de actualización. El BDE trata a las vistas del mismo modo que a las consultas y tendremos, por lo tanto, los mismos problemas de navegación y apertura de siempre. Sin embargo, nos ahorraremos el tener que especificar un comportamiento tan complejo como el anterior utilizando Delphi. Las reglas de empresa siguen su trayectoria habitual: hacia el servidor, siempre que se pueda...

EN VEZ DE...

... es la traducción de instead of. Y éste es el nombre de la cláusula que necesita Oracle para poder definir un trigger sobre una vista. Por ejemplo:

create trigger UpdPedidoCliente
    instead of update on PedidoCliente
    for each row
declare
    NuevoCliente integer;
begin
    if :old.Numero <> :new.Numero then
        raise_application_error(-20001, 'Operación no permitida');
    end if;
    update Pedidos
    set    Fecha = :new.Fecha, Enviado = :new.Enviado
    where  Numero = :old.Numero;
    if :old.Nombre <> :new.Nombre then
        select Codigo
        into   NuevoCliente
        from   Clientes
        where  Nombre = :new.Nombre;
        if NuevoCliente is null then
            raise_application_error(-20001, 'El cliente no existe');
        end if;
        update Pedidos
        set    Cliente = NuevoCliente
        where  Numero = :old.Numero;
    end if;
end;