Programación defensiva

Deja un comentario

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

La forma más fácil de cambiar un dominio

1 comentario

Lo correcto es que cuando definimos la estructura de una tabla todas sus columnas referencien a un dominio. ¿Pero qué pasa si luego queremos cambiar el dominio de una columna?

Supongamos que en muchas tablas tenemos columnas que referencian al dominio D_NOMBRE25 (que es un VARCHAR(25)) y luego queremos usar D_NOMBRE30 (que es un VARCHAR(30)). En ese caso, ningún problema, porque pasamos de un número menor (25 en este ejemplo) a un número mayor (30 en este ejemplo).

O tenemos columnas que usan un dominio D_INTEGER que es un INTEGER y queremos pasarlas a D_BIGINT que es un BIGINT. Ningún problema tampoco, ya que pasamos de INTEGER a BIGINT, de menor a mayor, está todo ok.

Sin embargo encontraremos un problema cuando queremos hacer al revés: pasar de un número mayor a un número menor. Si queremos pasar de VARCHAR(30) a VARCHAR(25) el Firebird no lo permitirá. Tampoco permitirá pasar de BIGINT a INTEGER.

¿Por qué no permite?

Porque se puede perder precisión.

Pero yo estoy seguro que los datos cabrán bien.

No importa eso. Supongamos que quieres pasar de VARCHAR(30) a VARCHAR(25) y tú estás completamente seguro que todos los datos que tienes caben en un VARCHAR(25), que ninguno necesita más caracteres. Puedes tener razón, pero el Firebird no verifica eso. Él simplemente no permite pasar de una precisión mayor a una precisión menor. Aunque el mayor número que se guarde en una columna sea el 12, tampoco te permitirá cambiar de INTEGER a SMALLINT, por ejemplo.

¿Y cómo lo soluciono si necesito hacer esos cambios?

Tienes tres formas, dos largas y una corta.

Una forma larga es revisar todas las dependencias de cada columna, eliminar o poner comentarios en las columnas relacionadas, borrar la columna problemática y luego volver a insertarla, ya con el nuevo tipo de datos. Una vez que está insertada, recrear o quitar los comentarios de todas las dependencias.

Otra forma larga es agregar una columna adicional, con el nuevo tipo de datos, copiar todos los datos de la columna original a la nueva columna, cambiar todas las referencias a la columna original por referencias a la nueva columna y finalmente borrar la columna original.

En ambos casos, si no se trata solamente de una o dos columnas sino de decenas o de centenas de columnas, el trabajo será laborioso y demandará bastante tiempo.

Hay una forma más práctica: crear una nueva Base de Datos con los cambios deseados.

Ejemplo: Cambiar el dominio D_IDENTIFICADOR que es un BIGINT a INTEGER

DOMINIOS1

Captura 1. Si haces clic en el imagen la verás más grande

DOMINIOS2

Captura 2. Si haces clic en el imagen la verás más grande

DOMINIOS3

Captura 3. Si haces clic en el imagen la verás más grande

despues “Next”, “Next”, “Next”, “Finish” y “Close”.

Al finalizar el proceso, tendrás un script con el contenido de tu Base de Datos. Ahora ya es simplemente cuestión de buscar a D_IDENTIFICADOR y  cambiar su tipo de BIGINT a INTEGER.

DOMINIOS4

Captura 4. Si haces clic en el imagen la verás más grande

Una vez hecho eso, se ejecuta el script y listo, asunto solucionado.

DOMINIOS5

Captura 5. Si haces clic en el imagen la verás más grande

entonces, sin importar si el dominio D_IDENTIFICADOR había sido usado 40 veces, 70 veces, o lo que fuera, al crear una nueva Base de Datos su tipo de datos es el tipo de datos que queríamos que tuviera. Realizar todos estos pasos no tarda más de uno o dos minutos, una ganancia considerable de tiempo comparado con los otros dos métodos.

Advertencia:

Para asegurarte que todos los metadatos se hayan copiado, siempre es importante que hagas esa verificación.

DOMINIOS6

Captura 6. Si haces clic en el imagen la verás más grande

O sea, si la Base de Datos original tiene 44 dominios, la Base de Datos nueva también debe tener 44 dominios. Si la original tiene 80 tablas la nueva también debe tener 80 tablas, etc.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21