DBM (database manager)
V této kapitole popíši jednoduché datové úložiště. Ačkoliv už máte dostatek znalostí na to, aby jste si dokázali data ukládat do vámi navržených (binárních/textových) souborů, určitě stojí za zvážení, zda nepoužít už hotové, otestované a časem prověřené řešení.
O DBM
DBM byla jednoduchá „databáze“, napsaná v roce 1979 firmou AT&T. Slovo databáze je v uvozovkách, protože se ve skutečnosti jedná jen o jednoduché uložiště dat indexovaných nějakým klíčem.
DBM vám nabízí jednoduchý způsob, jak uložit libovolná data (čísla, řetězce, struktury) pod nějakým klíčem. Klíčem mohou být zase libovolná data. DBM pracuje s klíčem i s daty k uložení jen jako s posloupností bajtů a vůbec se nezajímá o jejich význam.
DBM tedy umožňuje data jednoduše uložit do souboru, načíst je, smazat a procházet celý obsah souboru. A to je všechno. Žádné tabulky, žádné relace, žádné cizý klíče …
Oproti XML má DBM výhodu v jednoduchosti práce a v rychlosti nalezení dat (pokud znáte klíč, pod kterým jsou data uložena).
DBM vyhledá data podle klíče velmi rychle, pomalejší je ale ukládání dat. Z klíče se totiž vždy vypočítává hash a podle toho se pak klíče uspořádávají tak, aby se mohla rychle načíst.
Kdybyste si psali vlastní způsob, jak si uložit nějaká data (nějaké struktury) do souboru, obvykle byste ukládali strukturu neměnné velikosti. Pak můžete snadno říct, že x-tá struktura začíná v souboru na (x-1)*sizeof(data) bajtu (první struktura na 0 bajtu, tedy na začátku souboru). DBM toto ale nevyžaduje, do DBM souboru můžete ukládat libovolná data libovolné velikosti.
V neposlední řadě výhodou DBM je to, že je součástí UNIX standardu, takže by nějaká verze DBM měla být na každém Linuxu.
Oproti XML má DBM nevýhodu jako jakýkoliv jiný binární formát souboru – není přenositelný mezi různými OS (32-bit vs 64 bit, little endian vs big endian).
NDBM
NDBM (new DBM) vznikla v roce 1986 taktéž v AT&T jako náhrada za původní DBM. Ani tato verze se už nepoužívá (kvůli nějaké chybě, která způsobovala, že se občas klíče špatně zaheshovali a došlo ke ztrátě dat). Nicméně její rozhraní (deklrace funkcí) je stále součástí Unix standardu.
NDBM rozhraní poskytuje knihovna GDBM (viz dále). Bohužel, ne na každém systému se používá stejný hlavičkový soubor. Někde je to <ndbm.h> (má tedy stejný název jako původní NDBM), někde zase <gdbm-ndbm.h> (z čehož je jasnější, že se jedná o implementaci NDBM pomocí GDBM). Dříve se dala najít i v <gdbm/ndbm.h>.
Než začnete s GDBM pracovat, nainstalujte si vývojový balík gdbm-devel
(nebo libgdbm-dev nebo jak se se ve vaší linuxové distribuci jmenuje).
Kvůli problému s hlavičkovými soubory jsem si napsal tento jednoduchý skript v bashi, který se snaží uhodnout, jakou knihovnu máte nainstalovanou.
# create-xdbm.sh
if [ -f "/usr/include/gdbm-ndbm.h" ]
then
echo "#include <gdbm-ndbm.h>" > xdbm.h
elif [ -f "/usr/include/gdbm/ndbm.h" ]
then
echo "#include <gdbm/ndbm.h>" > xdbm.h
else
if
[ ! -f "/usr/include/ndbm.h" ] &&
[ ! -f "/src/include/ndbm.h" ]
then
echo "Error: Nebyl nalezen soubor ndbm.h !"
echo -e "\tPravdepodobne nemate nainstalovany vyvojovy balik knihovny gdbm"
echo -e "\tNainstalujte, prosim, balik libgdbm-dev"
else
echo "#include <ndbm.h>" > xdbm.h
fi
fi
Skript vytvoří soubor xdbm.h
, který obsahuje jen příkaz pro includování
knihovny, kterou našel.
Hlavičkový soubor ndbm.h (nebo gdbm-ndbm.h) tedy obsahuje funkce stejného jména se stejným rozhraním, jako původní NDBM databáze (dle standardu). Funkcí je jen pár a snadno se používají.
Rozhraní NDBM
Zde popíšu rozhraní NDBM, tedy struktury a deklarace funkcí (které můžete ve svém programu používat i s „novou“ knihovnou GDBM.
Klíč i data se předávají do NDBM pomocí struktury datum
.
Tato struktura se používá jak pro klíč, tak pro ukládaná data. S obojím pracuje DBM jen jako s polem bajtů, takže mu stačí ukazatel na začátek a délka.
Soubor s databází otevřete / vytvoříte pomocí funkce dbm_open()
.
Argumenty flags a mode mají stejný význam, jako u funkce
open(). Agument flags by měl obsahovat O_RDWR
,
protože databáze potřebuje číst i zapisovat. Pokud bude obsahovat O_CREAT
,
pak se neexistující databázový soubor i vytvoří. Naproti tomu volba O_APPEND
je ignorována (DBM musí být schopný pracovat s celým souborem, aby mohl data přeuspořádávat
tak, aby se snadno našla, takže O_APPEND nedává smysl).
Argument mode, stejně jako u funkce open()
, nastavuje přístupová práva
k souboru, pokud je zrovan vytvářen. Jinak se mode ignoruje.
Podle standardu vytváří DBM dva soubory … jeden pro data, jeden pro indexy (hashe klíčů). Jméno souboru file je doplněno o koncovky .dir a .pag.
DBM * je ukazatel na otevřenou databázi. Pokud dbm_open()
selže, vrátí
NULL.
Po skončení práce s databází se musí uzavřít voláním funkce dbm_close()
:
Pro uložení dat se používá dbm_store()
:
Poslední argument může být DBM_INSERT
nebo DBM_REPLACE
.
Pokud záznam s daným klíčem key už existuje, tak se
s DBM_REPLACE
přepíše, s DBM_INSERT
se nezmění (nový záznam se neuloží).
Přečtení dat z databáze se provede pomocí dbm_fetch()
:
Pokud se záznam nenajde, pak bude datum.dptr NULL.
Smazat data můžete pomocí funkce dbm_delete()
.
A to jsou všechny funkce, které pro prácí s DBM potřebujete. Ještě jsou tu tedy dvě funkce, které můžete použít pro procházení všech záznamů v databázi.
datum dbm_nextkey (DBM *dbf)
Z názvů je asi patrné, jak se funkce používají. Uvidíte to v příkladu níže.
Jak vidíte, moc se toho s daty dělat nedá. Žádné řazení, žádné vyhledávání podle hodnot uložených dat, pouze načítání a ukládání podle klíče, nebo procházení od prvního záznamu k poslednímu (a to nejsou záznamy při procházení nijak setříděné, ani podle pořadí, v jakém jste je ukládali!).
Příklad s NDBM
V tomto příkladu budu ukládat strukturu struct text
.
Ta obsahuje číslo a pole znaků. Dejte si pozor na to, abyste neměli
ve strutkuře jen ukazatel na pole znaků, to by se vám uložil
do souboru jen tento ukazatel a ne samotný text.
Mohl bych ukládat vždy celou strukturu, tj data velikosti sizeof(struct text), ale abych demonstroval, že můžete ukládat data libovolné velikosti, vždy ukládám jen tolik znaků z textu, kolik je potřeba.
Jako klíč k datům používám řetezec, který je uložený v poli klice.
Větší smysl by asi dávalo jako klíč používat id ze sturktury struct text
,
ale přišlo mi lepší ukázat v příkladu textový klíč. Jak už jsem psal, jako klíč můžete
použít cokoliv, klidně i samotnou strukturu. Ale pokud je klíč moc dlouhý, bude
trvat dlouho i jeho hashování a tím se zpomalí načítání i ukládání dat.
/* 28dbm/ndbm.c */
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include "xdbm.h"
#define ULOZISTE "uloziste"
#define MAX_LEN 512
#define POLOZEK 4
struct text {
int id;
char text[MAX_LEN];
};
void printResult(datum * key, datum * result);
int main(void)
{
DBM *dbm;
datum key;
datum content;
datum result;
int i;
struct text polozka;
char *klice[POLOZEK] = { "1", "2", "3", "3" };
char *texty[POLOZEK] = { "jedna", "dva", "tři", "čtyři" };
/* otevreni database */
dbm = dbm_open(ULOZISTE, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (!dbm) {
perror("dbm_open");
return 1;
}
/* ulozeni polozek */
for (i = 0; i < POLOZEK; i++) {
key.dptr = (void *)klice[i];
key.dsize = strlen(klice[i]) + 1;
polozka.id = i + 1;
strncpy(polozka.text, texty[i], MAX_LEN - 1);
polozka.text[MAX_LEN-1] = '\0';
content.dptr = (void *)&polozka;
content.dsize = sizeof(polozka.id) + strlen(polozka.text) + 1;
if (dbm_store(dbm, key, content, DBM_INSERT)) {
perror("dbm_store");
}
}
/* nacteni a vypsani polozky "3" */
key.dptr = (void *)"3";
key.dsize = 2; /* '3' a '\0' */
result = dbm_fetch(dbm, key);
printResult(&key, &result);
/* nacteni a vypsani polozky "4" */
key.dptr = (void *)"4";
key.dsize = 2; /* '3' a '\0' */
result = dbm_fetch(dbm, key);
printResult(&key, &result);
/* nacteni a vypsani vsech polozek */
printf("***\n");
for (key = dbm_firstkey(dbm); key.dptr != NULL; key = dbm_nextkey(dbm)) {
result = dbm_fetch(dbm, key);
printResult(&key, &result);
}
dbm_close(dbm);
return 0;
}
/**
* Zkontroluje, zda neni result prazdny
* a pak jej vytiskne.
*/
void printResult(datum * key, datum * result)
{
struct text r;
if (!result->dptr) {
fprintf(stderr, "polozka[%s]: ", (char *)key->dptr);
perror(NULL);
return;
}
memcpy(&r, result->dptr, result->dsize);
printf("polozka[%s]: [%i, %s]\n", (char *)key->dptr, r.id, r.text);
}
/*------------------------------------------------*/
Při překladu musíte uvést knihovny gdbm a gdbm_compat. Ta druhá zmíněná je tu právě kvůli kompatibilitě s NDBM.
Výstup z programu po prvním spuštění bude vypadat takto:
dbm_store: Invalid argument polozka[3]: [3, tři] polozka[4]: Invalid argument *** polozka[1]: [1, jedna] polozka[2]: [2, dva] polozka[3]: [3, tři]
První chyba je způsobená tím, že jsem se snažil použít stejný klíč pro 4 položku,
jako byl pro třetí. Druhá chyba je způsobená tím, že se snažím načíst položku
pod neexistujícím klíčem "4". Všiměte si,
že chybové hlášení z perror()
je nicneříkající. Vlastně byste
perror()
neměli vůbec používat, protože NDBM se o použití errno vůbec
nezmiňuje. Můžete použít funkce z GDBM, které vám vypíší srozumitelné chyby (viz dále),
čímž přestane vaše aplikace být podle Unix standardu (použijete nestandardní funkce),
na což ale už dneska kálí pes ;-)
Program vytvoří soubory uloziste.dir a uloziste.pag.
Při druhém spuštění se nepodaří zapsat žádnou položku, protože všechny položky s daným klíčem už existují:
dbm_store: Invalid argument dbm_store: Invalid argument dbm_store: Invalid argument dbm_store: Invalid argument polozka[3]: [3, tři] polozka[4]: Invalid argument *** polozka[1]: [1, jedna] polozka[2]: [2, dva] polozka[3]: [3, tři]
GDBM
V předchozí části jste viděli, jak použít GDBM se starším rozhraním NDBM.
GDBM je GNU varianta DBM. Funguje skoro stejně, jen poskytuje některé funkce navíc
(třeba pro výpis chyb). Jediné, co by vás mohlo zarazit je funkce gdbm_open()
,
která má 2 parametry navíc. Oba je ale lepší nechat prázdné (čímž použije
gdbm_open()
defaultní hodnoty).
blok_size se používá při vytváření databáze k určení velikosti různých interních struktur
GDBM. 0 znamená defaultní hodnoty.
fatal_func je odkaz na funkci, která se zavolá, když dojde k fatální chybě. Pokud zadáte
NULL, vypíše se chyba na obrazovku.
Zbylé parametry jsou stejné, jako u dbm_open()
.
GDBM také datový typ GDBM_FILE místo dbm *
. Pro strukturu datum
vrácenou funkcemi gdbm_fetch()
, dbm_firstkey()
a dbm_nextkey()
je alokovaná paměť, takže ji musíte i uvolnit (viz např. funkce
printResult()
, nešťastně to zvolené místo pro uvolňování paměti).
A to už je k rozdílům celkem všechno.
Následující příklad dělá totéž co předchozí, jen obsahuje lepší výpis chyb. GDBM také vytváří jen jeden soubor, k jehož názvu nepřidává žádnou příponu.
Pro výpis chyb používám funkci gdbm_strerror()
,
která dostává jako argument globální proměnnou gdbm_errno
.
- /*------------------------------------------------*/
- /* 28dbm/gdbm.c */
- #include <stdio.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <string.h>
- #include <stdlib.h>
- #include <gdbm.h>
- #define ULOZISTE "uloziste"
- #define MAX_LEN 512
- #define POLOZEK 4
- struct text {
- int id;
- };
- {
- GDBM_FILE dbm;
- datum key,nextkey;
- datum content;
- datum result;
- int i;
- struct text polozka;
- /* otevreni database */
- dbm = gdbm_open(ULOZISTE,0, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, NULL);
- }
- /* ulozeni polozek */
- polozka.id = i + 1;
- polozka.text[MAX_LEN-1] = '\0';
- }
- }
- /* nacteni a vypsani polozky "3" */
- key.dsize = 2; /* '3' a '\0' */
- result = gdbm_fetch(dbm, key);
- printResult(&key, &result);
- /* nacteni a vypsani polozky "4" */
- key.dsize = 2; /* '3' a '\0' */
- result = gdbm_fetch(dbm, key);
- printResult(&key, &result);
- /* nacteni a vypsani vsech polozek */
- key = gdbm_firstkey(dbm);
- result = gdbm_fetch(dbm, key);
- printResult(&key, &result);
- nextkey = gdbm_nextkey(dbm, key);
- key = nextkey;
- }
- gdbm_close(dbm);
- }
- /**
- * Zkontroluje, zda neni result prazdny
- * a pak jej vytiskne.
- * Nakonec uvolni pamet alokovanou pro result.
- */
- {
- struct text r;
- gdbm_strerror(gdbm_errno));
- }
- }
- /*------------------------------------------------*/
Při překladu musíte uvést knihovnu gdbm.
Výstup z programu:
dbm_store: Cannot replace polozka[3]: [3, tři] polozka[4]: Item not found *** polozka[1]: [1, jedna] polozka[2]: [2, dva] polozka[3]: [3, tři]
Druhé spuštění:
dbm_store: Cannot replace dbm_store: Cannot replace dbm_store: Cannot replace dbm_store: Cannot replace polozka[3]: [3, tři] polozka[4]: Item not found *** polozka[1]: [1, jedna] polozka[2]: [2, dva] polozka[3]: [3, tři]
Závěr
Pokud potřebujete ukládat a načítat datové struktury, GDBM je jedna z možností, kterou můžete zvážit.
Rozhraní NDBM používejte jen tehdy, když víte, že bude váš program běhat na systému, který nemá GDBM. Původní NDBM nepoužívejte nikdy (kvůli chybě, o které jsem psal dříve).
Na jiných systémech mohou existovat jiné DBM-like knihovny, které NDBM emulují, takže použítí NDBM je o něco přenositelnější, než použití GDBM, ale na druhou stranu, NDBM toho neumí tolik co GDBM.
Mimochodem, z DBM vychází i Berkeley
DB, která též poskytuje rozhraní pro NDBM.
Berkeley DB používá mnoho slavných programů, například i subversion.
Pokud chcete něco složitějšího a výkonějšího než GDBM, podívejte
se na Berkeley DB
Programmer's Reference Guide. Vývojový balíček v Linuxu pro Berkeley DB se bude jmenovat nějak
jako libdb-4_8-devel
.