Už je to šílených dvanáct let, co jsem na tomto blogu
představil knihovnu Dibi. Dnes se dočkala
čtvrté verze. Využívající všech předností moderního PHP 7.1.
Vývoj a testování verze 4.0 trvalo 11 měsíců a čítá 114 komitů,
zapojilo se do něj několik autorů, kterým děkuji.
A co je nového? Dibi 4 běží v přísném režimu, tedy s
declare(strict_types=1)
. Je plně typovaná, tedy parametry a
návratové hodnoty metod mají nativní typehinty. Což si vyžádalo jednu
drobnou změnu: metody fetch()
nebo fetchSingle()
v případě, že už v tabulce není další řádek, vracejí nově
null
na místo false
, protože návratové hodnoty
mohou být nullable,
nikoliv falseable. Doplněná byla podpora pro JSON (automaticky
dekóduje tyto sloupce), kontroluje, zda s modifikátorem %i
nebo
%f
nepoužijete řetězec, co není číslo, přibyl
Dibi\Expression
a Connection::expression()
(příklad), interface
IConnection
a spousta dalších drobností.
Protože Composer se dnes považuje za standard, jak instalovat balíčky,
archív ZIP i s minifikovanou verzí zmizel v propadlišti dějin.
Změnou je, že metody Connection::query()
a
Fluent::execute()
nevrací v případě DML příkazů počet ovlivněných
řádek, ale objekt Dibi\Result
. Počet řádek zjistíte z něj
($res->getRowCount()
) nebo jako dříve
($connection->getAffectedRows()
).
Dále objekt Dibi\DateTime
je nyní potomkem DateTimeImmutable .
Má v sobě implementovaný magický mechanismus, který by měl odhalit, pokud
někde v kódu stavíte na tom, že je mutable, a došlo by tak k chybě.
Pak jsem dal pryč několik historických reliktů nebo zbytečností,
kompletní přehled najdete v changelogu. Ač ten
seznam může vypadat dlouze, v praxi byste krom výše zmíněného neměli na
žádný BC break narazit.
A ještě pro úplnost: Dibi od verze 3.1 podporuje mikrosekundy, což
může ve specifickém případu vést k BC breaku (viz
vlákno) a od verze 3.2 podporuje jen třídy s namespaces (tedy krom
třídy dibi
).
Co bude dál?
Určitě zajímavé by bylo do Dibi doplnit podporu pro nativní bindování
parametrů, třeba pro upload binárních souborů je to nutnost. A s tím úzce
souvisí i prepared statements. Občas zaznívají žádosti o vylepšení
fluent interface, volání uložených procedur atd.
Zcela na rovinu říkám, že budoucnost stojí zejména na tom, jestli budu
mít za Dibi nějaké příspěvky. Takže pokud máte Dibi rádi, nastavte prosím měsíční
donation a svět bude nadále krásný 😁
Pokud se rozhodnete tvořit aplikace podle architektury
Model-View-Controller, dříve nebo později zjistíte, že postupy, které tak
krásně vypadají na papíře, při praktické realizaci pokulhávají. Narazit
se dá už u nejtriviálnějších úkolů – třeba obyčejný výpis
článků na blogu.
Teorie hovoří takto:
- model má metodu
getArticles()
, která vrátí všechny
články (například z databáze jako result-set)
- controller / presenter tuto metodu zavolá a získaný seznam předá
do view
- view se postará o vykreslení
V praxi se ale ukáže, že budeme chtít navíc:
- výpis stránkovat
- mít možnost řadit i podle jiných sloupců
- omezit výběr podle vlastní podmínky
- určit, které sloupce přenášet
Přitom půjde vždy o požadavky prezentační logiky. Model nemusí
zajímat, že výpis článků stránkujeme (bod 1). Nemusí ho zajímat, že
jsme se rozhodli na web přidat boxík s deseti nejčtenějšími články
(body 2, 3, 4). Nemusí ho nic z toho zajímat, avšak postup, kdy získáme
z databáze tabulku všech článků, ty setřídíme a vyfiltrujeme samotnou
aplikací a nakonec zahodíme nepotřebná data, by v praxi představoval
šílený overhead. Proto se zdá vhodnější rezignovat na MVC a zamořit
model spoustou metod alá getArticles()
, které vracejí články
různě seřazené a filtrované, vždy na míru konkrétním
požadavkům view.
…a nebo lze použít dibi 🙂
Řešení podle DibiDataSource
Dibi nabízí více způsobů, jak MVC paradox řešit, nicméně
DibiDataSource je pro tento typ úkolu vyloženě navržen. Zkusím tedy
nastínit koncept řešení.
Nejprve implementujeme model a jeho metodu getArticles()
. Ta se
pomocí SQL dotazu (libovolně složitého) dotáže na všechny články a
jejich atributy.
class Model
{
function getArticles()
{
return dibi::dataSource('SELECT ... FROM table1 INNER JOIN table 2 ... WHERE ...');
}
}
Pokud dibi znáte, všimněte si, že místo obvyklého
dibi::query()
nebo dibi::fetchAll()
zde volám
dibi::dataSource()
. Jinak se nic neliší.
Controller/presenter aplikuje stránkovací logiku:
class AnyPresenter extends Presenter
{
...
public function renderDefault($page)
{
$articles = $this->model->getArticles(...);
// přidáme nějakou dodatečnou podmínku
$articles->where('category=%i', $this->category);
// inicializujeme stránkovač
$paginator = new Paginator; // Paginator je třída z Nette Framework
$paginator->itemsPerPage = 40;
$paginator->itemCount = count($articles); // vrací celkový počet článků; ekvivalent k $articles->count()
$paginator->page = $page;
// a omezíme datasource na LIMIT a OFFSET
$articles->applyLimit($paginator->length, $paginator->offset);
// seznam článků předáme do šablony
$this->template->articles = $articles;
}
...
}
Nakonec si šablona může říci, o které sloupce má zájem a může
také nastavit vlastní řazení (kvůli čitelnosti jsem vynechal volání
htmlSpecialChars):
<h1>Stručný přehled článků</h1>
<?php foreach ($articles->select(array('url', 'title', 'perex'))->orderBy('title') as $article):?>
<h2><?= $article->title ?></h2>
<p><a href="<?= $article->url ?>"><?= $article->perex ?></a></p>
<?php endforeach ?>
Klíčové je, že se provedou jen dva SQL dotazy, které si
vyžádají jen skutečně vypisovaná data. V tomto případě se
třeba nemusí přenášet celé texty článků, protože šablona vypisuje
pouze perexy. Výsledkem je přehledný kód, optimalizované využítí
databáze a neposkvrněná architektura MVC. Za zmínku také stojí, že dotaz
na databázi se provádí až při volání count
v presenteru a
foreach
v šabloně.
Doplnění: bohužel, pro MySQL
to je nepoužitelné ☹
Dibi má ode dneška velice šikovnou novinku. Jak se vám líbí zápis SQL
příkazů ve stylu fluent interfaces?
$res = dibi::select('product_id')->as('id')
->select('title')
->from('products')
->innerJoin('orders')->using('(product_id)')
->orderBy('title')
->execute();
// nebo
$record = array(
'title' => 'Výrobek',
'price' => 318,
'active' => true,
);
dibi::update('products', $record)
->where('product_id = %d', $id);
->execute();
Tento přístup má jednu zásadní výhodu – velmi snadno se SQL
parametrizuje. Továrna může připravit SQL příkaz, který později
upravím:
function sqlFactory()
{
return dibi::select('*')
->from('products')
->innerJoin('orders')->using('(product_id)')
->where('active = ', true);
}
$sql = sqlFactory();
// doplníme řazení
$sql->orderBy('price');
$sql->orderBy('name')->asc((bool) $dir);
// doplnime podminku
$sql->where('[price] > %i', $minPrice)->or('[price] IS null');
// a ještě SQL flag
$sql->setFlag('IGNORE');
// todo: možná bude vhodnější $sql->setIgnore();
Tuhle hračku jsem se poukoušel implementovat už asi dvakrát, ale pokud to
chcete udělat fakt hodně dobře, není to taková legrace. Tentokrát jsem na
to kápl. Výsledkem je třída DibiFluent. Není omezena na příkaz SELECT,
ale poradí si s jakoukoliv syntaxí. Také velmi úzce spolupracuje s dibi
modifikátory, což vidíte v uvedených příkladech. A možná budete
překvapeni, jak je její kód krátký a srozumitelný.
…tu mám ještě několik novinek. Především, nyní je možné do
řetězce zapsat více modifikátorů najednou a teprve poté uvést jejich
hodnoty:
dibi::query('
SELECT * FROM [table]
WHERE id = %i AND added > %d', $id, $time
);
Tímto se z modifikátorů stávají takové chytré „placeholders“. Už
ani nemusí být umístěny zcela na konci řetězce.
Zároveň zavádím několik nových modifikátorů:
%ex
pro expanzi pole do argumentů (Rubysté znají jako
*[1,2,3]
)
%or
a %and
před polem spojí jeho prvky
s oddělovačem ‚AND‘ nebo ‚OR‘
%lmt
a %ofs
pro přenositelné a možná
snadnější nastavení LIMIT
a OFFSET
Příklady použití:
$where[] = '[age] > 20';
$where[] = '[email] IS NOT NULL';
dibi::query('SELECT * FROM [table] WHERE %and', $where);
// SELECT * FROM [table] WHERE [age] > 20 AND [email] IS NOT NULL
nebo také
$where['age'] = 20;
$where['email'] = 'franta@example.com';
dibi::query('SELECT * FROM [table] WHERE %and', $where);
// SELECT * FROM [table] WHERE [age]=20 AND [email]='franta@example.com'
Limity a ofsety:
// with limit = 30, offset = 90
dibi::query('SELECT * FROM [products] %lmt %ofs', 30, 90);
// SELECT * FROM [products] LIMIT 30 OFFSET 90
// with offset = 100
dibi::query('SELECT * FROM [products] %ofs', 100);
// pro SQLite:
// SELECT * FROM [products] LIMIT -1 OFFSET 100
// pro MySQL:
// SELECT * FROM [products] LIMIT 18446744073709551615 OFFSET 100
// pro PostgreSQL:
// SELECT * FROM [products] OFFSET 100
Funkčnost modifikátorů %and
& %or
považujte
za experimentální. Očekávám vaše připomínky a podle nich ji případně
upravím. Jinak změny si vyžádaly větší zásah do zdrojového kódu
dibi-překladače a ačkoliv jsem všechno velmi důkladně
otestoval, buďte při nasazování pozorní.
Knihovna se neustále vyvíjí, aktuální informace
najdete na webu
dibiphp.com.
Zkusil jsem do dibi přidat takovou
hračku:
/*
CREATE TABLE [albums] (
[id] INTEGER NOT NULL PRIMARY KEY,
[artist] VARCHAR(100) NOT NULL,
[title] VARCHAR(100) NOT NULL)
*/
class Albums extends DibiTable
{
}
Třída Albums
reprezentuje databázovou tabulku (pokud jméno
tabulky neurčíme, bude se detekovat z názvu třídy). Nad ní můžeme
vykonávat třeba tyto operace:
$albums = new Albums;
// přečtení jednoho záznamu podle primárního klíče:
$key = 5;
$data = $albums->fetch($key);
// smazání záznamu č. 7
$albums->delete(7);
// smazání více záznamů
$count = $albums->delete(array(1, 2, 4));
// úprava záznamu č. 4
$data->title = 'new title';
$albums->update(4, $data);
// obdobně jako u delete lze měnit více záznamů
// vložení záznamu
$id = $albums->insert($data);
Dále lze provádět jednoduché výběry:
// řádky s klíčem 2, 3 a 5
foreach ($albums->find(2, 3, 5) as $row) {
...
}
// vypiš celou tabulku v HTML
$albums->findAll()->dump();
// výběr s řazením podle artist, title
$albums->findAll('artist', 'title');
Jde o velmi jednoduchého pomocníka (tzv. helper) pro rutinní operace nad
tabulkou. Tedy žádné složité ORM nebo ActiveRecords. Tuším se tomu
říká Table Data
Gateway. Funkce je čerstvá a ve stádiu experimentování.
Ještě drobnost. Dibi je už ze své podstaty zcela imunní vůči SQL
injection, takže se jich nemusíte obávat:
$key = '3 OR 1=1'; // podvrh
$albums->delete($key);
// --> DELETE FROM [albums] WHERE [product_id] = 3
Tak hele, v šest ráno po mně nemůžete chtít kultivovaný titulek,
spokojte se i s tímto.
K zálohování nebo přenášení databází mezi více servery se
používá tzv. SQL dump. Jde o textový soubor obsahující popis struktury
i obsahu tabulek ve formě série SQL příkazů. K jeho generování
z příkazové řádky je určen nástroj mysqldump
, na hostingu
se obvykle používá interaktivní phpMyAdmin.
Aplikaci phpMyAdmin lze použít i k obnovení ze zálohy, tj. načtení
SQL dumpu. Bohužel se s tím neskutečně párá a proces trvá moc dlouho.
Databáze obsahující jen pár tisíc záznamů nelze takto vůbec
importovat – to dřív vyprší časový limit běhu PHP skriptu.
Pokusil jsem se kdysi napsat rychlejší importér a povedlo se. Co
phpMyAdmin louská dlouhé minuty, zvládne tento za zlomek sekundy.
Nástroj jsem nyní začlenil do dibi a
používá se takto:
dibi::connect();
dibi::loadFile('dump.sql');
Lze načítat i komprimovaný soubor:
dibi::loadFile('compress.zlib://dump.sql.gz');
Soubor se čte postupně, takže nevadí, když je větší než
dostupná paměť.
Metoda vrací počet vykonaných příkazů. V případě chyby vyhodí
výjimku. Ještě zdůrazňuji, že je určen pro SQL dump ve formátu, který
generují zmíněné nástroje.
Tip: SQL dump generujte se zaškrtnutou volbou „Rozšířené inserty“
(Extended inserts), má to zásadní vliv na rychlost načítání, ať už
používáte jakýkoliv importér.
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:
…pokračování
Uplynulo sedm měsíců od doby, kdy jsem tu poprvé psal o databázovém
layeru dibi. Nechtěl jsem předvádět hotové řešení, spíš
otevřít diskusi. Ale místo podnětů mi přišlo několik desítek žádostí
o zdrojové kódy ;)
Konečně mohu všechny žadatele potěšit. Náhledová verze je
k dispozici:
Upozornění: Knihovna dibi se neustále vyvíjí.
Její popis v tomto článku postupně aktualizuji, takže některé
komentáře pod ním mohou být již nesouvisející. Aktuální informace
najdete na webu
dibiphp.com.
Řešení, které jsem navrhoval v původním článku, dnes považuji
z více důvodů za překonané. Co se však nezměnilo, to jsou cíle tohoto
layeru:
- maximálně ulehčit práci programátorům. Jak?
- zjednodušit zápis SQL příkazů, co to jen půjde
- snadný přístup k metodám, i bez globálních proměnných
- funkce pro několik rutinních úkonů
- eliminovat výskyt chyby. Jak?
- přehledný zápis SQL příkazů
- přenositelnost mezi databázovými systémy
- automatická podpora konvencí (escapování/slashování, uvozování
identifikátorů)
- automatické formátování spec. typů, např. datum, řetězec
- sjednocení základních fcí (připojení k db, vykonání příkazu,
získání výsledku)
- a především KISS (Keep It Simple, Stupid)
- zachovat maximální jednoduchost
- raději jeden geniální nápad, než 10.000 hloupých řádků kódu
A naopak záležitosti, o které mi vůbec nejde:
- zajištění kompatibility SQL příkazů
- emulace funkcí chybějících některým databázím
- vytvoření bohatých knihoven plných funkcí
- nechci konkurovat ActiveRecords apod., jde mi jen
o čisté SQL
A také neřeším následující věci (a vysvětlím proč):
- funkce pro zkoumání struktury databáze a tabulek
- prepared SQL statements
Pokud neprogramujete aplikaci typu phpMyAdmin, tak žádné funkce na
zkoumání databázové struktury nepotřebujete. Vlastně bych řekl, že
jejich potřeba vypovídá o špatně navržené aplikaci. Dokud tuto
funkčnost nebudu potřebovat, nebo ji nenaprogramuje někdo jiný, tak v dibi
nebude 😉
Prepared SQL statements jsem taktéž shledal zbytečnými. Proč?
Především se mi nikdy nestalo, že bych v jednom skriptu volal tolikrát
tentýž SQL příkaz lišící se jen v hodnotách parametrů. Za druhé se
podle mých měření zrychlení dosažené pomocí prepare pohybuje
v řádu procent. V reálném nasazení je tedy naprosto zanedbatelné. Oproti
tomu takový vícenásobný INSERT, který dibi
podporuje, umí zrychlit vkládání až tisícinásobně. A nakonec –
výhody, které prepared statements přináší z programátorského hlediska,
tedy pohodlné vkládání proměnných, řeší dibi výrazně lépe.
Takže pojďme se podívat, jak to celé funguje.
…pokračování