Na navigaci | Klávesové zkratky

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

před 17 lety v rubrice dibi | blog píše David Grudl | nahoru

Mohlo by vás zajímat

Komentáře

  1. Martin Grames #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 17 lety
  2. Tomik #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 17 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 17 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 17 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 17 lety
  6. Martin Hujer #6

    avatar

    To už je skoro lepší než v Zend Frameworku 🙂

    před 17 lety
  7. Zdeněk #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 17 lety | reagoval [12] David Grudl
  8. finc #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 17 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 16 lety | reagoval [12] David Grudl [13] v6ak
  10. enoice #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 16 lety
  11. johno #11

    avatar

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

    před 16 lety | reagoval [12] David Grudl
  12. David Grudl #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 16 lety | reagoval [13] v6ak
  13. v6ak #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 16 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 16 lety | reagoval [15] v6ak
  15. v6ak #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 16 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 16 lety | reagoval [17] v6ak
  17. v6ak #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 16 lety | reagoval [19] David Grudl
  18. Charlie #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 16 lety | reagoval [19] David Grudl
  19. David Grudl #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.

    před 16 lety | reagoval [20] v6ak [22] The Zero [23] Petr Procházka
  20. v6ak #20

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

    před 16 lety | reagoval [21] David Grudl
  21. David Grudl #21

    avatar

    #20 v6aku, nemyslel jsem to nijak zle ;)

    před 16 lety
  22. The Zero #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 16 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 16 lety
  24. Michaels #24

    avatar

    #22 The Zero, I u Table jdou joiny.

    před 16 lety
  25. David Grudl #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 16 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 16 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 16 lety
  28. David Grudl #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 16 lety
  29. Martin Mates #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 16 lety | reagoval [30] David Grudl
  30. David Grudl #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 16 lety

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


phpFashion © 2004, 2024 David Grudl | o blogu

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