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

Anuncios

Entendiendo sweep y garbage collection

9 comentarios

Como habíamos visto en este artículo:

https://firebird21.wordpress.com/2013/06/14/la-arquitectura-mga/

cuando se actualiza (UPDATE) o se borra (DELETE) un registro el Firebird guarda en la Base de Datos la versión anterior de ese registro. ¿Para qué hace eso? Para poder revertir al registro original cuando se hace un ROLLBACK.

Un ROLLBACK se hace si ocurrió un error o si el usuario cambió de idea y no quiere guardar lo último que hizo, o si se cerró anormalmente la conexión.

Por lo tanto, cada comando UPDATE y cada comando DELETE le agrega un registro a la tabla respectiva (en el caso del DELETE el nuevo registro solamente tiene un marca que le indica al Firebird que ese registro está “borrado”).

Veamos un ejemplo de UPDATE:

El precio de “Televisor Toshiba de 20 pulgadas” es de 120 dólares. Como se está vendiendo poco ya que la mayoría de la gente ahora prefiere televisores de más pulgadas se decidió disminuir su precio a 100 dólares, tratando de conseguir que aumenten las ventas al disminuir el precio.

Entonces, en nuestra tabla de PRODUCTOS, luego del correspondiente UPDATE, tendríamos:

SWEEP1

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

Ahora hay dos posibilidades:

  1. Que la transacción termine con un COMMIT exitoso
  2. Que la transacción termine con un ROLLBACK

Si la transacción terminó con un COMMIT exitoso entonces el registro antiguo, el que tiene el TID 143 ya no sirve, es inútil. Pero el Firebird no lo elimina de la Base de Datos, lo deja ahí. Y eso implica que está ocupando espacio inútilmente.

Si la transacción terminó con un ROLLBACK entonces el registro nuevo, el que tiene el TID 279 ya no sirve, es inútil. Pero el Firebird no lo elimina de la Base de Datos, lo deja ahí. Y eso implica que está ocupando espacio inútilmente.

De lo anterior se deduce que cada vez que escribes el comando UPDATE dejas un registro inservible dentro de la Base de Datos. O el original queda inservible o el actualizado queda inservible.

Hay por lo tanto dos tipos de registros inservibles:

  • Los dejados por los COMMIT
  • Los dejados por los ROLLBACK

¿Hay diferencia si el registro inservible fue dejado por un COMMIT o por un ROLLBACK?

Sí, la diferencia es que los registros inservibles dejados por los COMMIT pueden ser eliminados automáticamente durante la recolección de la basura (ver más abajo), en cambio los dejados inservibles por los ROLLBACK nunca son eliminados automáticamente.

¿Qué es la “basura”?

Esos registros inservibles, inútiles, que fueron dejados por los UPDATE o por los DELETE siguen ocupando espacio dentro de la Base de Datos, pero como ya son inservibles y totalmente inútiles se les llama “basura”.

¿Cuál es el problema con la basura?

Que ocupa espacio inútilmente dentro de la Base de Datos y por lo tanto ésta es más grande de lo que debería ser y además se vuelve cada vez más lenta.

¿Qué significa “garbage collection”?

En castellano: recolección de la basura. Cuando se recolecta la basura todos esos registros inservibles dejados por los COMMIT son eliminados permanentemente y definitivamente de la Base de Datos. Como consecuencia de ello, la Base de Datos queda con espacio reutilizable y además las operaciones dentro de ella (INSERT, UPDATE, DELETE, FETCH, SELECT) se realizan más rápidamente.

¿Cuándo se realiza la “garbage collection”?

Cada vez que un registro es “tocado” por un SELECT toda la basura relacionada con ese registro como consecuencia de los COMMIT es eliminada. En otras palabras, si al escribir un SELECT un registro está en el resultado obtenido entonces toda la basura relacionada con ese registro, producida por los  COMMIT, es eliminada.

Fíjate que solamente el SELECT elimina a la basura, los demás comandos: INSERT, UPDATE, DELETE, no la eliminan, la dejan así mismo como estaba.

¿Cuál es la forma más rápida de eliminar toda la basura de una tabla?

Escribir el comando:

SELECT
   COUNT(*)
FROM
   MiTabla

Lo que hace ese comando es contar la cantidad de registros que tiene la tabla pero para hacerlo debe recorrer la tabla desde el primer registro hasta el último registro porque el Firebird no guarda en alguna parte la cantidad de registros de la tabla. Por lo tanto, todos los registros de esa tabla están involucrados en el SELECT y por consiguiente en todos esos registros es recolectada la basura. Si la tabla tiene 12.000 registros entonces SELECT COUNT(*) recorre los 12.000 registros y el “garbage collection” elimina toda la basura que haya encontrado en cualquiera de esos 12.000 registros.

Recuerda que esta forma de eliminar a la basura solamente elimina a la basura que dejaron los COMMIT, la basura dejada por los ROLLBACK no se puede eliminar así.

¿Cuál es el problema con recolectar la basura usando SELECT?

Que para recolectar la basura de todas las tablas debes hacer SELECT COUNT(*) en todas las tablas o esperar que algún SELECT involucre a los registros que tienen basura. A veces, puede ocurrir que se hizo el UPDATE de un registro pero nunca se hizo un SELECT que involucre a ese registro y por lo tanto la basura que dejó el UPDATE permanece dentro de la Base de Datos.

Además, no te olvides que cuando se ejecuta un SELECT solamente se recolecta la basura que dejaron los COMMIT, la basura dejada por los ROLLBACK continúa allí.

¿Qué es el sweep?

El sweep (en castellano: barrido, de barrer con una escoba) es un proceso que recorre toda la Base de Datos y elimina toda la basura que se encuentra en ella, tanto la proveniente de los COMMIT como la proveniente de los ROLLBACK. Cuando el sweep finaliza la Base de Datos está bien limpia, sin basura.

¿Cuándo se realiza el sweep?

El sweep se puede realizar automáticamente o manualmente. Automáticamente es cuando lo inicia el propio Firebird y manualmente es cuando lo inicia un programa (por ejemplo: GFIX y GBAK pueden iniciar el sweep).

Sweep automático

Cada Base de Datos tiene una entrada denominada “sweep interval” o sea intervalo del sweep. Es un número de tipo INTEGER  y por lo tanto su valor puede estar entre 0 y 2.147.483.647. Por defecto su valor es 20.000.

Cuando la diferencia entre la OST y la OAT es mayor que el intervalo del sweep, se inicia el sweep. Por ejemplo:

OST = 45.201

OAT = 25.200

Sweep interval = 20.000

Diferencia = OST – OAT = 45.201 – 25.200 = 20.001

Como la diferencia es mayor que el intervalo del sweep, se inicia el sweep automático.

Si no se quiere realizar un sweep automático entonces el intervalo del sweep debe ser 0 (cero)

¿Cuál es el problema con el sweep?

Que este proceso de recorrer toda la Base de Datos y de eliminar toda la basura que hay en ella es lento, en algunos casos extremadamente lento y los usuarios se quejan de que todas las operaciones (INSERT, UPDATE, DELETE, FETCH, SELECT) se realizan muy despaciosamente. Y por supuesto podría ocurrir que el sweep automático se inicie cuando los usuarios están más apurados, más impacientes y por culpa del sweep todas sus transacciones son más lentas que una tortuga con tres patas. Es por lo tanto normal que el DBA (Administrador de la Base de Datos) ponga el intervalo del sweep en 0 (cero) y de esa manera realizará el sweep manualmente, en momentos en que nadie está usando la Base de Datos (o cuando muy pocos usuarios la están usando).

Pero …. a veces el DBA se olvida de que dejó el intervalo del sweep en cero y la Base de Datos se va volviendo cada vez más grande y más lenta por toda la basura que tiene acumulada.

En otras palabras, se necesita un DBA con cerebro, algo que no siempre se consigue.

Aprovechar el backup para hacer el sweep

Cuando haces un backup usando el programa GBAK tienes la opción de que también se haga el sweep de la Base de Datos. Esa es la opción por defecto y la recomendable. Así, cuando finaliza el programa GBAK puedes estar seguro de que el archivo de backup generado no tiene basura. Recuerda: la Base de Datos original continúa con toda la basura que tenía, el backup generado es el que no tiene basura.

Usando el programa GFIX para hacer el sweep

Una de las opciones de este programa nos permite pedirle que haga el sweep:

GFIX -sweep -user SYSDBA -password masterkey MiBaseDatos

Un pequeño “defecto” que tiene el programa GFIX es que no te avisa que finalizó exitosamente y eso confunde a los principiantes. O sea, si termina sin ningún mensaje significa que todo estuvo ok, si ocurrió algún problema entonces termina con un mensaje de error.

Conclusión:

El uso normal de la Base de Datos va dejando basura dentro de ella. Esa basura debe ser eliminada en algún momento porque de no eliminarse solamente causará que la Base de Datos tenga un tamaño mayor que el necesario y además que todas las transacciones sean más lentas de lo que deberían. La eliminación de la basura puede hacerse en forma automática (cuando el intervalo del sweep es mayor que cero y la diferencia entre la OST y la OAT es mayor que ese intervalo) o en forma manual (cuando el intervalo del sweep es cero y en ese caso hay que ejecutar el programa GFIX). También se elimina la basura cuando se hace un backup usando el programa GBAK y no se especifica la opción -garbage collection.

Artículos relacionados:

La arquitectura MGA

Entendiendo a las transacciones

Entendiendo a los identificadores de las transacciones

El índice del blog Firebird21

Descubriendo valores incorrectos en nuestras tablas

Deja un comentario

Siempre debemos verificar que en nuestras tablas tengamos valores correctos y solamente valores correctos porque cualquier valor incorrecto es basura y la basura jamás puede ser buena.

Si una columna tiene una restricción Foreign Key entonces si la tabla padre tiene valores correctos podemos estar seguros que la tabla hija también los tendrá. Pero … ¿y si la columna no tiene una restricción Foreign Key?

Supongamos que tenemos una columna para conocer el sexo de una persona, ‘F’=femenino, ‘M’=masculino. ¿Cómo podríamos verificar que tengamos solamente esos valores y que no tengamos ‘m’, ‘f’, ‘1’, ‘0’, o cualquier otra cosa?

SELECT
   *
FROM
   MiTabla
WHERE
   MiColumna NOT IN ('F', 'M')

En nuestra tabla cabecera de movimientos tenemos una columna llamada MVC_TIPMOV que nos indica cual es el tipo de movimiento. ‘ECM’ = entrada por compras, ‘SVT’ = salida por ventas, ‘EDV’ = entrada por devolución, ‘SDV’ = salida por devolución, ‘COB’ = cobranza, ‘PAG’ = pago. ¿Cómo podemos estar seguros que la columna MVC_TIPMOV tenga solamente uno de esos valores?

SELECT
   *
FROM
   MOVIMCAB
WHERE
   MVC_TIPMOV NOT IN ('ECM', 'SVT', 'EDV', 'SDV', 'COB', 'PAG')

¿Y para saber si algún movimiento ocurrió en un año distinto a 2011, 2012, 2013, que son los únicos años permitidos?

SELECT
   *
FROM
   MOVIMCAB
WHERE
   EXTRACT(YEAR FROM MVC_FECHAX) NOT IN (2011, 2012, 2013)

Conclusión:

Siempre debemos verificar que en nuestras tablas tengamos valores correctos y solamente valores correctos porque los valores incorrectos jamás servirán para algo bueno. Podemos hacer esa verificación con NOT IN.

Inclusive sería muy bueno que en nuestros programas agreguemos una opción que le permita a los usuarios realizar esta verificación. Si la verificación finaliza sin errores entonces podemos asegurarle que no tiene basura en sus tablas. Y si al verificar se encuentran errores entonces por supuesto que se debe corregirlos inmediatamente.

Artículo relacionado:

El índice del blog Firebird21