Bases de datos shareware (2)

Deja un comentario

En este artículo ya habíamos visto las ventajas de tener una Base de Datos shareware, las dos formas usuales de hacerla, y los trucos que los usuarios tramposos podrían intentar:

https://firebird21.wordpress.com/2014/01/23/bases-de-datos-shareware/

Ahora explicaremos mejor el funcionamiento de esas dos formas.

Protección por fecha

Se le permite al usuario usar la aplicación hasta que llegue una cierta fecha, después ya no podrá usarla. Por ejemplo si instaló la aplicación el 24 de enero de 2014 podrá usarla hasta el 24 de febrero de 2014. Si quiere usarla después, entonces tendrá que pagar.

Aquí lo que debemos impedir es que si le cambia la fecha a la computadora la aplicación siga funcionando. Por ejemplo, el día 25 de febrero el usuario no puede usar la aplicación, le cambia la fecha a su computadora y le coloca 1 de febrero de 2014. A pesar de haber cambiado la fecha no debería poder usar la aplicación. Debemos detectar ese cambio y actuar en consecuencia.

¿Cómo protegemos por fecha?

Guardamos en una tabla de nuestra Base de Datos o en un archivo externo a ella, tres valores: la fecha inicial, la fecha final, y la fecha última.

La fecha inicial es el primer día que puede usar la aplicación. Por ejemplo el 24 de enero de 2014

La fecha final es el último día que puede usar la aplicación. Por ejemplo el 24 de febrero de 2014

La fecha última, cuando se instala la aplicación es igual a la fecha inicial. Después, cada vez que se ejecuta la aplicación se verifica si la fecha del Servidor es igual o posterior a la fecha última. Si es así, se actualiza la fecha última.

Entonces, si el 25 de febrero el usuario ejecuta la aplicación el valor de fecha última será 25 de febrero. Si le cambia la fecha a su computadora y le pone 1 de febrero, fecha última seguirá siendo 25 de febrero, porque el valor de fecha última jamás puede retroceder. El nuevo valor de fecha última solamente puede ser igual o posterior al valor anterior de fecha última.

En nuestra rutina de validación verificamos:

  1. Que la fecha de la computadora sea mayor o igual que fecha inicial y que también sea menor o igual a fecha final
  2. Que la fecha última sea mayor o igual que fecha inicial y que también sea menor o igual a fecha final

Podemos mejorar el algoritmo si en vez de guardar solamente la fecha guardamos la fecha y la hora. Inclusive lo mejoraríamos aún más si nos aseguramos que cada vez que se ejecuta la aplicación el tiempo aumente por lo menos en 5 minutos.

Primera ejecución del sistema: 24 de enero de 2014, 16:12

Segunda ejecución del sistema, como mínimo debería ser el 24 de enero de 2014 a las 16:17, ó sea 5 minutos después que la ejecución anterior. Fíjate que no importa cuando el usuario realmente ejecutó la aplicación, lo que nos aseguramos es que siempre haya un incremento de por lo menos 5 minutos. Si la segunda ejecución fue el 24 de enero a las 16:50, en fecha última tendremos guardado 24 de enero y 16:50, pero si la segunda ejecución fue el 24 de enero a las 16:13 en fecha última tendremos guardado 24 de enero y 16:17 porque siempre incrementamos como mínimo 5 minutos.

Por supuesto que no es obligatorio sumarle 5 minutos, podrías sumarle 10 minutos, 30 minutos, 60 minutos, los que te parezcan. La idea es que siempre que se ejecuta la aplicación la fecha y hora se incrementen, que jamás puedan mantenerse igual o retroceder.

De esta manera, tarde o temprano, haga el usuario lo que haga, se sobrepasará la fecha última y la aplicación dejará de funcionar.

Protección por cantidad de ejecuciones

Esto es mucho más fácil de programar que la protección por fechas. Simplemente guardamos en una tabla de nuestra Base de Datos o en un archivo externo la cantidad de veces que el usuario ejecutó nuestra aplicación. Cada vez que la ejecuta incrementamos esa cantidad en 1.

Cuando instala la aplicación, el contador es 0. La primera vez que la ejecuta, el contador es 1. La segunda vez el contador es 2. Y así sucesivamente. Cuando se llegue a la cantidad predeterminada (por ejemplo: 25), la aplicación dejará de funcionar.

Usando ambas protecciones

Para que nuestra aplicación esté más protegida lo mejor es protegerla con ambos métodos: fecha y cantidad.

Siguiendo con nuestro ejemplo, la aplicación dejará de funcionar el 25 de febrero de 2014 ó cuando haya sido ejecutada 25 veces, lo que ocurra primero.

Encriptando los valores

Evidentemente ninguna protección será efectiva si el usuario puede ver y cambiar las fechas o las cantidades de ejecución. Esos valores deben encontrarse encriptados (es decir, ilegibles para quienes no conozcan la clave) y cuanto más ocultos, mejor.

Un lugar donde tradicionalmente se guardan esos valores es en el Registro del Windows. Muy pocos usuarios conocen el Registro del Windows, menos aún han entrado a curiosearlo y muchos menos aún se atreven a cambiar algo por su propia cuenta.

Otro lugar es en la propia Base de Datos. Si tienes una tabla de un solo registro donde guardas la configuración o algo similar entonces puedes aprovechar y agregarle una columna a esa tabla. En esa nueva columna guardas (convenientemente encriptadas, por supuesto) la fecha inicial, la fecha final, la fecha última y la cantidad de ejecuciones.

Recuperando los valores

Así como has guardado las fechas y la cantidad de ejecuciones encriptadas, debes poder desencriptarlas para realizar las verificaciones y determinar si la aplicación puede seguir usándose o no.

Esto puedes realizarlo en dos lugares:

  1. En tu aplicación. Es decir en el programa que escribiste con Visual FoxPro, Visual Basic, C, C++, Delphi, Java, etc.
  2. En un trigger de tu Base de Datos. El usuario debe tener solamente la versión compilada de ese trigger, no debe tener el código fuente o de nada te servirán las protecciones.

Lo ideal y lo correcto por lo tanto es que verifiques que tu aplicación puede ejecutarse, en ambos lugares: en tu aplicación y en un trigger. De esa manera aunque el usuario llegara a descubrir y evitar la protección que se encuentra en un lugar aún le faltaría descubrir y evitar la protección que se encuentra en el otro. Y si debe trabajar más para conseguirlo es más probable que desista de su intención. Y entonces la protección habrá cumplido su misión.

En el siguiente artículo ya veremos como implementar lo expuesto hasta acá.

Artículos relacionados:

Bases de datos shareware

El índice del blog Firebird21

Un stored procedure para guardar la cantidad de registros de cada tabla

Deja un comentario

Como seguramente sabes, usar SELECT COUNT(*) FROM MiTabla para conocer la cantidad de filas que tiene una tabla no es recomendable en tablas cuyas filas se cuentan por millones. El motivo es que el Firebird hace un recorrido secuencial a todas las filas de la tabla para hallar la cantidad de esas filas y por lo tanto en tablas grandes demora mucho tiempo en responder.

Una alternativa mucho mejor para conocer la cantidad de filas de cada tabla habíamos visto en este artículo:

https://firebird21.wordpress.com/2013/03/08/como-saber-rapidamente-la-cantidad-de-filas-que-tiene-una-tabla/

que funciona perfecto pues cada vez que se inserta o se borra una fila de una tabla la nueva cantidad de filas es guardada en la tabla RECCOUNT.

Esta es la estructura de la tabla RECCOUNT:

RECCOUNT1

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

Este es el trigger que actualiza a la tabla RECCOUNT cuando una nueva fila es insertada en la tabla BANCOS

CREATE TRIGGER AI_BANCOS FOR BANCOS
   ACTIVE AFTER INSERT
   POSITION 0
AS
   DECLARE VARIABLE lnCantidadFilas INTEGER;
BEGIN

   lnCantidadFilas = COALESCE((SELECT REC_CANTID FROM RECCOUNT WHERE REC_NOMBRE = 'BANCOS'), 0);

   UPDATE OR INSERT INTO RECCOUNT
            (REC_CODSUC, REC_NOMBRE, REC_CANTID)
     VALUES (NEW.BAN_CODSUC, 'BANCOS', :lnCantidadFilas + 1)
   MATCHING (REC_NOMBRE) ;

END;

Y este es el trigger que actualiza a la tabla RECCOUNT cuando una fila es borrada de la tabla BANCOS

CREATE TRIGGER AD_BANCOS FOR BANCOS
   ACTIVE AFTER DELETE
   POSITION 1
AS
   DECLARE VARIABLE lnCantidadFilas INTEGER;
BEGIN

   lnCantidadFilas = (SELECT REC_CANTID FROM RECCOUNT WHERE REC_NOMBRE = 'BANCOS');

   IF (lnCantidadFilas IS NOT NULL AND lnCantidadFilas > 0) THEN
      UPDATE
         RECCOUNT
      SET
         REC_CANTID = :lnCantidadFilas - 1
      WHERE
         REC_CODSUC = OLD.BAN_CODSUC AND
         REC_NOMBRE = 'BANCOS';

END;

Pero ¿y si la tabla involucrada ya tenía filas? en ese caso la cantidad de filas guardada en la tabla RECCOUNT no será la correcta porque los triggers empezarán a sumar y a restar desde el momento en que fueron agregados, ellos no saben cuantas filas tenía la tabla previamente. O sea, en tablas nuevas la cantidad de filas guardadas en la columna REC_CANTID de la tabla RECCOUNT será la correcta, pero en tablas que ya tenían filas, no.

Por eso, para actualizar la cantidad de filas de cada tabla escribí el siguiente stored procedure y así cuando los triggers actualicen la columna REC_CANTID estará guardada en esa columna la cantidad correcta de filas de cada tabla.

CREATE PROCEDURE SP_ACTUALIZAR_CANTIDAD_FILAS(
   tnCodSuc TYPE OF D_CODIGOSUCURSAL)
AS
   DECLARE VARIABLE lcNombreTabla   VARCHAR(32);
   DECLARE VARIABLE lnCantidadFilas INTEGER;
   DECLARE VARIABLE lcComando       VARCHAR(128);
BEGIN

   FOR
      SELECT
         RDB$RELATION_NAME
      FROM
         RDB$RELATIONS
      WHERE
         RDB$SYSTEM_FLAG = 0 AND
         RDB$RELATION_TYPE = 0
      ORDER BY
         RDB$RELATION_NAME
      INTO
         :lcNombreTabla
   DO BEGIN
      lcComando = 'SELECT COUNT(*) FROM ' || lcNombreTabla ;
      EXECUTE STATEMENT (lcComando) INTO :lnCantidadFilas ;
      UPDATE OR INSERT INTO RECCOUNT
               (REC_CODSUC, REC_NOMBRE , REC_CANTID)
        VALUES (:tnCodSuc , :lcNombreTabla, :lnCantidadFilas)
      MATCHING (REC_NOMBRE);
   END

END;

¿Qué hace este stored procedure?

  1. Halla el nombre de cada tabla del usuario. O sea que están excluidas las tablas del sistema y las vistas
  2. Usando la función COUNT(*) halla la cantidad de filas de cada tabla
  3. Si el nombre de la tabla ya existía en la tabla RECCOUNT entonces actualiza la cantidad de filas. Si no existía la tabla, la agrega

Como este stored procedure utiliza la función COUNT() para conocer la cantidad de filas de cada tabla, su ejecución puede demorar varios minutos así que … paciencia. Lo bueno es que deberás ejecutarlo una sola vez.

Artículos relacionados:

Nombres de todas las tablas

Como saber rápidamente la cantidad de filas que tiene una tabla

El índice del blog Firebird21

 

Ejemplo Nº 051 – ¿Cómo saber si una tabla tiene registros?

4 comentarios

A veces queremos saber si una tabla tiene registros (filas). No nos interesa saber cuantas filas tiene sino si tiene alguna. Hay varias formas de obtener esa información:

Ejemplo 1:

SELECT
   COUNT(*)
FROM
   MiTabla

Esto funciona pero si la tabla tiene muchos millones de registros será muy lento porque el Firebird contará cuantas filas hay en la tabla y nos devolverá el resultado y esto tomará un buen tiempo. Pero no queremos saber cuantas filas hay en la tabla, solamente queremos saber si tiene filas o no y con este método estamos perdiendo mucho tiempo innecesariamente.

Ejemplo 2:

SELECT
   MIN(MiColumna)
FROM
   MiTabla

Si la tabla tiene un índice ascendente sobre la columna “MiColumna” entonces hallar el mínimo de esa columna será muy rápido. Este método es mucho más eficiente que el del Ejemplo 1, el resultado se obtiene mucho más rápido.

Ejemplo 3:

SELECT
   MiColumna
FROM
   MiTabla
WHERE
   MiColumna > 0
ROWS
   1 TO 1

Y este método es el más eficiente de los tres ya que el Firebird ni siquiera necesita llamar a una función sino simplemente verifica si la primera fila tiene un valor mayor que cero y si ese es el caso ya devuelve el resultado porque ROWS 1 TO 1 le dice que devuelva una fila. Desde luego que “MiColumna” debería ser de tipo numérico y todos sus valores mayores que cero. Si ese no es el caso entonces habría que poner una condición que se cumpla siempre, en el 100% de los casos, para que este método sea el más rápido. En general lo conveniente es que “MiColumna” sea la Primary Key, que es una columna autoincremental y por lo tanto siempre tiene valores mayores que cero.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

Un poco más sobre la función AVG()

1 comentario

La función AVG() nos devuelve el promedio de una columna numérica. O sea que halla la suma de esa columna y luego la divide por la cantidad de filas que no sean NULL, es decir que si en una fila hay NULL no la utiliza en el cálculo.

Para entender mejor a la función AVG() consideremos una tabla que tiene 7 filas, las cuales en una columna numérica tienen estos valores:

20
40
10
20
NULL
NULL
NULL

Ejemplo 1:

El promedio según la función AVG() es:

SELECT
   AVG(MiColumna)
FROM
   MiTabla

90 / 4 = 22,5

Se dividió por 4 y no por 7, porque las filas con NULL para la función AVG() son inexistentes.

Ejemplo 2:

En la misma tabla anterior queremos hallar el promedio de esa columna pero esta vez considerando a las filas que tienen NULL, en ese caso deberíamos escribir:

SELECT
   SUM(MiColumna) / COUNT(*)
FROM
   MiTabla

90 / 7 = 12,8571

Ejemplo 3:

Si lo que nos interesa es hallar el promedio considerando solamente los valores distintos entonces escribiríamos:

SELECT
   AVG(DISTINCT MiColumna)
FROM
   MiTabla

70 / 3 = 23,3333

Hay 3 valores distintos (20, 40, 10) porque uno de ellos (20) está repetido y por lo tanto no se lo usa ni en la suma ni en la cantidad de filas. La suma de los valores distintos (20 + 40 + 10) es 70 y la cantidad de valores distintos es 3, por lo tanto el promedio es 70 / 3 = 23,3333

Artículos relacionados:

La función AVG()

El índice del blog Firebird21