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

Databáze SQLite v C/C++ na Linuxu - základy

22.10.2019

Instalace potřebných nástrojů

Nejjednodušší cestou jak získat vše potřebné pro psaní aplikací nad SQLite v C/C++ je v Linux nainstalovat příslušný vývojářský balíček. V Debianu a jeho derivátech jde o balíček libsqlite3-dev. Při kompilaci a sestavování projektu pak do zdrojového kódu přidáme (direktivou #include) hlavičkový soubor sqlite3.h a do sestavení knihovnu sqlite3, tj. při použití kompilátoru GCC přidáme do parametrů -lsqlite3. Doporučuji si nainstalovat také balíček sqlitebrowser, což je nástroj DB Browser for SQLite, umožňující prohlížet a upravovat databázové soubory SQLite, samozřejmě jak data tak struktury tabulek.

Další možností je stažení zdrojových souborů z webu SQLite. V archivu sqlite-amalgamation-xxx.zip (kde xxx je číslo aktuální verze) najdeme soubory sqlite3.h a sqlite3.c, které přidáme do sestavení projektu. Jak je zřejmé, celý zdrojový kód databáze je v souboru sqlite3.c.

Ukázka vytvoření databáze a přidání dat

Nyní si již můžeme vytvořit jednoduchý ukázkový program, který vytvoří databázový soubor SQLite, v něm vytvoří tabulku s položkami typu číslo a text, do které přidá data. Následné pak data zobrazí textově v konzoli.

Vše bude v jednom zdrojovém souboru (main.cpp) který přeložíme a sestavíme příkazem:

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

Do zdrojového kódu si přidáme potřebné hlavičkové soubory. Kromě běžných céčkovských hlaviček a výše zmíněné hlavičce sqlite3.h si přidáme také specifické Linuxové hlavičky obsahující deklarace funkcí které využijeme ve vlastních pomocných funkcích pro dotazování na souborový systém.

#include <cstdio>
#include <stdexcept>
#include <unistd.h>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <assert.h>
#include <sqlite3.h>

Nejprve si napíšeme jednoduché pomocné funkce pro zjištění existence zadaného souboru a adresáře:

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;
}

Nyní již můžeme přikročit k vytvoření databázového souboru pomocí funkce sqlite3_open, která vytvoří zadaný soubor nebo pokud již existuje, otevře ho a v případě úspěchu naplní ukazatel zadaný jako 2. parametr. Ten je typu sqlite3* a představuje handle na otevřenou databázi. Pokud máme tento platný handle, můžeme použít některou z mnoha funkcí sqlite, například sqlite3_exec, který provede nad databází zadaný SQL příkaz. V našem případě po otevření a vytvoření tabulky přidáme do databáze pro ukázku 2 položky.

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");
printf("Soubor databáze: %s\n", db_file.c_str());
int ires = sqlite3_open(db_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));
ires = sqlite3_exec(_sqlite3, "INSERT INTO polozky (cislo, nazev) VALUES (1, 'první položka')",
	nullptr, nullptr, nullptr);
if (ires != SQLITE_OK)
	throw std::runtime_error(sqlite3_errstr(ires));
ires = sqlite3_exec(_sqlite3, "INSERT INTO polozky (cislo, nazev) VALUES (2, 'druhá položka')",
	nullptr, nullptr, nullptr);
if (ires != SQLITE_OK)
	throw std::runtime_error(sqlite3_errstr(ires));

V kódu jak vidno používám výjimky při zjištění chyby, proto celý kód z něhož je uvedená část je uvnitř bloku try-catch.

Nyní si ještě ukážeme jeden ze způsobů jak procházet tabulku a vypsat všechny položky, resp. ty které nadefinujeme v SQL příkazu SELECT. Opět použijeme funkci sqlite3_exec s tím, že jako 3. parametr uvedeme vlastní callback funkci, která bude volaná pro každý výsledný řádek příkazu select. V této funkci máme v parametrech vše potřebné, tj. počet sloupců a pole ukazatelů na data v jednotlivých sloupcích a také názvy sloupců. V našem případě může tato funkce vypadat například takto:

int select_callback(void* p, int count, char** data, char** col_name)
{
	for (size_t i = 0; i < count; i++)
	{
		printf("%s: %s \t", *(col_name+i), *(data+i));
	}
	printf("\n");
	return 0;
}

A volání příkazu SELECT pak vypadá takto:

ires = sqlite3_exec(_sqlite3, "SELECT * FROM polozky",
	select_callback, nullptr, nullptr);
if (ires != SQLITE_OK)
	throw std::runtime_error(sqlite3_errstr(ires));

Po použití databázi zavřeme funkcí:

sqlite3_close(_sqlite3);

Ukázkový program najdete také na mém githubu a kompletní kód vypadá takto:

#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;
}

int select_callback(void* p, int count, char** data, char** col_name)
{
	for (size_t i = 0; i < count; i++)
	{
		printf("%s: %s \t", *(col_name+i), *(data+i));
	}
	printf("\n");
	return 0;
}

int main(int argc, char** argv)
{
	try
	{
		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");
		printf("Soubor databáze: %s\n", db_file.c_str());
		int ires = sqlite3_open(db_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));
		ires = sqlite3_exec(_sqlite3, "INSERT INTO polozky (cislo, nazev) VALUES (1, 'první položka')",
			nullptr, nullptr, nullptr);
		if (ires != SQLITE_OK)
			throw std::runtime_error(sqlite3_errstr(ires));
		ires = sqlite3_exec(_sqlite3, "INSERT INTO polozky (cislo, nazev) VALUES (2, 'druhá položka')",
			nullptr, nullptr, nullptr);
		if (ires != SQLITE_OK)
			throw std::runtime_error(sqlite3_errstr(ires));
		ires = sqlite3_exec(_sqlite3, "SELECT * FROM polozky",
			select_callback, nullptr, nullptr);
		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(e.what());
		if (_sqlite3)
			sqlite3_close(_sqlite3);
		return EXIT_FAILURE;
	}
	
}