Klávesové zkratky na tomto webu - rozšířené Na obsah stránky

MVC paradox a jak jej rešit

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:

  1. výpis stránkovat
  2. mít možnost řadit i podle jiných sloupců
  3. omezit výběr podle vlastní podmínky
  4. 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/pre­senter 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ě.

clock 23. 3. 2009 pencil dibi comments Komentáře: 31


DibiFluent - tekuté SQL příkazy

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

clock 21. 5. 2008 pencil dibi comments Komentáře: 30


Závěrem dibitýdne...

…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í LIMITOFFSET

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

clock 18. 1. 2008 pencil dibi comments Komentáře: 20


Experiment DibiTable

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

clock 15. 1. 2008 pencil dibi comments Komentáře: 15


Extrémě rychlý "load SQL file"

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.

clock 12. 1. 2008 pencil dibi comments Komentáře: 20


phpFashion © 2004, 2010 David Grudlo webu

Pokud není uvedeno jinak, podléhá obsah těchto stránek licenci Creative Commons BY-NC-ND Creative Commons License BY-NC-ND

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.