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

Databáze SQLite v C++ - binární data - BLOB

19.11.2019

V předchozích částech tohoto tématu jsme si ukázali základy práce s daty typu celé číslo a text. Nyní se podíváme na obecná binární data - BLOB. Konkrétně jak ukládat a načítat celý obsah zvoleného souboru, v našem případě půjde o obrázky.

V databázi bude tabulka se sloupci (kromě číselného primárního klíče) typu TEXT a BLOB.

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,"
		" nazev TEXT, obrazek BLOB)", nullptr, nullptr, nullptr);
	if (ires != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(ires));
}

Pro přidání nebo editaci položky typu BLOB je klíčová funkce sqlite3_bind_blob64 použitá v rámci prepared statement. Jako ukázku si napíšeme funkci pro přidání souboru (v našem případě budeme přidávat obrázky, ale může samozřejmě jít i libovolný typ souboru) do nové řádky databáze. Do sloupce nazev si uložíme cestu k zadanému souboru.

void pridat_obrazek(const char* file_path)
{
	struct stat stat_file;
	if (stat(file_path, &stat_file) != 0)
		throw std::runtime_error("chyba vstupního souboru");
	if ((stat_file.st_mode & S_IFMT) != S_IFREG)
		throw std::runtime_error("vstupního soubor neexistuje");
	if (0 == stat_file.st_size)
		throw std::runtime_error("soubor je prázdný");
	FILE* pfile = nullptr;
	void* file_buffer = nullptr;
	try
	{
		pfile = fopen(file_path, "r");
		if (nullptr == pfile)
			throw std::runtime_error("chyba fopen");
		file_buffer = malloc(stat_file.st_size);
		if (nullptr == file_buffer)
			throw std::runtime_error("chyba alokace - malloc");
		if (fread(file_buffer, stat_file.st_size, 1, pfile) != 1)
			throw std::runtime_error("chyba čtení souboru");
		sqlite3_stmt* stmt = nullptr;
		char sz_sql[255];
		strcpy(sz_sql, "INSERT INTO polozky (nazev, obrazek) VALUES (?,?)");
		int sqlres = sqlite3_prepare_v2(_sqlite3, sz_sql,
			static_cast(strlen(sz_sql)), &stmt, nullptr);
		if (sqlres != SQLITE_OK)
			throw std::runtime_error(sqlite3_errstr(sqlres));
		sqlres = sqlite3_bind_text(stmt, 1, file_path, (int)(strlen(file_path)), SQLITE_STATIC);
		if (sqlres != SQLITE_OK)
			throw std::runtime_error(sqlite3_errstr(sqlres));
		sqlres = sqlite3_bind_blob64(stmt, 2, (const void*)file_buffer,
				(sqlite3_int64)stat_file.st_size, 0);
		if (sqlres != SQLITE_OK)
			throw std::runtime_error(sqlite3_errstr(sqlres));
		sqlres = sqlite3_step(stmt);
		if ((sqlres != SQLITE_OK) && (sqlres != SQLITE_DONE))
			throw std::runtime_error(sqlite3_errstr(sqlres));
		sqlres = sqlite3_reset(stmt);
		if ((sqlres != SQLITE_OK) && (sqlres != SQLITE_DONE))
			throw std::runtime_error(sqlite3_errstr(sqlres));
		sqlres = sqlite3_finalize(stmt);
		if ((sqlres != SQLITE_OK) && (sqlres != SQLITE_DONE))
			throw std::runtime_error(sqlite3_errstr(sqlres));
		fclose(pfile);
		free(file_buffer);
	}
	catch(const std::exception& e)
	{
		if (file_buffer)
			free(file_buffer);
		if (pfile)
			fclose(pfile);
		throw e;
	}
}

Poté co naplníme databázi pár obrázky (v doprovodném kódu zadané natvrdo cestou), napíšeme si "opačnou" funkci, která projde všechny řádky tabulky a obrázky uloží na původní umístění. Pro otestování před tím původní soubory vymažeme. V tomto případě jsou klíčové funkce sqlite3_column_bytes (vrátí počet BYTů v BLOBu) a sqlite3_column_blob (vrátí ukazatel na data typu BLOB - tento ukazatel je alokován interně uvnitř SQLite takže ho po použití nemusíme a ani nesmíme uvolňovat funkcí free).

void ulozit_obrazky()
{
	std::string str_file = get_cesta_db();
	if (!file_exists(str_file.c_str()))
		throw std::runtime_error("soubor databáze neexistuje");
	int sqlres = sqlite3_open(str_file.c_str(), &_sqlite3);
	if (sqlres != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(sqlres));
	sqlite3_stmt* stmt;
	const char* strsql = "SELECT nazev, obrazek FROM polozky";
	sqlres = sqlite3_prepare_v2(_sqlite3, strsql,
		static_cast(strlen(strsql)), &stmt, nullptr);
	if (sqlres != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(sqlres));
	int istep;
	size_t sizedata;
	void* pdata;
	istep = sqlite3_step(stmt);
	FILE* pfile;
	while (SQLITE_ROW == istep)
	{
		sizedata = sqlite3_column_bytes(stmt, 1);
		pdata = (void*)sqlite3_column_blob(stmt, 1);
		pfile = fopen((const char*)sqlite3_column_text(stmt, 0), "w");
		if (nullptr == pfile)
		{
			istep = sqlite3_step(stmt);
			continue;
		}
		if (1 != fwrite((const void*)pdata, sizedata, 1, pfile))
		{
			printf("chyba zápisu do: %s\n", sqlite3_column_text(stmt, 1));
		}
		fclose(pfile);
		istep = sqlite3_step(stmt);
	}
	sqlres = sqlite3_reset(stmt);
	if (sqlres != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(sqlres));
	sqlres = sqlite3_finalize(stmt);
	if (sqlres != SQLITE_OK)
		throw std::runtime_error(sqlite3_errstr(sqlres));
	sqlite3_close(_sqlite3);
}

Celý kód obsahující i zde volané pomocné funkce si můžete prohlédnout na mém githubu. Přeložit lze kompilátorem GCC příkazem:

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