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.

#!/bin/bash
# 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.

typedef struct {
    void   *dptr, /* ukazatel na data */
    size_t  dsize /* velikost dat v bytech */
} 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().

DBM * dbm_open (char *file, int flags, int mode)

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():

void dbm_close (DBM *dbf)

Pro uložení dat se používá dbm_store():

int dbm_store (DBM *dbf, datum key, datum content, int mode)

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():

datum dbm_fetch (DBM *dbf, datum key)

Pokud se záznam nenajde, pak bude datum.dptr NULL.

Smazat data můžete pomocí funkce dbm_delete().

int dbm_delete (DBM *dbf, datum key)

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_firstkey (DBM *dbf)
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.

$ gcc -g -Wall -lgdbm -lgdbm_compat -o ndbm ndbm.c

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

GDBM_FILE gdbm_open (const char *name, int block_size, int flags,
                     int mode, void (*fatal_func)(const char *))

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

  1. /*------------------------------------------------*/
  2. /* 28dbm/gdbm.c                                   */
  3.  
  4. #include <stdio.h>
  5. #include <sys/stat.h>
  6. #include <fcntl.h>
  7. #include <string.h>
  8. #include <stdlib.h>
  9. #include <gdbm.h>
  10.  
  11. #define ULOZISTE "uloziste"
  12. #define MAX_LEN 512
  13. #define POLOZEK 4
  14.  
  15. struct text {
  16.     int id;
  17.     char text[MAX_LEN];
  18. };
  19.  
  20. void printResult(datum * key, datum * result);
  21.  
  22. int main(void)
  23. {
  24.     GDBM_FILE dbm;
  25.     datum key,nextkey;
  26.     datum content;
  27.     datum result;
  28.     int i;
  29.     struct text polozka;
  30.     char *klice[POLOZEK] = { "1", "2", "3", "3" };
  31.     char *texty[POLOZEK] = { "jedna", "dva", "tři", "čtyři" };
  32.  
  33.     /* otevreni database */
  34.     dbm = gdbm_open(ULOZISTE,0, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, NULL);
  35.     if (!dbm) {
  36.     fprintf(stderr, "%s\n", gdbm_strerror(gdbm_errno));
  37.         return 1;
  38.     }
  39.  
  40.     /* ulozeni polozek */
  41.     for (i = 0; i < POLOZEK; i++) {
  42.         key.dptr = (void *)klice[i];
  43.         key.dsize = strlen(klice[i]) + 1;
  44.  
  45.         polozka.id = i + 1;
  46.         strncpy(polozka.text, texty[i], MAX_LEN - 1);
  47.         polozka.text[MAX_LEN-1] = '\0';
  48.  
  49.         content.dptr = (void *)&polozka;
  50.         content.dsize = sizeof(polozka.id) + strlen(polozka.text) + 1;
  51.  
  52.         if (gdbm_store(dbm, key, content, GDBM_INSERT)) {
  53.         fprintf(stderr,"dbm_store: %s\n", gdbm_strerror(gdbm_errno));
  54.         }
  55.     }
  56.  
  57.     /* nacteni a vypsani polozky "3" */
  58.     key.dptr = (void *)"3";
  59.     key.dsize = 2;        /* '3' a '\0' */
  60.     result = gdbm_fetch(dbm, key);
  61.     printResult(&key, &result);
  62.     /* nacteni a vypsani polozky "4" */
  63.     key.dptr = (void *)"4";
  64.     key.dsize = 2;        /* '3' a '\0' */
  65.     result = gdbm_fetch(dbm, key);
  66.     printResult(&key, &result);
  67.  
  68.     /* nacteni a vypsani vsech polozek */
  69.     printf("***\n");
  70.     key = gdbm_firstkey(dbm);
  71.     while(key.dptr) {
  72.         result = gdbm_fetch(dbm, key);
  73.         printResult(&key, &result);
  74.  
  75.         nextkey = gdbm_nextkey(dbm, key);
  76.         free(key.dptr);
  77.         key = nextkey;
  78.     }
  79.  
  80.     gdbm_close(dbm);
  81.  
  82.     return 0;
  83. }
  84.  
  85. /**
  86. * Zkontroluje, zda neni result prazdny
  87. * a pak jej vytiskne.
  88. * Nakonec uvolni pamet alokovanou pro result.
  89. */
  90. void printResult(datum * key, datum * result)
  91. {
  92.     struct text r;
  93.  
  94.     if (!result->dptr) {
  95.         fprintf(stderr, "polozka[%s]: %s\n", (char *)key->dptr,
  96.         gdbm_strerror(gdbm_errno));
  97.         return;
  98.     }
  99.     memcpy(&r, result->dptr, result->dsize);
  100.     printf("polozka[%s]: [%i, %s]\n", (char *)key->dptr, r.id, r.text);
  101.     free(result->dptr);
  102. }
  103.  
  104. /*------------------------------------------------*/

Při překladu musíte uvést knihovnu gdbm.

$ clang -g -Wall -lgdbm -o gdbm gdbm.c

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.

Komentář Hlášení chyby
Created: 10.9.2014
Last updated: 10.10.2014
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..