La tarea más importante que tenemos cuando diseñamos una Base de Datos es evitar que entre basura en ella. Todo lo demás que hagamos será inservible si permitimos la entrada de basura. Se llama “basura” a cualquier dato que está pero que no debería estar.

Ejemplos de basura

  • La empresa inició sus actividades el 11 de octubre de 2009 y hay una venta con fecha 22 de marzo de 2007
  • La venta se hizo el día 14 de mayo de 2010 y la cobranza el día 20 de febrero de 2010
  • El precio de costo de un producto es mayor que su precio de venta
  • Se tiene registrada una venta, pero no la fecha de esa venta
  • Se tiene registrada la venta de un producto, pero el identificador de ese producto no existe en la tabla de PRODUCTOS
  • Se tiene registrada una venta, pero no se registró el identificador del cliente a quien se le vendió

Estos son solamente algunos ejemplos, hay miles más que podrían agregarse. El común denominador de todos ellos es que hay datos faltantes o datos inconsistentes. Un dato es inconsistente cuando es ilógico, cuando algo así no podría ocurrir. Por ejemplo, no se puede vender antes de constituir la empresa, ni se puede cobrar antes de vender, eso no tiene sentido.

Programando defensivamente

La programación defensiva tiene como ventaja que evita la introducción de basura (o al menos disminuye muchísimo la probabilidad de que ocurra) y la desventaja que nos hace trabajar más.

Pero como nuestra principal tarea es evitar la introducción de basura ese trabajo de más es el precio que deberemos pagar.

En la programación defensiva lo que hacemos es poner trabas en cuantos lugares sean posibles a los datos faltantes o inconsistentes. En la programación normal pondríamos una sola traba (aunque un mal diseñador ni siquiera haría eso), en cambio en la programación defensiva pondremos muchas trabas.

Ejemplo 1

Queremos evitar que el precio de costo sea mayor que el precio de venta.

Creamos un dominio llamado D_PRECIO:

CREATE DOMAIN D_PRECIO AS
   NUMERIC(18, 4)
   CHECK (VALUE >= 0);

Este dominio aún no nos asegura que el precio de venta sea mayor que el precio de costo, pero sí que no se introduzcan precios negativos. Algo es algo.

Ahora le agregamos una restricción CHECK a la tabla de PRODUCTOS:

ALTER TABLE PRODUCTOS
   ADD CONSTRAINT CHK_PRODUCTOS
   CHECK (PRD_PREVTA > PRD_PRECTO);

Esta restricción CHECK nos asegurará que siempre el precio de venta sea mayor que el precio de costo. ¿Ya está bien, verdad? ¿Es suficiente con esta restricción? Pues sí, pero queremos programar defensivamente para duplicar las comprobaciones y evitar la introducción de basura, entonces también escribimos un trigger before insert (o sea un trigger que se ejecutará antes de que la fila sea grabada en la tabla):

CREATE TRIGGER BIU_PRODUCTOS FOR PRODUCTOS
   ACTIVE BEFORE INSERT OR UPDATE
   POSITION 1
AS
BEGIN

   IF (NEW.PRD_PREVTA <= NEW.PRD_PRECTO) THEN
      EXCEPTION E_PRECIO_VENTA_MENOR_QUE_COSTO ;

END;

Y a la excepción E_PRECIO_VENTA_MENOR_QUE_COSTO podríamos definirla como:

CREATE EXCEPTION E_PRECIO_VENTA_MENOR_QUE_COSTO
   'El precio de venta no es mayor que el precio de costo';

Ejemplo 2

Queremos asegurarnos que el identificador del producto que vendemos exista en la tabla de PRODUCTOS

Primero, agregamos una FOREIGN KEY a nuestra tabla de movimientos de productos (o sea, la tabla en la cual registramos las ventas)

ALTER TABLE MOVIMDET
   ADD CONSTRAINT FK_MOVIMDET1
   FOREIGN KEY (MOV_CODSUC, MOV_IDEPRD)
   REFERENCES PRODUCTOS(PRD_CODSUC, PRD_IDENTI);

Ya está, ¿verdad? con esto nos aseguramos que no se pueda vender un producto cuyo identificador no se encuentre en la tabla de PRODUCTOS. Muy bien, funcionará, pero somos exquisitos y queremos aumentar la seguridad, entonces también escribimos una restricción CHECK en la tabla MOVIMDET

ALTER TABLE MOVIMDET
   ADD CONSTRAINT CHK_MOVIMDET CHECK (
      MOV_CODSUC IN (SELECT
                        SUC_CODIGO
                     FROM SUCURSALES) AND
      MOV_IDEPRD IN (SELECT
                        PRD_IDENTI
                     FROM
                        PRODUCTOS
                     WHERE
                        PRD_CODSUC = MOVIMDET.MOV_CODSUC)
);

Esta restricción CHECK nos asegurará que el código de la sucursal se encuentre en la tabla de SUCURSALES y que el identificador del producto se encuentre en la tabla de PRODUCTOS. ¿Ya está bien, ya es suficiente? Pues no, estamos programando defensivamente así que también escribiremos un trigger que se ejecutará antes de insertar o actualizar filas:

CREATE TRIGGER BIU_MOVIMDET FOR MOVIMDET
   ACTIVE BEFORE INSERT OR UPDATE
   POSITION 1
AS
   DECLARE VARIABLE lnCodigoSucursal TYPE OF D_CODIGOSUCURSAL;
   DECLARE VARIABLE lnIdentificadorProducto TYPE OF D_IDENTIFICADOR;
BEGIN

   lnCodigoSucursal = (SELECT
                          SUC_CODIGO
                       FROM
                          SUCURSALES
                       WHERE
                          SUC_CODIGO = NEW.MOV_CODSUC);

   IF (lnCodigoSucursal IS NULL) THEN
      EXCEPTION E_SIN_SUCURSAL;

   lnIdentificadorProducto = (SELECT
                                 PRD_IDENTI
                              FROM
                                 PRODUCTOS
                              WHERE
                                 PRD_CODSUC = NEW.MOV_CODSUC AND
                                 PRD_IDENTI = NEW.MOV_IDEPRD);

   IF (lnIdentificadorProducto IS NULL) THEN
      EXCEPTION E_SIN_PRODUCTO;

END;

¿Qué hicimos en este trigger? Primero, averiguamos si el código de la Sucursal que se quiere grabar en la columna MOV_CODSUC existe en la tabla de SUCURSALES. Si el resultado de la búsqueda es NULL eso significa que no existe y por lo tanto enviamos una excepción. Al enviar una excepción el código finaliza con error, todo el código fuente que se encuentre después de la excepción jamás se ejecutará porque el control regresó al nivel superior. Si la sucursal existe entonces continuamos y ahora averiguamos si existe un producto con ese identificador en esa sucursal. Si el resultado de la búsqueda es NULL, eso significa que no existe, y por lo tanto enviamos una excepción.

Recuerda que si un trigger envía una excepción las columnas no se graban en la tabla. Al enviar una excepción le estamos diciendo al Firebird: “aquí encontré un error, no grabes estas columnas en la tabla porque hay algo que está mal”.

Conclusión:

La programación defensiva es una herramienta que tenemos para conseguir que nuestro código fuente sea más confiable. Siempre debemos pensar que los usuarios son muy tontos o muy malos, tipos que pueden hacer un desastre por ignorancia o por maldad, y debemos tomar todas las precauciones habidas y por haber para evitar que causen problemas.

Entonces, en nuestro lenguaje de programación ya les debemos poner trabas a sus acciones, pero también debemos hacerlo en la Base de Datos. Firebird nos provee de cinco herramientas muy buenas para evitar que introduzcan basura en nuestras tablas: los dominios, las restricciones foreign key, las restricciones check, las restricciones unique keys y los triggers.

Debemos usarlas y quitarles el máximo provecho, que para eso están.

La programación defensiva nos hará trabajar más, pero también nos dará mayor tranquilidad, porque les tendremos mucha confianza a los datos que están guardados en las tablas.

Y si nuestras aplicaciones son muy robustas tendremos clientes muy contentos. Y clientes muy contentos siempre significa ganar más dinero.

Artículos relacionados:

Entendiendo a los dominios

Entendiendo a las Foreign Keys

Usando la restricción CHECK

Entendiendo las excepciones

Entendiendo la integridad referencial

Entendiendo a los triggers

El índice del blog Firebird21