Publi

Usando SQLite en nuestros programas en C/C++ (I)


Foto: Eirik Stavelin (Flickr CC-by)
A menudo, nuestros programas necesitan almacenar información (temporal o no) de forma ordenada, rápida y que no nos complique la vida. Luego también necesitamos poder acceder a ella con la misma facilidad. Para eso vale SQLite. Tendremos un pequeño motor de base de datos que con sólo un par de archivos (.h y .c) más un archivo de datos lo tendremos todo listo.

Una pequeña introducción

SQLite nos proporciona una forma muy sencilla de introducir y eliminar información (si estamos familiarizados con el lenguaje SQL) sin las complicaciones de tener un motor de base de datos corriendo (MySQL, MariaDB, PostreSQL, MSSQL…). Por un lado, al no realizar conexiones, todo debería ir mucho más rápido, en bases de datos relativamente pequeñas se nota. Además, no podremos hacer llamadas de forma remota (como hemos hecho siempre, conectando con el gestor de base de datos), ni tampoco podremos montar clusters ni nada de eso (directamente, seguro que hay algún proyecto por ahí que lo permita). Las instrucciones SQL soportadas no son muchas (comparado con motores grandes) pero en muchísimos casos tendremos suficiente.

¿Quién usa SQLite?

En un primer momento, podríamos no adoptar una tecnología que no esté ampliamente aceptada (es normal, en cualquier momento es desatendida y ¡todo nuestro código a la basura!), y una tecnología que no sea libre (sobre todo para poder estudiarla, para ver que no tenga una cara oculta). Éstas son dos condiciones que pongo personalmente cuando empiezo a trabajar con alguna.
SQLite se usa en muchos programas como Skype, algunos programas de Adobe, Firefox, Chrome, Safari y muchos más. Además, tenemos extensiones para SQLite en muchos lenguajes de programación como Java, Python, PHP, y muchísimos más (Wikipedia)

¿Dónde me bajo lo necesario?

Directamente en la web oficial: SQLite download. Yo siempre descargo sqlite-amalgamation, aquí tenemos todo el sistema sqlite en dos archivos sqlite.h y sqlite.c listos para trabajar.
Si utilizas GNU/Linux, lo más probable es que tu distribución tenga un paquete sqlite y otro sqlite-dev con el código fuente (necesitaremos los dos).

Para los ejemplos cuento con que tenemos sqlite3.h en el mismo directorio que el código de nuestro programa.

Compilar el código

Todos los códigos que pondré en el post se compilan de la misma manera con gcc. Aunque tenemos dos posibilidades. Por un lado podemos incluir sqlite en nuestro ejecutable. Nuestro programa pesará más, pero no será necesario tener la biblioteca instalada. Eso sí, para compilar, necesitamos el archivo sqlite3.c de la amalgama de código de sqlite. Esto lo haremos así:

$ gcc -o ejecutable fuente.c sqlite3.c -ldl -lpthread

(Si usas Windows, necesitarás otras bibliotecas diferentes de dl y phtread).

Por otro lado, si queremos aprovechar la biblioteca dinámica de sqlite3 instalada en nuestro sistema (ya que muchas aplicaciones lo utilizan, ahorramos cerca de 1Mb de código en cada ejecutable), lo podemos hacer de la siguiente manera:

$ gcc -o ejecutable fuente.c -lsqlite3

De esta forma, los ejecutables ocuparán muchísimo menos, pero necesitaremos tener SQLite instalado en nuestro sistema.

Hola mundo – Sacando la versión del motor

Como primer programa, antes de hacer nada con la base de datos, vamos a consultar la versión de SQLite que tenemos. Por ejemplo, si utilizamos la versión de SQLite instalada en el sistema es muy interesante, porque puede que nosotros estemos utilizando características propias de una versión de la biblioteca y debemos asegurarnos de que la versión que tiene el usuario es igual o posterior.

sqversion.c

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include "sqlite3.h"

int main(int argc, char* argv[])
{
   printf ("SQLITE LIB Version: %s\n", sqlite3_libversion());
   printf ("SQLITE LIB Version (int): %d\n", sqlite3_libversion_number());

   if (sqlite3_libversion_number()<3001008)
     printf ("Lo siento, tu versión de SQLite es muy antigua\n");
}

En este caso, para visualizar la versión, nos viene bien la forma bonita, a modo de cadena de caracteres, con sus puntos y todo; pero cuando queremos realizar la comparación, nos será mucho más fácil utilizar la forma numérica.

Abriendo y cerrando la base de datos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#include "sqlite3.h"

int main(int argc, char* argv[])
{
   sqlite3 *db;
   int res;

   /* Open database */
   res = sqlite3_open("test.db", &db);
   if (res)
   {
      fprintf(stderr, "No puedo abrir la base de datos: %s\n", sqlite3_errmsg(db));
      exit(0);
   }
   else
   {
      fprintf(stderr, "Base de datos OK\n");
   }

   sqlite3_close(db);

   return 0;
}

Este pequeño programa no hace nada… bueno sólo abre y cierra la base de datos, que normalmente se hará con éxito. Si queremos probar un caso en el que falle, así rápidamente, podemos quitar los permisos de lectura:

$ chmod -r test.db

al archivo test.db para que veamos que también puede fallar el programa.

Nuestra primera consulta : Creando una tabla

Podemos usar este pequeño código para empezar creando una tabla. En este ejemplo, vamos a hacer un pequeño log de eventos de nuestro programa, en el que podemos almacenar la marca de tiempo, nivel (si es más o menos crítico), tipo (otro número), y mensaje.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include "sqlite3.h"

int main(int argc, char* argv[])
{
   sqlite3 *db;
   char *error = 0;
   int res;
   char *sql;

   /* Open database */
   res = sqlite3_open("test.db", &db);
   if (res)
     {
       fprintf(stderr, "No puedo abrir la base de datos: %s\n", sqlite3_errmsg(db));
       exit(0);
     }
   else
     {
       fprintf(stderr, "Base de datos OK\n");
     }
   /* Create SQL statement */
   sql = "CREATE TABLE events ("
     "`timestamp` DATETIME, "
     "`level` NUMBER, "
     "`type` NUMBER, "
     "`message` TEXT)";

   /* Execute SQL statement */
   res = sqlite3_exec(db, sql, NULL, 0, &error);
   if (res != SQLITE_OK)
     {
       fprintf(stderr, "Error: %s\n", error);
       sqlite3_free(error);
     }
   else
     {
       fprintf(stdout, "Tabla creada!\n");
     }

   sqlite3_close(db);

   return 0;
}

De nuevo, si lo ejecutamos, nos dirá que la tabla ha sido creada, y saldrá del programa.

Insertando información en la tabla

Para ello, vamos a cambiar el SQL por lo siguiente:

1
2
3
4
5
6
7
8
   /* STRFTIME('%s','now') - Unix timestamp
    DATETIME(STRFTIME('%s','now')) = DATETIME('now') but we can operate with
       the timestamp
*/

   sql = "INSERT INTO events VALUES (DATETIME(STRFTIME('%s','now'), 'unixepoch'), 1, 2, 'This is a test');"
     "INSERT INTO events VALUES (DATETIME(STRFTIME('%s','now')+86400, 'unixepoch'), 10, 4, 'This is a test again');"
     "INSERT INTO events VALUES (DATETIME(STRFTIME('%s','now')+86400*2, 'unixepoch'), 100, 8, 'This is a test again x2');"
     "INSERT INTO events VALUES (DATETIME(STRFTIME('%s','now')+86400*20, 'unixepoch'), 1000, 16, 'This is a test again x3');";

Hemos insertado cuatro filas de datos, en las que rellenamos la fecha y hora, ponemos valores numéricos en level y type y un mensaje final.
Me he complicado la vida un poco para poner la fecha y hora, más que nada para que se vayan sumando algunos días y las fechas sean diferentes.

Obteniendo datos de la tabla

Ahora el tema va a ser ligeramente diferente, porque vamos a extraer información en lugar de generarla y, en muchos casos, puede ser gran cantidad de información la que estamos extrayendo. En este caso, cuando llamamos a la función sqlite3_exec(), el tercer parámetro que hasta ahora es NULL, va a ser el nombre de una función de callback encargada de recibir los datos por parte de SQLite, y ya, nosotros veremos lo que hacemos con ellos. La forma de pasar los datos, será parecida a cómo recibe los argumentos (desde la función main()) un programa en C o C++, gracias a dos variables: una que devuelve el número de argumentos recibidos (argc) y otra que recibe el contenido de éstos (argv). Además, tendremos otra variable más que recibirá los nombres de los campos. Esta función será llamada a cada fila recibida.

Podemos hacer algo como esto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include "sqlite3.h"

static int selectCb(void *nada, int argc, char **argv, char **colNames){
   int i;

   for(i=0; i<argc; i++){
      printf("%s => %s\n", colNames[i], argv[i]);
   }
   printf("\n");
   return 0;
}

int main(int argc, char* argv[])
{
   sqlite3 *db;
   char *error = 0;
   int res;
   char *sql;

   /* Open database */
   res = sqlite3_open("test.db", &db);
   if (res)
     {
       fprintf(stderr, "No puedo abrir la base de datos: %s\n", sqlite3_errmsg(db));
       exit(0);
     }
   else
     {
       fprintf(stderr, "Base de datos OK\n");
     }

   /* Create SQL statement */
  sql = "SELECT * FROM events;";

   /* Execute SQL statement */
  res = sqlite3_exec(db, sql, selectCb, 0, &error);
   if (res != SQLITE_OK)
     {
       fprintf(stderr, "Error: %s\n", error);
       sqlite3_free(error);
     }
   else
     {
       fprintf(stdout, "SELECT Ok!\n");
     }
   sqlite3_close(db);

   return

En este caso tendremos una salida completa con todos los valores de la tabla, aunque bien podemos usar WHERE, LIMIT, etc.

Tenemos un argumento curioso para sqlite3_exec(), que directamente he colocado como 0 en todas las peticiones, ese valor será una variable que podemos pasar al callback cada vez que se ejecute, y que puede tomar el valor que queramos (y también recibir, que para eso es un puntero), lo que significa que tenemos muchas más posibilidades, como poner un identificador para decidir qué hacer con la información recibida o crear otra estructura basada en listas enlazadas para almacenar todo el SELECT y poder leerlo tras ejecutar sqlite3_exec() si todo ha ido bien.

Una pequeña buena práctica

Es bueno utilizar llamadas a sqlite3_initialize() cuando vamos a empezar a utilizar la biblioteca. Y a sqlite3_shutdown() cuando terminamos de utilizarla y no vamos a hacerlo más. Es verdad que no hace realmente falta, pero podemos compilar especificando el define SQLITE_OMIT_AUTOINIT, como su nombre indica, no auto-inicializará. No ganaremos excesiva velocidad no inicializando porque sqlite3_initialize() cuando ya está el sistema inicializado no hará nada, pero son llamadas y comprobaciones que podemos hacer y si nuestra aplicación exige mucho uso de SQLite o el sistema es modesto, seguro que se agradece.

También podría interesarte....

There are 8 comments left Ir a comentario

  1. Pingback: Usando SQLite en nuestros programas en C/C++ (I) | PlanetaLibre /

  2. Pingback: Usando prepared statements en SQLite en C/C++ /

  3. Manu /
    Usando Mozilla Firefox Mozilla Firefox 50.0 en Windows Windows NT

    En mi programa en C estoy usando esta sentencia:
    “SELECT * FROM test WHERE idreg BETWEEN ? AND ? ORDER BY ?;”
    Los dos primeros parámetros “idreg BETWEEN ? AND ?” funcionan perfectamente pero el tercero “ORDER BY ?” pasa de él, como si no existiera.
    He hecho varias pruebas y no lo consigo… Los tres parámetros les asigno el valor con las funciones sqlite3_bind_…().

    Qué estoy haciendo mal?

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 50.0 en Ubuntu Linux Ubuntu Linux

      Hola Manu, hasta donde yo sé, al ORDER BY no le puedes poner una ? porque SQLite no es capaz de compilar la sentencia. Precisamente ese es uno de los sitios que no le gustan a SQLite.

      Felices fiestas !

  4. Manu /
    Usando Mozilla Firefox Mozilla Firefox 50.0 en Windows Windows NT

    Por cierto, Feliz Navidad

  5. Manu /
    Usando Mozilla Firefox Mozilla Firefox 50.0 en Windows Windows NT

    Gracias Gaspar, felices fiestas…
    Ya me parecía a mi. En MySQL si se puede.
    Yo si puedo compilar la sentencia pero siempre me la ordena por el orden natural (rowid). Por favor me puedes dar un enlace donde se explique eso?

    Lo dicho felices fiestas y gracia por contestar 😉

  6. Alex /
    Usando Mozilla Firefox Mozilla Firefox 58.0 en Windows Windows NT

    Hola, mis felcitaciones por el articulo, es lo que estaba necesitando.

    Estoy iniciando con C++ y me veo en la necesidad de crear una “dll” que interactue con mi aplicación, he podido llevar acabo este proyecto y tu articulo me guio para ese.

    En lo unico que me estoy estancando es que no se como recuperar los valores de una busqueda. Me seria de gran ayuda que me des algunas recomendaciones.

    Desde ya muy agradecido y te animo a que sigas con este tipo de articulos que contribuyen de gran manera.

    Atentamente.

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 57.0 en Linux Linux

      Gracias por tu comentario Alex. Puede que te pueda ayudar la segunda parte del post que encuentras aquí: https://poesiabinaria.net/2015/04/usando-sqlite-en-nuestros-programas-en-cc-ii-nueva-interfaz-v2-y-prepared-statements/

      ¡Ya me vas contando! Un cordial saludo!

Leave a Reply