Vývoj databázového layeru dibi se nezadržitelně blíží k finální verzi. Vyzkoušel jsem si trošku jiný přístup k open source, existoval pouze jediný článek o této knihovně a připomínkování probíhalo v komentářích nebo přes e-maily. Za podněty děkuji zejména Tomáši Bartoňovi. Komorní atmosféra vývoje mi sedla, nemusel jsem se tolik svazovat zpětnou kompatibilitou ani řešit podporu.

Co je dibi?

Jde o minimalistický databázový layer, dobře padnoucí do ruky. Jednosouborová verze obsahující ovladače pro 8 databází (MySQL, MySQLi, PostgreSQL, SQLite, ODBC a experimentální MS SQL, Oracle a PDO) má pouhých … a neřeknu 🙂 Tipněte si.

Dibi má plnit tyto tři cíle:

1) zapouzdřit funkce do intuitivního rozhraní

Samozřejmě do rozhraní objektového, s využitím syntaktických cukrlátek PHP 5 a vyhazováním výjimek. Prostě místo

$link = mysql_connect(...);

$res = mysql_query('...', $link);
while ($row = mysql_fetch_assoc($res)) {
   ...
}

a pozdějšího překopávání na mysqli_query, kde jsou navíc prohozené parametry, je víc sexy psát:

dibi::connect('driver=mysqli');

$res = dibi::query('...');
foreach ($res as $row) {
   ...
}

V tomto směru není dibi nikterak objevné, vlastně je jen náplastí na můj značně nedůvěřivý vztah k open source (a nejen můj), prostě co si nenapíšu sám…

Tip jak mít dibi vždy po ruce: v php.ini si jako auto_prepend_file nastavte soubor, který bude inkludovat dibi nebo tak učiní přes autoloading. Dále nastavte přihlašovací údaje do direktiv mysql.default_user, mysql.default_password a mysql.default_host. Pak stačí v kódu napsat jen dibi::connect(); a hned můžete databázovat.

Tip pro ladění: při ladění aplikací by se vám mohly hodit tyto statické proměnné:

echo dibi::$sql; // zobrazí poslední SQL příklaz
echo dibi::$elapsedTime; // jeho doba trvání v sec
echo dibi::$numOfQueries; // celkový počet SQL příkazů
echo dibi::$totalTime; // jejich celkový čas v sec

2) asistovat při psaní SQL dotazů

Jelikož píšu aplikace na míru konkrétním databázím, bod č. 1 by nikdy nebyl impulsem k psaní další vrstvy. Tím bylo nepohodlné a otravné skládání SQL dotazů. Potřeboval jsem něco, co mi dovolí

  • zapsat dotaz maximálně přehledně (kouknu a hned vidím)
  • bude uvozovat identifikátory a řetězce
  • konvertovat typy boolean nebo date do formátu databáze
  • bude to blbuvzdorné a navíc setsakramentsky rychlé

Hledání správné'n'kůl syntaxe trvalo nesmírně dlouho a dodnes nejde o uzavřenou kapitolu vývoje. Základ tvoří systém modifikátorů…

$name = "Sinead O'Connor";
$age = "41";

dibi::query('
	SELECT *
	FROM [people]
	WHERE [name]=%s', $name, 'AND [age]>%i', $age
);

// SQLite driver ->
//   SELECT * FROM [people] WHERE [name]='Sinead O''Connor' AND [age]>41
// MySQL driver ->
//   SELECT * FROM `people` WHERE `name`='Sinead O\'Connor' AND `age`>41

…a operace nad poli i políčky:

dibi::query('INSERT INTO `people`', array(
	'name' => $name,
	'active' => true,
	'password' => hash('md5', 'nbusr123'),
));

// SQLite -> INSERT INTO [people] ([name], [active], [password])
//  VALUES ('Sinead O''Connor', 1, 'c2750a7d522eb4df4e842980ed3f3e78')
//
// PostgreSQL -> INSERT INTO "people" ("name", "active", "password")
//  VALUES ('Sinead O''Connor', true, 'c2750a7d522eb4df4e842980ed3f3e78')

Dibi také disponuje podporou transakcí, má tzv. podmíněné modifikátory (ale váhám, zda je nezrušit) a pomocníka pro aplikování limitu či offsetu do SQL dotazu. Podrobnější informace o psaní SQL příkazů najdete ve zmíněném článku.

Tip: pokud nechce používat SQL asistenta, vykonávejte příkazy metodou dibi::nativeQuery(...SQL...). Pokud si naopak chcete asistenta vyzkoušet a neprovádět žádné příkazy, zkuste dibi::test(...SQL...).

A ještě jeden: dibi umí zobrazit SQL příkaz s obarvenou syntaxí:

$sql = 'DELETE * FROM [catalog]';
dibi::dump($sql);

dibi::dump(); // bez parametru: zobrazí posledně vykonaný příkaz

3) zapouzdřit result-set a asistovat při zpracování

Po dotazu následuje odpověď a tou je buď true (u příkazů INSERT, UPDATE, …), množina výsledků (SELECT, DESCRIBE, EXPLAIN) nebo výjimka.

U množiny výsledků se zastavím. Přesněji řečeno u objektu, který ji reprezentuje.

$res = dibi::query('SELECT ...');

// můžeme ji číst po řádcích:
while ($row = $res->fetch()) {
   ...
}

// nebo ráz naráz:
$table = $res->fetchAll();

// lze nad ní iterovat:
foreach ($res as $row) ...

// a je možné dokonce rozsah iterace omezit:
foreach ($res->getIterator(3, 5) as $row) ...

// užitečná je i metoda pro získání hodnoty prvního sloupce:
dibi::query('SELECT [name] FROM ...')->fetchSingle();

Statická třída dibi umí cestu k výsledkům ještě zkrátit. Místo dibi::query()->fetch() je možné psát dibi::fetch()

$row = dibi::fetch('SELECT ...');

$field = dibi::fetchSingle('SELECT ...');

$table = dibi::fetchAll('SELECT ...');

Experimentálně jsem zkusil do dibi implementovat práci s automatickým přetypováním vrácených výsledků podle metadat, ale sám to nepoužívám.

Skutečným killerem jsou nenápadné metody fetchPairs() a především fetchAssoc(). První z nich vrátí výsledek v podobě asociativního pole ve tvaru key => value. Který sloupec představuje klíč a který hodnotu je možné volitelně specifikovat parametry.

$pairs = dibi::query('SELECT [name], [age] FROM [people]')->fetchPairs();
// --> array(
//   'Jane Doe' => 25,
//   'John Doe' => 29,
//   ...

Splněným mokrým snem programátorů je však fetchAssoc. Máte SQL dotaz spojující několik tabulek s různými typy vazeb. Databáze z toho udělá prachobyčejnou plochou tabulku. fetchAssoc jí vrátí přirozený tvar a krásu.

Příklad. Mějme tabulku zákazníků a objednávek (vazba N:M) a položíme dotaz:

$res = dibi::query('
  SELECT customer_id, customers.name, order_id, orders.number, ...
  FROM customers
  INNER JOIN orders USING (customer_id)
  WHERE ...
');

A rádi bychom získali vnořené asociativní pole podle ID zákazníka a poté podle ID objednávky:

$all = $res->fetchAssoc('customer_id,order_id');

// budeme jej procházet takto:
foreach ($all as $customerId => $orders) {
   foreach ($orders as $orderId => $order) {
	   ...
   }
}

Asociativní deskriptor má obdobnou syntax, jako když pole píšete pomocí přiřazení v PHP. Tedy 'customer_id,order_id' představuje sérii přiřazení $all[$customerId][$orderId] = $row;, postupně pro všechny řádky.

Někdy by se hodilo, aby se asociovalo podle jména zákazníka namísto jeho ID:

$all = $res->fetchAssoc('name,order_id');

// k prvkům pak přistupujeme třeba takto:
$order = $all['Arnold Rimmer'][$orderId];

Co když ale existuje více zákazníků se stejným jménem? Tabulka by měla mít spíš tvar:

$data = $all['Arnold Rimmer'][0][...];
$data = $all['Arnold Rimmer'][1][...];
...

Rozlišujeme tedy více možných Rimmerů pomocí klasického pole. Asociativní deskriptor má opět formát podobný přiřazování, s tím, že sekvenční pole představuje mřížka:

$all = $res->fetchAssoc('name,#,order_id');

// iterujeme všechny Arnoldy ve výsledcích
foreach ($all['Arnold Rimmer'] as $arnoldOrders) {
   foreach ($arnoldOrders as $orderId => $order) {
	   ...
   }
}

Je to srozumitelné? Pokud ne, zkuste si to přečíst ještě jednou. Bude následovat další level.

Vrátím se k příkladu s deskriptorem 'customer_id,order_id' a zkusím vypsat objednávky jednotlivých zákazníků:

$all = $res->fetchAssoc('customer_id,order_id');

foreach ($all as $customerId => $orders) {
   echo "Objednávky zákazníka $customerId":

   foreach ($orders as $orderId => $order) {
	   echo 'Číslo dokladu: ',  $order['number'];
	   // jméno zákazníka je v $order['name'];
   }
}

Bylo by hezké místo ID zákazníka vypsat jeho jméno. Jenže to bych musel dohledávat v poli $orders. Docela by se šiklo, kdyby výsledky byly v takovémto nějakém tvaru:

$all[$customerId]['name'] = "John Doe";
$all[$customerId]['order_id'][$orderId] = $row;
$all[$customerId]['order_id'][$orderId2] = $row2;

Tedy mezi $customerId a $orderId vrazit ještě mezičlánek. Tentokrát ne číslované indexy, jaké jsem použil pro odlišení jednotlivých Rimmerů, ale rovnou databázový záznam. Řešení je velmi podobné, jen si stačí zapamatovat, že záznam symbolizuje rovnítko:

$all = $res->fetchAssoc('customer_id,=,order_id');

foreach ($all as $customerId => $record) {
   echo "Objednávky zákazníka $record[name]":

   foreach ($record['order_id'] as $orderId => $order) {
	   echo 'Číslo dokladu: ',  $order['number'];
   }
}

Docela by mě zajímalo, jestli jste teď zmatení nebo nadšení 🙂 Asociativní deskriptor je úžasně trefný způsob, jak popsat strukturu libovolně složitého vnořeného pole, jenže uvažovat v několika rozměrech dává lidskému mozku docela zabrat. Přiznám se, že sám občas docela tápu. Celá nápad s deskriptory a realizaci metody fetchAssoc připisuji nějakému vyššímu vnuknutí, protože ji pokaždé musím vstřebávat znovu a znovu. Ano, tento článek si píšu jako tahák pro vlastní kód 🙂

Tip: při ladění aplikace se může hodit vykreslení množiny výsledků v podobě HTML tabulky:

$res = dibi::query('SELECT ...');
$res->dump();

Téměř v cíli

Teď pracuji na logování a profilování provozu. Dibi umožní volat před a po každém SQL příkazu handler, takže bude možné sledovat provoz vlastními nástroji. Stejně jako celé dibi, tak i tato funkčnost se rodí a formuje za chodu. S tím, jak zjišťuji, co vlastně potřebuji a co mi nejlépe vyhovuje. Trvá to sice déle, ale jen tak může vzniknout skutečně užitečný nástroj.

A co Active Record, Object-relational mapping? Dibi bylo zamýšleno jinak, jako pomocník při psaní SQL příkazů. Faktem je, že by mohlo fungovat i jako spolehlivá low-level vrstva pro ORM nadstavbu, ale nemám v plánu to programovat. Tyto techniky sice elegantně řeší určitou sortu úloh, ale vybírají si za to daň v podobě zcela nových problémů. Nejsem jejich fanda (toho času).

Co dibi versus PDO? Obojí jsou data-access abstraction layer, liší se však v jedné podstatné věci: dibi je. Kdežto narazit na hosting s pdo_mysql je úkol hodný zlatokopa. Navíc po dvou letech vývoje mi dibi vyhovuje natolik, že přejít k PDO by byl skok zpět (dokonce i výkonostní). V PDO také chybí některé důležité funkce, např. vyznačení identifikátorů. Stále jej považuji za nedodělek, nicméně má budoucnost jistou, proto existuje PDO driver pro dibi.

Jo, a ještě licence. Dibi šířím pod Dibi licencí (překlad), což je licence ve stylu velmi volné BSD, přidal jsem však několik omezení navíc, které se běžných uživatelů nedotknou. Dibi můžete použít prakticky v jakémkoliv projektu (komerčním, nekomerčním), nesmíte však odstranit nebo zatajit copyrighty. To byste se nedostali do nebe!

Download dibi