Nette\Database: refactoring API
- hrach
- Člen | 1838
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 parametruneni znamy presny dotaz, ktery se na sql serveru spustilv 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 metodufetchAll
,Selection
nemaPDO\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
aDb\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
, implementujeDb\Statement
IActiveRowContainer extends IRowContainer
, implementujeTable\Selection
Row
, vraci jeIRowContainer
Table\ActiveRow extends Row
, vraci jeIActiveRowContainer
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 odArrayHash
a cast(array)
jiz nevraci standarni pole. Nicmene objekt standardne implementuje iterator, tzn.iterator_to_array($row)
funguje. DB\Statement
jiz nededi odPDO\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)
- David Ďurika
- Člen | 328
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
Zrušit bindované parametry je docela odvážné.
Parametrům se přisuzují dvě výhody:
- bezpečnost – nezpůsobí se jimi SQL injection
- 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
@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
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í.)
- Honza Marek
- Člen | 1664
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}
- David Grudl
- Nette Core | 8228
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
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
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
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
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
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
Odstranil jsem z Debugger baru sloupeček params a zobrazuje se poskládané SQL se zkrácenými řetězci.
- hrach
- Člen | 1838
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
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..
- Eda
- Backer | 220
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
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
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
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
aref
:$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
- bumprask
- Člen | 59
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?
- bumprask
- Člen | 59
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
- David Grudl
- Nette Core | 8228
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
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
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
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, stacitranslator.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 @juzny. https://github.com/…tte/pull/649
- 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
PS: vsimnete si, jak texy spatne odradkovava v prvnim zanorem bloku codu.
- David Grudl
- Nette Core | 8228
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.
- David Grudl
- Nette Core | 8228
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()?).