Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

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

Komentáře

  1. Martin Grames http://www.chapadlo.cz #1

    avatar

    Jen si takhle surfuju na netu a co najednou nenajdu – nový blog Davida Grudla o PHP, paráda ! :-)

    Díky, těším se opět na kvalitní články nejen o Nette.

    před 9 lety
  2. Tomik http://tomik.jmx.cz #2

    avatar

    Davide, to je špička! Taky jsem nad tím jeden čas přemýšlel, ale od přemýšlení k realizaci je hodně dlouhá cesta. Díky!

    P.S.: Vítej zpět! ;-)

    před 9 lety
  3. Petr Procházka #3

    Už jsem se začínal po tak dlouhé odmlce bát, ale rozdělit latrine není špatný nápad. Trošku jsem poprskal monitor když jsem dneska poprvé zahlídl novej design, tak doufám že tady zůstane tenhle v jednoduchosti úžasnej.

    Už se těším na další články.

    před 9 lety
  4. Milan #4

    avatar

    Věřil jsem, že David to bez blogu dlouho nevydrží. Ještě by možná bylo dobré, kdyby zde byly odkazy na původní článkz na LaTrine.

    Ale hlavně že je David zpět. Hurááááá

    před 9 lety | reagoval [12] David Grudl
  5. deric #5

    Výborné!! Vypadá to velice přehledně. Skládání dotazů přes pole bylo přece jen dosti krkolomné.

    před 9 lety
  6. Martin Hujer http://www.martinhujer.cz #6

    avatar

    To už je skoro lepší než v Zend Frameworku :-)

    před 9 lety
  7. Zdeněk http://www.dozd.eu #7

    Konečně jsem se dostal k vyzkoušení a musím číct pouze: pěkné, moc pěkné.

    Jen otázečka, je nějaký důvod k tomu, aby byla třída final? Hodilo by se mi, kdybych ji mohl dědit.

    před 9 lety | reagoval [12] David Grudl
  8. finc http://blog.irminsul.cz #8

    avatar

    Vypadá to moc hezky. Takovéto sestavování dotazu je mnohem efektivnější i znovupoužitelnější než opakované sestavování stringu ala SQL.

    Jenom se zeptám, koukal jsi někdy na Hibernate Criteria API? Myslím, že jako námět na implementaci je to naprosto skvělá záležitost.
    V postatě se jedná o něco podobného, alespoň na úrovni zápisu.

    před 9 lety
  9. Šimon #9

    avatar

    Konečně je dibi použitelný. Akorát se mi nelíbí to execute, nebylo by lepší třebe Return, Get, Fetch, … ?

    před 9 lety | reagoval [12] David Grudl [13] v6ak
  10. enoice http://enoice.fialove.eu #10

    avatar

    Super, jsem uchávcen hned ze dvou věcí, dostalo mě jak DibiFluent, tak i phpFashion… Ááá, opravdu se musím přiznat, že mě to hodně potěšilo, že si tě zas můžu přidat do čtečky… :)

    před 9 lety
  11. johno http://johno.jsmf.net/ #11

    avatar

    Prečo tie zátvorky v using('(product_id)') Bohumile?

    před 9 lety | reagoval [12] David Grudl
  12. David Grudl http://davidgrudl.com #12

    avatar

    #4 Milane, odkazy jsou v archívu

    #7 Zdeňku, důvod není, final dám pryč. Ale je to zatím testovací a připomínkovací verze, API a chování se nejspíš změní.

    #9 Šimone, takové ...delete()->from('table')->where('id = %i', $id)->fetch() nebo get() nevypadá moc dobře. Pro jednořádkové select by se teoreticky dalo fetch() použít, jako zkratka execute()->fetch().

    #11 johno, závorky vyžaduje syntax SQL a inteligence, která by je doplnila, v DibiFluent (zatím) není.

    před 9 lety | reagoval [13] v6ak
  13. v6ak http://v6ak.profitux.cz/ #13

    #12 Davide Grudle, +#9 Šimon Mám nápad: ->fetch() by vyžádalo ->limit(1). Buď v SQL dotazu, nebo by při vrácení >1 výsledků vrhnulo výjimku.

    před 9 lety | reagoval [14] Šimon [19] David Grudl
  14. Šimon #14

    #13 v6aku, To je dobrej nápad, ale výjimku bych neházel …

    před 9 lety | reagoval [15] v6ak
  15. v6ak http://v6ak.profitux.cz/ #15

    #14 Šimone, Proč? Mělo by to význam neřízené výjimky (z Javy – výjimka, která by při správném vstupu neměla nastat) a říkalo by to, že je něco špatně. Ale zas pro výkon by bylo lepší LIMIT 1…

    před 9 lety | reagoval [16] Šimon
  16. Šimon #16

    #15 v6aku, Ovšem potom by si musel psat ->limit(1)->fetch(), kdybys jen napsal ->fetch() a automaticky si to vynutilo limit 1 tak by to bylo logičtější, výjimku by to mohlo hodit kdybys dal ->limit(15)->fetch() … Co ty na to?

    před 9 lety | reagoval [17] v6ak
  17. v6ak http://v6ak.profitux.cz/ #17

    #16 Šimone, Špatně jsem se vyjádřil – tím, že by to vyžádalo ->limit(1) jsem myslel, že by to automaticky zavolalo.

    Takže vidím to takhle: fetch v závislosti na limitu se rozhodne o řešení:

    • 1 – return $this->execute()->fetch();
    • null (nebo jak to je, pokud to není nastaveno) – return $this->limit(1)->execute()->fetch();
    • jiné – throw new …Exception(…);
    před 9 lety | reagoval [19] David Grudl
  18. Charlie http://charlie.cz #18

    avatar

    Dřívější absence fluent interfaces byla důvod, proč jsem přešel k Zend_Db. Tušil jsem, že dibi bude něco takového časem obsahovat, tento návrhový vzor totiž rulez :)

    před 9 lety | reagoval [19] David Grudl
  19. David Grudl http://davidgrudl.com #19

    avatar

    Otázka do pléna: $sql->setFlag('IGNORE'); slouží k nastavení příznaků, které se v SQL příkazech zapisují hned po prvním klíčovém slově (např. DISTINCT, HIGH_PRIORITY, SQL_CALC_FOUND_ROWS u SELECT pod MySQL). Váhám, jestli by nebyla stylovější syntax $sql->setIgnore(), $sql->setSqlCalcFoundRows(FALSE) apod. V tom případě bych setFlag zrušil.

    #13 v6aku, -#17 v6ak není lepší zjistit, co si vyžádá praxe?

    #18 Charlie, Zend_Db to umí? Jen se ptám, nikdy jsem to nepoužíval a v manuálu to nejspíš přehlédl.

  20. v6ak http://v6ak.profitux.cz/ #20

    #19 Davide Grudle, Já jsem vpodstatě jen uvažoval nad implementací.

    před 9 lety | reagoval [21] David Grudl
  21. David Grudl http://davidgrudl.com #21

    avatar

    #20 v6aku, nemyslel jsem to nijak zle ;)

    před 9 lety
  22. The Zero http://www.thezero.info/ #22

    avatar

    #19 Davide Grudle, Jo, třídy Zend_Db_Select a Zend_Db_Table_Select, ale jen pro select a u Table zase nejdou joiny.

    před 9 lety | reagoval [24] Michaels
  23. Petr Procházka #23

    #19 Davide Grudle, Používám jen pár příznaků a tak druhá varianta je kratší na zápis, přesto bych preferoval setFlag, které je hezčí.

    U fetch automaticky přidávat ->limit(1) se mi zdá jako dobrej nápad. Podle čeho si myslíš že by si praxe žádala něco jiného? Jen bych asi nevyhazoval vyjímku při zadaní jiné hodnoty, to je podle mě zbytečné.

    před 9 lety
  24. Michaels #24

    avatar

    #22 The Zero, I u Table jdou joiny.

    před 9 lety
  25. David Grudl http://davidgrudl.com #25

    avatar

    Koukal jsem na Zend_Db_Select, to používá malinko jiný přístup. DibiFluent je a má být magické skládání příkazu, syntactic SQL sugar.

    Ad fetch() a limit 1: on je to vlastně dobrej nápad ;)

    před 9 lety
  26. rmaslo #26

    avatar

    Něco podobného (pro akční SQL dotazy) také používám, z tohoto důvodu:

    V té funkci kde to SQL poskládám vím název tabulky. Takže jsem schopen zjistit zda v adresáři „trigery“ existuje třeba PHP script nejakatabulka_afterupdate. Pokud takovýto script najdu tak ho includnu a něm pomocí call_user_func zavolám funkci nejakatabulka_afterupdate(). (Samozřejmě si předám to pole měněných hodnot.)

    Tímto přístupem jsem schopen nahradit „db trigery“ pomocí PHP kódu (vytvářím si v PHP „datové eventy“). Což bylo ve starších verzích MySQL nutnost. I v nových verzích MySQL považuju jazyk trigerů za velmi slabý. A takové věci jako třeba smazat v trigeru soubory na disku asi nikdy nepůjdou. (např. smazat obrázek produktu (soubor na disku) při jeho rušení toho produktu v db)

    Celej tenhle můj pohled na věc vychází z toho že jsem vždy preferoval aplikační logiku v trigerech nikoliv v objektech.

    Takže na závěr taková zvědavá otázka na autora. Nejplánuje něco podovného? tj. třeba v metodě
    dibi::update(‚products‘, …
    hledat zda neexistují funkce products_AfterUpdate() a products_BeforeUpdate?
    A v případě že existují je zavolat? Programátor pak třeba v products_AfterUpdate může u toho produktu nastavit datum a uživatel,e jenž udělal poslední změnu a tím centralizovat aplikační logiku.
    O využití products_BeforeDelete pro kaskádové mazání záznamů (třeba nějakých parametrů produktu) ani nemluvě…

    před 9 lety
  27. gogloid #27

    avatar

    Chtěl poukázat na chybu – s aktuální dibi (dibi 0.9 (revision 155 released on 2008/10/22 13:44:11)) nefunguje příklad

    update(dibi::update('products', $record)
            ->where('product_id = %d', $id);
            ->execute();

    tento ale ano:

    update(dibi::update('products')->set($record)
            ->where('product_id = %d', $id);
            ->execute();

    (Samotnou implementaci jsem zběžně ze zdrojáku nepochopil, tudíž nemůžu napsat bugfix :-), ale naštěstí tohle jsem tipnul a funguje to :-) )

    před 8 lety
  28. David Grudl http://davidgrudl.com #28

    avatar

    Právě naopak, první příklad funguje a druhý vygeneruje error.

    dibi::update('products', $record)
        ->where('product_id = %d', $id)
        ->test();

    vypíše

    UPDATE `products`
    SET ...
    WHERE product_id = NULL
    před 8 lety
  29. Martin Mates http://www.martin-mates.cz #29

    avatar

    Je to moc pěkné! Zkoušel jsem, a funguje parádně. Jenom se chci zeptat, jak bych tímto způsobem zapsal následující SQL dotaz:

    SELECT *, (
            SELECT COUNT(*) FROM precteni P
            WHERE P.id_clanku = C.id_clanku) precteni
    FROM clanky C
    WHERE id_clanku = $id
    LIMIT 0,1`

    tedy vnořený SELECT.

    před 8 lety | reagoval [30] David Grudl
  30. David Grudl http://davidgrudl.com #30

    avatar

    #29 Martine Matesi, dosud to asi nešlo, tak jsem přidal do poslední revize podporu pro vložené selecty:

    dibi::select('*')
        ->select(
            dibi::select('count(*)')
            ->from('precteni')->as('P')
            ->where('P.id_clanku', '=', 'C.id_clanku')
        )
        ->from('clanky')->as('C')
        ->where('id_clanku=%i', $id)
        ->limit(1)
        ->offset(0);
    před 8 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.