Empezando con SQLite en C++
Categorías : C y C++, SQL, Programación.

Ú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.
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).

Para evitar errores de compilación relacionados con el archivo stdafx tenemos 2 opciones :
- Crear el proyecto sin encabezado precompilado (No es recomendable para aplicaciones medias o grandes).
- Excluir el archivo "sqlite3.c" del encabezado precompilado. Para ello debemos ir al explorador de soluciones, buscar el archivo mencionado, y hacer click en propiedades. (observa la imagen de la derecha)
Una vez dentro de las propiedades debemos ir a Propiedades de configuración -> C/C++ -> Encabezado precompilado, y desde allí estableceremos la opción Encabezado precompilado a No utilizar encabezados precompilados.

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.

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.
#pragma once#include "sqlite3.h"class BaseDatos {public: ////////// ConstructorBaseDatos(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 datosconst 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 BDconst int MostrarDatos(void);// Función que guarda los datos y cierra la bdvoid Terminar(void);protected: /////// Puntero a la estructura sqlite3 que contiene la base de datossqlite3 *_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.
/* 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 AppDataif (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 BDconst 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
Una vez estamos seguros de tener el directorio para la base de datos, en la
Y por ultimo comprobamos si ha habido algún error, y en ese caso
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.
// 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 consultawhile (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 :
Ahora que ya he explicado como funciona una consulta ya podemos empezar por crear una tabla, echad un vistazo a la función
// 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
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.
// 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 memoriaint Ret = Consulta(L"BEGIN TRANSACTION");// Añado varias entradasRet = 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 datosRet = Consulta(L"COMMIT TRANSACTION");return TRUE;}
Como podéis ver, he utilizado la sentencia
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
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 consultawhile (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 nombrestd::wcout << L"(";std::wcout << sqlite3_column_int(SqlQuery, 2); // Muestro la edadstd::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
Para extraer los datos de una columna se pueden utilizar varias funciones dependiendo del tipo de datos a obtener, por ejemplo en la
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.
// 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 datosif (BD.Iniciar() == 0) {std::wcout << L"Error al crear la base de datos\n";return -1;}// Creación de la tablaif (BD.CrearTabla())BD.InsertarDatosPorDefecto(); // Si la tabla no existia insertamos los datos por defecto/* Opciones :0 Salir1 Insertar datos2 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 datosstd::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 datosBD.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.