Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Databáze SQLite v C++ na Linuxu - podrobněji o základech

24.10.2019

Vygenerování testovací databáze

Nejprve si vytvoříme databázi se stejnou strukturou jako v předchozím díle, a tentokrát ji naplníme 100 řádky na základě vygenerovaných náhodných čísel. Pro lepší přehlednost a další použití si připravíme samostatné funkce pro vytvoření nové (s přepsáním případné již existující) a otevření existující databáze.

#include <cstdio>
#include <stdexcept>
#include <unistd.h>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <assert.h>
#include <sqlite3.h>
	
sqlite3* _sqlite3 = nullptr;

bool directory_exists(const char* fs_path)	
{
	struct stat ss;
	if (stat(fs_path, &ss) != 0)
		return false;
	else
		return (ss.st_mode & S_IFMT) == S_IFDIR;
}

bool file_exists(const char* file_path)	
{
	struct stat ss;
	if (stat(file_path, &ss) != 0)
		return false;
	else
		return (ss.st_mode & S_IFMT) == S_IFREG;
}

std::string get_cesta_db()
{
	std::string db_file = getenv("HOME");
	if (!directory_exists(db_file.c_str()))
		db_file = getpwuid(getuid())->pw_dir;
	if (!directory_exists(db_file.c_str()))
		throw std::runtime_error("Nepodařilo se zjistit domovský adresář.");
	db_file.append("/Dokumenty/test.db");
	return db_file;
}

void vytvorit_databazi()
{
	std::string str_file = get_cesta_db();
	if (file_exists(str_file.c_str()))
	{
		if (remove(str_file.c_str()) !=0)
			throw std::runtime_error("Chyba smazání souboru");
	}
	int ires = sqlite3_open(str_file.c_str(), &_sqlite3);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
	ires = sqlite3_exec(_sqlite3, "CREATE TABLE IF NOT EXISTS polozky (id INTEGER PRIMARY KEY,"
		" cislo INTEGER DEFAULT 0, nazev TEXT)", nullptr, nullptr, nullptr);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
}

void otevrit_databazi()
{
	std::string str_file = get_cesta_db();
	if (!file_exists(str_file.c_str()))
		throw std::runtime_error("Soubor databáze nebyl nalezen");
	int ires = sqlite3_open(str_file.c_str(), &_sqlite3);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
}

Ve funkci pro naplnění dat budeme generovat náhodné číslo v intervalu 1-100, které nastavíme do sloupce cislo> a do sloupce nazev uložíme jeho zobrazení v desítkové a šestnáctkové podobě:

void naplnit_nahodne(size_t pocet = 100)
{
	assert(_sqlite3);
	srand(time(nullptr));
	char text[255];
	int cislo;
	int ires;
	for (size_t i = 0; i < pocet; i++)
	{
		cislo = rand() % 100 + 1;
		sprintf(text, "INSERT INTO polozky (cislo, nazev) VALUES (%d, '%d - %X')", cislo, cislo, cislo);
		ires = sqlite3_exec(_sqlite3, text, nullptr, nullptr, nullptr);
		if (ires != SQLITE_OK)
			throw std::runtime_error(sqlite3_errstr(ires));
	}
}

Podrobněji o callback funkci pro výpis dat

V úvodním díle jsme si ukázali callback funkci pro výpis dat, použitou při volání funkce sqlite3_exec, která musí mít následující formát:

int select_callback(void* p, int count, char** data, char** col_name);

Význam parametrů count, data a col_name již známe z minulého dílu. Prvním (volitelným) parametrem je ukazatel na libovolná data, která chceme do funkce předat. zadáme ho jako 4. parametr funkce sqlite3_exec. Jak ho můžeme využít v praktickém použití? například v aplikaci s uživatelským rozhraním si v něm můžeme předat ukazatel na prvek typu "list-box", do kterého chceme v tom okamžiku data vypsat, nebo cestu k souboru do kterého výpis uložit. V naší ukázce si v tomto parametru předáme text který použijeme jako nadpis výpisu, jak si následně ukážeme. Nyní ještě k významu návratové hodnoty této callback funkce. Tato funkce je samozřejmě volána opakovaně pro každou řádku vrácenou zadaným SELECT příkazem pokud ji ukončíme návratovou hodnotou 0. Pokud z nějakého důvodu chceme procházení vrácených dat ukončit za nějaké podmínky předčasně, vrátíme nenulovou hodnotu. V naší ukázce budeme vypisovat vygenerovaná data tak dlouho dokud nenajdeme ve sloupci cislo hodnotu 78.

int select_callback(void* p, int count, char** data, char** col_name)
{
	for (size_t i = 0; i < count; i++)
	{
		if (p && (0==i))
		{
			printf("%s\n", (const char*)p);
		}
		printf("%s: %s \t", *(col_name+i), *(data+i));
	}
	printf("\n");
	if (strcmp(*(data+1), "78") == 0)
	{
		printf("Nalezeno číslo 78 na pozici %s\nKončím výpis\n", (char*)(*data));
		return 1;
	}
	return 0;
}

Detekce chyb

Mnoho funkcí SQLite, které nevracejí nějaká data indikuje výsledek operace číselnou návratovou hodnotou (typ int). K snadnému vyhodnocení situace existují nadefinované hodnoty s mnemotechnickými názvy. Zatím jsme použili pouze test na hodnotu SQLITE_OK, která jak název napovídá, značí úspěch operace. Jak uvidíme dále, ne všechny ostatní návratové hodnoty musí nutně znamenat chybu. Pro textovou interpretaci návratové hodnoty máme k disposici funkci sqlite3_errstr, kterou jsme také již použili.

Při volání funkce sqlite3_exec máme další možnost získání textové interpretace výsledku. Pokud jako poslední parametr zadáme adresu ukazatele (zdůrazňuji že jde o ukazatel na ukazatel, tedy typ char**, nikoliv "pouze" char*), funkce nám tento ukazatel naalokuje a nakopíruje do něj textový popis, který bude ve většině případů totožný s textem získaným funkcí sqlite3_errstr. POZOR! Pokud na výstupu dostaneme nenulový ukazatel, musíme jej po použití uvolnit funkcí sqlite3_free.

Nyní si již výše uvedené můžeme ukázat na výpisu funkce main ukázkové aplikace používající již uvedené funkce:

int main(int argc, char** argv)
{
	try
	{
		//vytvorit_databazi();
		//naplnit_nahodne();
		otevrit_databazi();
		char* err_msg = nullptr;
		int ires = sqlite3_exec(_sqlite3, "SELECT * FROM polozky",
			select_callback, (void*)"Ukázka výpisu dat", &err_msg);
		if (err_msg)
		{
			printf("err_msg: %s\n", err_msg);
			sqlite3_free(err_msg);
		}
		printf("sqlite3_errstr: %s\n", sqlite3_errstr(ires));
		if (SQLITE_ABORT == ires)
		{
		}
		else
		{
			if (ires != SQLITE_OK)
				throw std::runtime_error(sqlite3_errstr(ires));
		}
		sqlite3_close(_sqlite3);
		printf("\nok\n");
		return EXIT_SUCCESS;
	}
	catch(const std::exception& e)
	{
		printf("Došlo k chybě: %s\n", e.what());
		if (_sqlite3)
			sqlite3_close(_sqlite3);
		return EXIT_FAILURE;
	}
}

Jak je vidět, pokud v callback funkci předčasně ukončíme procházení dat, funkce sqlite3_exec vrátí hodnotu SQLITE_ABORT, tedy jak jsem již zmínil sice různou od SQLITE_OK, ale neznamenající chybu a v našem případě indikující výskyt a nalezení čísla 78 mezi náhodně vygenerovanými daty. Pro demonstraci zde vždy vypisujeme "chybovou" hlášku a v případě výskytu čísla 78 dostaneme na konci výpisu toto:

id: 56 	cislo: 89 	nazev: 89 - 59 	
id: 57 	cislo: 90 	nazev: 90 - 5A 	
id: 58 	cislo: 41 	nazev: 41 - 29 	
id: 59 	cislo: 78 	nazev: 78 - 4E 	
Nalezeno číslo 78 na pozici 59
Končím výpis
err_msg: query aborted
sqlite3_errstr: query aborted

Celý kód si můžete prohlédnou na mém githubu. Přeložit lze kompilátorem GCC příkazem:

g++ main.cpp -g -lsqlite3 -osqlite-cpp