Nette\Database: refactoring API

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
hrach
Člen | 1838
+
0
-

Ahoj, rad bych zde obsahleji predstavil zmeny, ktere bych chtel brzy pull-requestnou. Tyto zmeny se neobejdou bez BC breaku – sice mensich, ale jsou tam. Zde bych zkusil sepsat vyhody a duvody k dane zmene pristupu.
Branch se zmenami najdete zde: https://github.com/…tte/pull/776

Bindovani parametru preneseno cele do nette

Doted se vyuzivalo bindovani parametru az na strane SQL, to vse pomoci API. Samozrejme toto prakticky fungovalo jen pro cisla, bool a stringy, ve chvili, kdy jste chteli neco jineho, treba pole, ci jinou instanci DB\Table\Selection, pustil se nettí preprocesor a pole patricne zformatoval. Nevyhody tohoto reseni jsou:

  • nejednodnost ve zpracovani parametru
  • neni znamy presny dotaz, ktery se na sql serveru spustil
  • v debug baru je videt miš-maš, protoze nektere query maji parametry, nektere ne, neni hned videt, co dotaz pousti, uzivatele nechapu logiku

Muj pull request rusi bindovani parametru na serveru a pouziva interni preprocesor, do debug baru dumpuje cele query.

Sjednocene API

Jak jsem ve sve prednasce zminoval, Db\Statement dedici od PDO\Statement ma zasadne rozdilen api od Db\Table\Selection. V mem pull requestu se zbavuji PDO\Statementu a sjednocuji api, zvlaste kvuli temto problemum:

  • bug v pdo pri fetch object, nasel hosiplan
  • kvuli hloupemu PDO pristupu, se obcas skrabeme levou rukou za pravym uchem
  • PDO\Statement ma metodu fetchAll, Selection nema
  • PDO\Statement::fetchAll nejde volat dvakrat, podruhe vraci prazdny result-set, to stejne plati pro iterator objektu
  • rozdilnost v implementaci fetchPairs
  • rozdilnost v chovani Db\Row a Db\Table\ActiveRow pro nezname hodnoty, atp., Db\Table\ActiveRow instanceof Db\Row === FALSE, …

Tyto spatne vlastnosti v pull odstranuji a zaroven pridavam zajimav rozhrani:

  • IRowContainer, implementuje Db\Statement
  • IActiveRowContainer extends IRowContainer, implementuje Table\Selection
  • Row, vraci je IRowContainer
  • Table\ActiveRow extends Row, vraci je IActiveRowContainer

Diky tomu je mozne efektive vyuzivat typehinting a jasne rict, co ocekavam k predani. Do budoucna mam zde jeste velmi zajimave napady.

Prinasi to i BC breaky:

  • Nove napsany Db\Row jiz nededi od ArrayHash a cast (array) jiz nevraci standarni pole. Nicmene objekt standardne implementuje iterator, tzn. iterator_to_array($row) funguje.
  • DB\Statement jiz nededi od PDO\Statement, takze je bc break v typehintu, metodach, fechAll odstranena

Novy sql join parser

Dalsi velmi zajimavou a dulezitou novinkou je novy parser na sql joiny.

Nove backjoin api

Bohuzel bylo mnou do nette porotvano backjoin api z notormu, kde nebylo prisli chytre, respektive syntax byla hloupa. Backjonovaci dvojtecka se psala za nazev tabulky, ne pred ni. Nove tedy, pokud je vyznam spojeniy 1:N → tedy hasMany, dvojtecka se pise pred. Toto je BC break!

$db->table('book')->group('author.id', 'COUNT(:books.id) > 1'); // drive count(books:id)

Absolutni cesta joinu

Jak vsichni vime, pro vazby ne jasne vyplivajici ze struktury db (ci proste slozitejsiho smerovani, 1:N, kdy v N jsou dva sloupce do 1), je obcas nutne pouziv primo funkce related a ref, kdy jim reknu primo, kam chci – tedy tabulku a pres ktery sloupce. Uvedme si priklady:

$book->related('book_tags.book_id');  // book.id = book_tag.book_id
$book->ref('author', 'translator_id') // author.id= book.translator_id

Toto jsou naprosto bezne operace, pokud neni DiscoveredReflection, nebo pokud je vazba „slozitejsi“ a DiscoveredReflection si s ni neporadi. Pokud bychom chteli ale tuto vazbu mit jako filtrovaci, tedy joinovat v sql, uz to neni tak jednoduche.

V DicoveredReflection muzeme pouzit:

// vybera vsechny knihy, jejich prekladatel je narozen ...
$db->table('book')->where('moderator.born > ?', ...);

// vybere prekladatele, u kterych byla kniha vydana az ...
// a tady si neporadime ani s DiscoveredReflection :P

Nova moznost dovoluje primou definici joinu, jako u metod related a ref:

$db->table('book')->where('(user, translator_id).born > ?', ...);

$db->table('author')->group('author.id', ':(book.translator_id).published > ?', ...);
// vsimnete si noveho backjoin api, drive by to vypadalo:
// translator:published, respektive (book.translator_id):published

Editoval hrach (19. 9. 2012 14:22)

Caine
Člen | 216
+
0
-

Vypada to zajimave az na
Nova moznost dovoluje primou definici joinu

$db->table('book')->where('(user, translator_id).born > ?', ...);

Nebylo by lepsi rovnou pridat metodu join nez toto?

$db->table('book')->join('table2', 'table1.id = table2.table1_id')
paranoiq
Člen | 392
+
0
-

@caine JOIN je použit pouze pro filtrování. metoda join() by naváděla k tomu, že spojena budou i data obou tabulek, ale tak to není

Caine
Člen | 216
+
0
-

Nerozumim, join se v sql dotazu vytvori tak jako tak..

David Ďurika
Člen | 328
+
0
-

hej vytvory, ale v selection mas zanam(y) iba z jednej tabulky, nikdy nie spojene z viac tabuliek… ako bolo povedane join sluzi lan na filtraciu…

David Grudl
Nette Core | 8228
+
0
-

Zrušit bindované parametry je docela odvážné.

Parametrům se přisuzují dvě výhody:

  1. bezpečnost – nezpůsobí se jimi SQL injection
  2. rychlost – nemusí se kódovat do SQL a poté z něj dekódovat

Bezpečnost nás nemusí zajímat, to zajistí ND vždy. Pokud jde o rychlost, nezaznamenal jsem, že by v případě MySQL bylo bindování rychlejší než prosté skládání SQL. MSSQL nebo Oracle jsem nikdy netestoval, ale ty jsou pro nás spíš okrajové databáze. Mezi důležité patří naopak Postgre, kde se binární data kódují dost ukecaně a z „fyzikální podstaty“ je pak bindování binárních dat rychlejší. Právě kvůli binárním datům mi bindování dává smysl.

Stejně tak nesouhlasím s tím, že by nebindování vylepšilo zobrazování v Debug Baru. Pokud ukládám binární data nebo obyčejný textový článek do DB, fakt nechci v Debug Baru vidět haldy textu, jde mi o smysl SQL dotazu. Současné chování je proto takové, že vše krom dlouhých řetězců dává přímo do SQL, a ty dlouhé bokem do params. Dalo by se to upravit tak, že by se sloupeček params úplně zrušil a do SQL dávalo místo otazníku něco jako "...". Rozhodně jde ale o lepší řešení, než tam třísknout obří kusy textu.

hrach
Člen | 1838
+
0
-

@dg: Nevim jak na tvych projektech, ale ani na jednom mem projektu (vcetne pracovnich) do db nedavame binarni data, takze s tim zkusenost nemam, ale troufnu si tvrdit, ze v 99% pripadu neni v debug baru insert binarnich dat ci clanku. DebugBar je imo priparne pro zobrazovaci stranky, kde provadim ruzne selecty dle priparnich klicu a vazby na dalsi tabulky.

Dlouhe texty opravdu bezne v sql nemam, a neni problem dany sql v debugu zkratit, neni nic neprijemnejsiho nez kdyz mas primarni hodnoty vpravo a neni jasne, ktera kam patri. Urcite je mozne zachovat bindovani pro binarni data. Rychlost mi prijde irelevantni.

David Grudl
Nette Core | 8228
+
0
-

Tady přece nejde o to, jestli ty dáváš do DB binární data nebo ne. Ale že tam nedáváš běžně texty delší než pár znaků je mi ale záhadou – neděláš pro Twitter? ;-)

(Možná je to tím, že po INSERT/UPDATE přesměruješ, takže vnímáš spíš Debug Bar jako zobrazovač SELECTů, kde delší texty fakt nebývají.)

Ivorius
Nette Blogger | 119
+
0
-

Jestli tomu rozumím, bude možné využít vlastní ActiveRow pro překlady ? Nyní mám poděděné Selection, GroupedSelection kde si vytvářím svůj ActiveRowLang – ale není to zrovna asi ideální řešení.

Honza Marek
Člen | 1664
+
0
-

Ivorius napsal(a):

Jestli tomu rozumím, bude možné využít vlastní ActiveRow pro překlady ? Nyní mám poděděné Selection, GroupedSelection kde si vytvářím svůj ActiveRowLang – ale není to zrovna asi ideální řešení.

Tohle je naprosto minoritní „problém“. Jde to řešit mnoha různými způsoby bez přímé podpory v Nette DB. např.:

class ActiveRowTranslator
{
	private $lng;

	function __construct($lng)
	{
		$this->lng = $lng;
	}

	function getTranslatedRow($row, $name)
	{
		return $row->{$name . '_' . $this->lng};
	}
}

$template->registerHelper('artranslate', array(new ActiveRowTranslator('cs'), 'getTranslatedRow'));
Text článku: {$article|artranslate:text}
hrach
Člen | 1838
+
0
-

dg:
(Možná je to tím, že po INSERT/UPDATE přesměruješ, takže vnímáš spíš Debug Bar jako zobrazovač SELECTů, kde delší texty fakt nebývají.)

přesně to sem chtěl rict a myslim si, ze to tak dela majorita. nebo delam neco spatne? :)

David Grudl
Nette Core | 8228
+
0
-

Jenže majorita je jenom část celku a návrh by měl respektovat celek. Že Debug bar neumí zobrazit dotazy při redirektu není ani tak jeho vlastnost, jako spíš nedostatek.

hrach
Člen | 1838
+
0
-

DavidGrudl: no to určitě, ale zobrazovani params na boku rozhodně toto neřešilo. Dotaz nebyl o nic přehlednější. A hlavně, je nepoužitelný po zkopírovaní. Dibi toto neřešilo též. Neříkám, že to nemůže být problém, jen to imo je další boj. Jde ti tedy jen o dlouhé stringy a binární data?

Editoval hrach (17. 9. 2012 11:52)

Filip Procházka
Moderator | 4668
+
0
-

Imho by bylo super, kdyby se dlouhé stringy vůbec nevypisovaly. Mohly by se vypisovat třeba v titulku nad tím '...'. A implementovat přenášení SQL dotazů pomocí session v panelu až do jejich zobrazení taky není špatný nápad a určitě to bude snadné implementovat :)

David Grudl
Nette Core | 8228
+
0
-

Jde jen o dlouhé řetězce a binární data, všechno ostatní se do SQL už vypisuje. Samozřejmě ta forma může být lepší, ale myslím, že je snazší upravit stávající chování než bindování zrušit a pak se snažit v SQL data zkracovat.

hrach
Člen | 1838
+
0
-

Nemas pravdu Davide, respektive ses nechal unest. Tvuj commit sice pridal prepisovani booleanu, ale ta nejvetsi krizovatka, zda bindovat ci preprocesorovat, je uplne nekde jinde.

edit: tak koukam, ze sis ten commit skudlil rok :D
edit2: njn, tak vidim, ze uz si to dopushoval :D

Editoval hrach (17. 9. 2012 19:39)

nAS
Člen | 277
+
0
-

Ještě bych dodal, že např. PostgreSQL umí logovat dotazy, které trvají déle než zvolenou dobu. A pokud se používá binding, tak si pak mohu jednoduše tyto dotazy seřadit podle četnosti a vím kde hledat úzké hrdlo v aplikaci (nevadí mi, když jeden dotaz za den trvá 3 vteřiny, ale vadí mi, když 100 dotazů za minutu trvá půl vteřiny). Pokud se zruší bindování, tak bude výrazně obtížnější sloučit stejné dotazy lišící se pouze hodnotami parametrů.

David Grudl
Nette Core | 8228
+
0
-

Odstranil jsem z Debugger baru sloupeček params a zobrazuje se poskládané SQL se zkrácenými řetězci.

hrach
Člen | 1838
+
0
-

Diky Davide za veskere upravy :) Dany commit sem zrusil a proskrtl text v prvnim prispevku.

Jeste vaham s tou novou syntaxi, jak padlo v prnim komentari, jeslti to neni zbytecne. Rozhodne bych to ale neresil metodou join, ale rozsirenim funkcionality db-reflexe. Neco ve stylu tohoto pullu od @juznacz ktery je opravdu prinejmensim zajimavy a podobnym smerem by se reflexe mela ubirat – tedy pokud selze Discovery, pouzit treba Conventional…

Caine
Člen | 216
+
0
-

Proc delat veci jednoduse, kdyz to jde slozite, ze?:)

Kdyz teda join by evokoval propojovani dat z vice tabulek, proc tu metodu nepojmenovat napr joinHint?

Dale, bude se nejak resit SqlLiteral pro vsechny casti dotazu (napr do order by), toho jsem si tu nejak nevsiml? Takhle si rozsiruju funkcionalitu svyho projektu a najednou potrebuju radit podle subquery (v jednom z nekolika pripadu, kde u zbytku to potreba neni), jenze NDB z tohoho subquery zaclo vytvaret joiny v hlavnim dotazu, cimz drasticky stoupla narocnost onoho dotazu, takze jsem si musel NDB trochu hacknout..

hrach
Člen | 1838
+
0
-

Ne, metoda join proste nebude, a joinHint uz vubec. :P Vtip je v tom, ze imo sou situace, kdy proste i s napovedou neni zapis jednoznacny. Zkusim zanalyzovat :)

Ohledne sql literal – na tom pracuju kdesi jinde a mam zatim tak 10 % hotovo. Neresit tady.

Eda
Backer | 220
+
0
-

ad DebugBar: Z mého pohledu bude vypisování celého SQL i s vloženými parametry vhodnější. Dost často kopíruju položený dotaz třeba do Admineru a dál si s ním hraju. Hledání, kolikátej parametr kde byl, je trošku otrava. Tzn. díky za úpravu :-)

Nové backjoin API vypadá suprově. Konečně to dává smysl.

Jen dotaz k tomuto:

$db->table('book')->where('(user, translator_id).born > ?', ...);
$db->table('author')->group('author.id', ':(book.translator_id).published > ?', ...);

Opravdu má být v prvním případě použita čárka s mezerou mezi tabulkou a cizím klíčem a v druhém tečka? Není to matoucí?

hrach
Člen | 1838
+
0
-

Ano. Ale funguje nezavysle oboji, je to stejne u metod ref a related, u ubou funguje, ze bud zavolam z dvema argumenty, nebo s jednim, kde to oddelim teckou. Volani s dema argumenty pouzivam u ref, protoze tam vyznam tech dvou hodnot nema smysl tabulky.jeji_sloupec, takze by takova notace mohla byt matouci. Parser na joiny se chova stejne jako api, tzn. muzes si vybrat, ale osobne kvuli edukaci pouzivam tento system :)

talpa
Člen | 44
+
0
-

super ze tu tak tleskate, ale me pdo pernamentne na postgresce rve
LSTATE[08P01]: <<Unknown error>>: 7 ERROR: bind message supplies 0 parameters, but prepared statement „pdo_stmt_00000006“ requires 1

	$rows = self::$connection->table(self::$table)->where('contents.id',
			self::$connection->table(self::$tableref)->select('contents_id')->where('categories_id', $categoryid));

--/
bumprask
Člen | 59
+
0
-

Prosím, je někde ke stažení verze, která podporuje tyto nové možnosti přímého zadávání JOIN?

Nova moznost dovoluje primou definici joinu, jako u metod related a ref:

$db->table('book')->where('(user, translator_id).born > ?', ...);

$db->table('author')->group('author.id', ':(book.translator_id).published > ?', ...);
// vsimnete si noveho backjoin api, drive by to vypadalo:
// translator:published, respektive (book.translator_id):published
enumag
Člen | 2118
+
0
-
bumprask
Člen | 59
+
0
-

Ještě taková drobnost, po zkopírování adresáře Database mi to všude hází hlášku:

Can't inherit abstract function Nette\Database\IRowContainer::fetch() (previously declared abstract in Nette\Database\IActiveRowContainer)

Je třeba aktualizovat víc než jen složku Database v mém Nette? Případně copak mám špatně, pouze jsem to zkopíroval?

enumag
Člen | 2118
+
0
-

To bude nějaká blbost, smaž odsud tu funkci fetch a mělo by to být ok.

Pavel Kouřil
Člen | 128
+
0
-

Updatni si Nette na novější než 5.3.9 (nebo .8, tušímže) a bude to ok.

enumag
Člen | 2118
+
0
-

Asi myslíš PHP. :-)

bumprask
Člen | 59
+
0
-

Už to frčí, díky za info. Nová přímá definice joinu je věc, která mi dost chyběla, ovšem objevil jsem jeden háček, týká se dotazu typu např.->(user, user_id).name. V případě, že v dané tabulce existuje více cizích klíčů do stejné tabulky, provede se LEFT JOIN pouze pro poslední definovanou reflexi, nepřipustí více cizích klíčů.

Příklad:
Mám tabulku CONTACT a v ní dva cizí klíče billing_address a delivery_address, které odkazují do společné tabulky ADDRESS.
Dám dotaz typu

->select("(address, billing_address).street` AS  billStreet, (address, delivery_address).street AS delStreet")

Vygeneruje ovšem ve výsledku něco takového:

SELECT address.street AS billStreet, address.street AS delStret FROM contact LEFT JOIN address ON address.id = contact.delivery_address
pekelnik
Člen | 462
+
0
-

Updatni si Nette na novější než 5.3.9

Tohle zavani dirou ve stazi :D

David Grudl
Nette Core | 8228
+
0
-

ad PDO\Statement::fetchAll nejde volat dvakrat:

PDO standardně pracuje s nebufferovanými dotazy, což má dva důsledky:

  • nelze předem zjistit počet řádků
  • nelze nad výsledkem iterovat vícekrát (a není to bug)

Nebufferované dotazy šetří paměť a výkon. Implementace vícenásobného procházení by tedy musela při prvním iterování všechny záznamy ukládat do paměti a držet je pro případné další procházení. A tady si nejsem úplně jist, jestli je to OK. Možná by se to dalo ovládat attributem PDO::MYSQL_ATTR_USE_BUFFERED_QUERY. (Pochopitelně chování PDO je špatné, druhé volání fetchAll by mohlo třeba vyhodit výjimku).

David Grudl
Nette Core | 8228
+
0
-

Co se týče opuštění PDO\Statement: jsem pro, nejspíš by to mělo jít ruku v ruce s opuštěním PDO (tj. Connection nebude potomek PDO. Problém by mohlo být zavedení vlastní výjimky, teoreticky by se dalo udělat něco jako class_alias('PDOException', 'Nette\Database\Exception').

David Grudl
Nette Core | 8228
+
0
-

ad syntax přímé definice joinu:

$db->table('book')->where('(user, translator_id).born > ?', ...);

Vůbec mi nedochází, co (user, translator_id).born má znamenat, nenajdeme nějakou intuitivnější syntax?

hrach
Člen | 1838
+
0
-

David:
jsem rad, ze se uz doslu k tomu opousteni. Ze je to spravna cesta vim uz pul roku, ale nejak to tady zapadlo. ;)

  • buffered queries:
    • jak pises, aktualni verze PR uklada pri fetchi aktualni radek, pro pozdejsi iteraci. Toto by mohlo byt treba zavisle na MYSQL_ATTR_USE_BUFFERED_QUERY, aby pak v poradku pracoval ten dvojnasobny fetchAll
    • vyjimka: chapu to tak, ze chces odchytavat Nette\Database\Exception, ktera === PDOException. Nechapu ovsem rozil oproti dedeni.
  • syntaxe joinu:
    • nevim jak pozorne jsi to cetl, ale to v zavorce ma stejny vyznam, jako pri volani metody ref.; tedy pripojit na tabulku users a joinovat pres books.translator_id = users.id. Nerikam ze jet o uplne intuitivni, ale zachovava to logiku. V pripade DiscoveredReflection je tento pripad zrovna zbytecny, staci translator.id.
    • jsou tu situace, kdy se bez necoho napovedneho neda obejit. tj
      // vybere prekladatele, u kterych byla kniha vydana az ...
      // a tady si neporadime ani s DiscoveredReflection :P
      $db->table('author')->group('author.id', ':(book.translator_id).published > ?', ...);
    • ted me napadl takovy algoritmus, na hledani vazby i pro tento pripad, aby to slo:
      $db->table('author')->group('author.id', ':translator.published > ?', ...);
    • takze jsem celkem naklonen od teto zmeny odstoupit. Navic by tomu jeste moc moc pomohl tento PR od @juznyhttps://github.com/…tte/pull/649

PS: vsimnete si, jak texy spatne odradkovava v prvnim zanorem bloku codu.

David Grudl
Nette Core | 8228
+
0
-

S oddělovačem tečkou je to jasné, ale ta čárka je naprosto matoucí. Čárka slouží k oddělování rovnocenných položek, tady bych ji určitě nepoužíval.

hrach
Člen | 1838
+
0
-

Nojo, jenze s teckou to vypada, jako by dany sloupec patril tabulce users, ne tabule book. Proto, kdyz neco takoveho pisu, tak pro osvetu pouzivam carku (i ve volani metody, kde to tez funguje i s teckou).

David Grudl
Nette Core | 8228
+
0
-

Jo, to vypadá. Jakou syntax teda zvolit? Čárka je fakt špatná (vypadá to jako vícesloupcový klíč), tečka zavádějící.

Každopádně bylo by dobré pořešit nejen sjednocení API, ale API třídy Selection jako takové. Existuje zbytečně moc způsobů, jak řešit totéž (metody find(), get(), offsetGet()), přičemž název není vypovídající (jak se liší find vs. where?; co vrací get() nebo fetch()?).

rixi
Člen | 109
+
0
-

get() je podla mna v pohode (vracia where('id', $id)->fetch())

find() je zbytocny, mozno by bol uzitocnejsi, keby vracal row