NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Empezando con SQLite en C++

22 de Mayo del 2014 por Josep Antoni Bover, 0 visitas, 0 comentarios, 0 votos
Categorías : C y C++, SQL, Programación.
La web está en segundo plano, animación en pausa.
Cargando animación...
Empezando con SQLite en C++

Últimamente estoy inmerso en un proyecto C++ que requiere una base de datos y me decidí por probar SQLite, y a decir verdad la documentación que hay sobre el tema es algo escasa, por ello voy a aportar mi granito de arena creando una aplicación de prueba en la cual se creara una base de datos con una tabla, y luego se usara esta base de datos para leer y escribir datos en ella.

La intención es ver como podemos añadir a un proyecto en C++ una base de datos SQL y como trabajar con ella.

SQLite se puede usar tanto de forma dinamica como estática, es decir con una dll externa o compilando el código dentro de nuestra aplicación. En este documento solo mostraré como compilar SQLite dentro de nuestra aplicación de forma estática.

El código de ejemplo está hecho para compilar en VisualStudio 2013.

En primer lugar debemos descargar el código de SQLite desde el siguiente enlace : SQLite Home.

Yo os recomiendo descargar la última versión de sqlite-amalgamation, que básicamente consta de una cabecera 'h' y un archivo de código 'c'. El termino amalgamation se refiere a que todo el código esta agrupado en un solo archivo 'c', por lo que resulta mucho mas fácil añadir SQLite a cualquier proyecto.


Añadiendo SQLite a nuestro proyecto

Con el SQLite descargado ya podemos crear el proyecto, para este caso yo utilizare una aplicación de consola, y de esta forma me ahorrare bastante código para el interface gráfico.

Una vez creado el proyecto descomprimimos el zip del SQLite en la carpeta donde tenemos el código de este proyecto, y añadimos al proyecto los archivos "sqlite3.h" y "sqlite3.c".

SQLite está hecho en C, por lo que si trabajamos en C++ podemos tener problemas con los encabezados precompilados de visual studio (stdafx.h).

Propiedades del archivo de código

Para evitar errores de compilación relacionados con el archivo stdafx tenemos 2 opciones :

Propiedades de sqlite3.c

Ahora que ya tenemos el proyecto configurado, podemos compilarlo para ver que no da error.

Si os aparece el siguiente error es que no habéis seguido los pasos anteriores correctamente.

Error encabezado precompilado


Empezando

Antes de nada debemos tener claro que en windows por defecto se utiliza el tipo wchar_t para las cadenas de caracteres, y la verdad es que es un follón tener que ir pasando de char a wchar_t, por lo que vamos a programar todo utilizando wchar_t.

SQLite trae un set de funciones para trabajar con wchar_t las cuales terminan con '16'. Por ejemplo para crear/abrir una base de datos se utiliza la función sqlite3_open para char y sqlite3_open16 para wchar_t.

Vamos a empezar por declarar una clase desde donde controlaremos la base de datos, que se llamara BaseDatos.

Archivo : BaseDatos.h
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
#pragma once
#include "sqlite3.h"
class BaseDatos {
public: ////////
// Constructor
BaseDatos(void);
// Destructor
~BaseDatos(void);
// Función que inicia la base de datos (devuelve FALSE en caso de error)
const int Iniciar(void);
// Función para realizar consultas simples que no devuelven datos (CREATE TABLE, INSERT, etc..)
const int Consulta(const wchar_t *ConsultaSQL);
// Función que crea una tabla con la que trabajaremos. (devuelve FALSE si la tabla ya existe)
const int CrearTabla(void);
// Función que inserta los valores especificados en la base de datos
const int Insertar(const wchar_t *Nombre, const unsigned int Edad);
// Función que rellena la tabla con datos (solo se usa cuando CrearTabla devuelve TRUE)
const int InsertarDatosPorDefecto(void);
// Función que imprime por la consola los datos que contiene la BD
const int MostrarDatos(void);
// Función que guarda los datos y cierra la bd
void Terminar(void);
protected: /////
// Puntero a la estructura sqlite3 que contiene la base de datos
sqlite3 *_BD;
}; //
//////////////////

La estructura de la clase BaseDatos es bastante intuitiva, por lo que no requiere explicación.


Iniciando y terminando la base de datos

Bueno para empezar, vamos a necesitar una ubicación donde guardar la base de datos en la que tengamos permisos de escritura. Lo mas fácil es crear un directorio dentro de ProgramData / AppData y tener la base de datos allí. En Windows XP esto no es un problema y podemos crear la base de datos donde nos plazca, pero a partir de Windows Vista necesitamos permisos de escritura para casi todos los directorios.

Veamos la función BaseDatos::Iniciar.

BaseDatos::Iniciar
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
/* Función para iniciar la base de datos, en caso de error retorna FALSE */
const int BaseDatos::Iniciar(void) {
std::wstring TmpStr;
PWSTR Tmp = NULL;
// Obtengo el directorio AppData
if (S_OK == SHGetKnownFolderPath(FOLDERID_ProgramData, NULL, 0, &Tmp)) {
TmpStr = Tmp;
CoTaskMemFree(Tmp);
}
TmpStr += L"\\Empezando con SQLite\\";
// Si no existe el directorio "APPDATA\Empezando con SQLite\" lo creo.
if (GetFileAttributes(TmpStr.c_str()) == INVALID_FILE_ATTRIBUTES)
CreateDirectory(TmpStr.c_str(), NULL);
// Una vez tenemos acceso a nuestro directorio podemos crear la base de datos.
TmpStr += L"Base de Datos.bd";
int Ret = 0;
Ret = sqlite3_open16(TmpStr.c_str(), &_BD);
if (Ret) { // Error creando la BD
const wchar_t *Error = reinterpret_cast<const wchar_t *>(sqlite3_errmsg16(_BD));
std::wcout << Error << L"\n";
sqlite3_close_v2(_BD);
return FALSE;
}
return TRUE;
}

Lo primero que hace esta función es obtener el directorio ProgramData para todos los usuarios utilizando la API SHGetKnownFolderPath. Luego en la línea 12 compruebo si existe el directorio para esta aplicación "Empezando con SQLite" utilizando la API GetFileAttributes, y en caso de no existir lo creamos utilizando la API CreateDirectory.

Una vez estamos seguros de tener el directorio para la base de datos, en la línea 17 creamos la base de datos utilizando la función sqlite3_open16.

Y por ultimo comprobamos si ha habido algún error, y en ese caso lo obtenemos con la función sqlite3_errmsg16, y luego cerramos la base de datos con la función sqlite3_close_v2.

Hay que remarcar que al terminar la aplicación hay que llamar a la función sqlite3_close_v2 para cerrar la base de datos como es debido.


Creando una tabla para la base de datos

Antes de nada hay que tener claro como realizar consultas a la base de datos, para empezar utilizaremos consultas que no devuelven datos, que son las mas fáciles de implementar. Echad un vistazo a la función BaseDatos::Consulta.

BaseDatos::Consulta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Función para realizar consultas simples que no devuelven datos (CREATE TABLE, INSERT, etc..)
const int BaseDatos::Consulta(const wchar_t *ConsultaSQL) {
int SqlRet = 0;
sqlite3_stmt *SqlQuery = NULL;
SqlRet = sqlite3_prepare16_v2(_BD, ConsultaSQL, -1, &SqlQuery, NULL);
if (SqlRet) {
const wchar_t *Error = reinterpret_cast<const wchar_t *>(sqlite3_errmsg16(_BD));
std::wcout << L"Error: " << Error << L"\n";
return FALSE;
}
// Ejecutamos todos los pasos necesarios para la consulta
while (SqlRet != SQLITE_DONE && SqlRet != SQLITE_ERROR) {
SqlRet = sqlite3_step(SqlQuery);
}
sqlite3_finalize(SqlQuery);
if (SqlRet == SQLITE_ERROR) {
const wchar_t *Error = reinterpret_cast<const wchar_t *>(sqlite3_errmsg16(_BD));
std::wcout << L"Error: " << Error << L"\n";
return FALSE; // Error
}
return TRUE;
}

Para realizar cualquier consulta hay que utilizar la función sqlite3_prepare16_v2, que prepara una estructura del tipo sqlite3_stmt, que luego debemos utilizar con la función sqlite3_step. Una vez preparada la consulta debemos llamar a la función sqlite3_step tantas veces como sea necesario hasta que devuelva SQLITE_DONE o SQLITE_ERROR, y por último debemos finalizar la consulta con la función sqlite3_finalize.

En resumen, hay que seguir 3 pasos : preparación, ejecución de cada paso (interno), y finalización de la consulta. Visto así puede parecer algo incomodo, pero cuando una consulta tiene que devolver datos, hay que mirar en cada sqlite3_step si nos ha devuelto datos de la tabla. (lo veremos mas adelante)

Ahora que ya he explicado como funciona una consulta ya podemos empezar por crear una tabla, echad un vistazo a la función BaseDatos::CrearTabla.

BaseDatos::CrearTabla
1
2
3
4
// Función que crea una tabla con la que trabajaremos. (devuelve FALSE si la tabla ya existe)
const int BaseDatos::CrearTabla(void) {
return Consulta(L"CREATE TABLE MiTabla (Id INTEGER PRIMARY KEY, Nombre VARCHAR(260), Edad INT)");
}

Como podéis ver, crear una tabla es de lo mas simple una vez tenemos una función para hacer consultas. También quería remarcar que en SQLite no podemos utilizar la sentencia AUTOINCREMENT, y si queremos tener una Id que se autoincremente tenemos que especificarla como INTEGER PRIMARY KEY. Para mas información echad un vistazo al siguiente enlace : SQLite AUTOINCREMENT.

En esencia se ha creado una tabla con 3 campos, Id que es un valor entero que se autoincrementa (no puede haber 2 ids iguales en la tabla), Nombre que es una cadena de caracteres con un máximo de 260 caracteres, y Edad que es un valor entero.


Insertando datos en la base de datos

El tema de insertar datos a decir verdad no tiene gran complicación tampoco, pero me gustaría remarcar que a la hora de insertar una gran cantidad de filas, el proceso se vuelve bastante lento, y hay una forma de acelerarlo bastante. Echad un vistazo a la función BaseDatos::InsertarDatosPorDefecto.

BaseDatos::InsertarDatosPorDefecto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Función que rellena la tabla con datos (solo se usa cuando CrearTabla devuelve TRUE)
const int BaseDatos::InsertarDatosPorDefecto(void) {
// Especificamos que se trabaje en memoria
int Ret = Consulta(L"BEGIN TRANSACTION");
// Añado varias entradas
Ret = Insertar(L"Ana", 27);
Ret = Insertar(L"Barbara", 21);
Ret = Insertar(L"Clara", 25);
Ret = Insertar(L"devildrey33", 33);
Ret = Insertar(L"Esteban", 31);
Ret = Insertar(L"Francisco", 22);
// Guardamos los datos al archivo de la base de datos
Ret = Consulta(L"COMMIT TRANSACTION");
return TRUE;
}

Como podéis ver, he utilizado la sentencia BEGIN TRANSACTION antes de empezar a insertar los datos, y luego he terminado con la sentencia COMMIT TRANSACTION. Al hacer el BEGIN TRANSACTION lo que sucede es que se hacen todas las operaciones en memoria, y hasta que no terminamos con la sentencia COMMIT TRANSACTION no se guardan en el disco. Otra cosa a destacar es que si sucede un error durante la transacción, todos los datos introducidos durante la misma se perderán.

Durante una transacción la base de datos queda bloqueada para escritura, de forma que si intentamos hacer cualquier consulta que no sea un SELECT desde otro hilo/thread, al llamar a la función sqlite3_step nos devolvera SQLITE_BUSY.

Para mas información os recomiendo el siguiente enlace : SQLite BEGIN TRANSACTION.


Obteniendo datos de la base de datos

En esencia obtener datos de la base de datos viene a ser muy similar a una consulta que no devuelva datos, pero hay que mirar en cada paso si nos devuelve SQLITE_ROW, y en tal caso significa que podemos obtener los datos de una fila. Echad un vistazo a la función BaseDatos::MostrarDatos.

BaseDatos::MostrarDatos
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
const int BaseDatos::MostrarDatos(void) {
int SqlRet = 0;
sqlite3_stmt *SqlQuery = NULL;
SqlRet = sqlite3_prepare16_v2(_BD, L"SELECT * FROM MiTabla", -1, &SqlQuery, NULL);
if (SqlRet == SQLITE_ERROR) {
const wchar_t *Error = reinterpret_cast<const wchar_t *>(sqlite3_errmsg16(_BD));
std::wcout << L"Error: " << Error << L"\n";
return FALSE; // Error
}
std::wcout << L"---------------\n";
// Ejecutamos todos los pasos necesarios para la consulta
while (SqlRet != SQLITE_DONE && SqlRet != SQLITE_ERROR) {
SqlRet = sqlite3_step(SqlQuery);
if (SqlRet == SQLITE_ROW) {
std::wcout << reinterpret_cast<const wchar_t *>(sqlite3_column_text16(SqlQuery, 1)); // Muestro el nombre
std::wcout << L"(";
std::wcout << sqlite3_column_int(SqlQuery, 2); // Muestro la edad
std::wcout << L")\n---------------\n";
}
}
sqlite3_finalize(SqlQuery);
if (SqlRet == SQLITE_ERROR) {
const wchar_t *Error = reinterpret_cast<const wchar_t *>(sqlite3_errmsg16(_BD));
std::wcout << L"Error: " << Error << L"\n";
return FALSE; // Error
}
return TRUE;
}

Cuando la función sqlite3_step devuelve SQLITE_ROW significa que estamos en una fila y podemos obtener sus datos. Para obtener los datos de dicha fila hay que tener muy claro el orden en que se han creado las columnas. Para este caso en concreto la columna 0 es la Id, la columna 1 es el Nombre, y la columna 2 es la Edad.

Para extraer los datos de una columna se pueden utilizar varias funciones dependiendo del tipo de datos a obtener, por ejemplo en la línea 15 utilizamos sqlite3_column_text16 para obtener la columna Nombre en formato wchar_t, y en la línea 17 utilizamos la función sqlite3_column_int para obtener la Edad en formato int.

También es posible obtener datos de un tipo formateados a otro tipo (siempre que la conversión sea posible), por ejemplo si deseamos obtener la columna de la Edad (que es del tipo int) en formato wchar_t podemos utilizar perfectamente la función sqlite3_column_text16.

Para mas información os recomiendo el siguiente enlace : SQLite Result Values From A Query.


La función main de este ejemplo

Para terminar solo queda ver la función main que se ha creado para este ejemplo.

_tmain
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
// Empezando con SQLite.cpp: define el punto de entrada de la aplicación de consola.
#include "stdafx.h"
#include "BaseDatos.h"
#include <iostream>
int _tmain(int argc, _TCHAR* argv[]) {
BaseDatos BD;
int Opcion = -1;
unsigned int TmpEdad = 0;
wchar_t TmpNombre[256];
// Creación / apertura de la base de datos
if (BD.Iniciar() == 0) {
std::wcout << L"Error al crear la base de datos\n";
return -1;
}
// Creación de la tabla
if (BD.CrearTabla())
BD.InsertarDatosPorDefecto(); // Si la tabla no existia insertamos los datos por defecto
/* Opciones :
0 Salir
1 Insertar datos
2 Mostrar datos */
while (Opcion != 0) {
std::wcout << L"Opciones disponibles : 0 Salir, 1 Insertar datos, 2 Mostrar datos\n";
std::wcin >> Opcion;
switch (Opcion) {
case 1 : // Insertar datos
std::wcout << L"Introduce el nombre\n";
std::wcin >> TmpNombre;
std::wcout << L"Introduce la edad\n";
std::wcin >> TmpEdad;
BD.Insertar(TmpNombre, TmpEdad);
std::wcout << L"Datos insertados correctamente\n";
break;
case 2 : // Mostrar datos
BD.MostrarDatos();
break;
}
}
// Se ha salido del bucle principal de opciones, terminamos la BD y salimos.
BD.Terminar();
return 0;
}

Y esto es todo, mi intención era crear un documento donde se explicase como crear una base de datos SQLite dentro de una aplicación de VisualC++, y de esta forma tener mis propios apuntes sobre el tema. Y ya de paso si este documento puede ser de ayuda para alguien mas, mejor que mejor.

Como siempre, podéis descargar el ejemplo de mi web y compilarlo vosotros mismos, el ejemplo incluye el código de SQLite.