ORM z druhé strany
Slíbil jsem článek o svém ORM pro PHP, tak tady je... Šel jsem na to trošku z jiné strany. Odkud?
Na úvod jedno upozornění, abych předešel dotazům "kde se to dá stáhnout?": Své kódy nepublikuju, ačkoli k tomu nemám ideologické důvody, spíš praktické. S každým publikovaným kódem se totiž vyrojí i plný pytel remcalů, kteří mají připomínky k tomu či onomu, proč nepoužívám "standardní konvenci zápisu kódu" (=tu jejich), přinejhorším proč píšu otvírací složenou závorku na řádek s hlavičkou funkce a ne až na nový. Někdy to jsou připomínky k věci, ale dost často je diskuse o vznesených výhradách jen neplodné žvanění, které nevede nikam. Takže věci, které píšu pro lidi, dávám ven pod "BSD No Whining" licencí, no a věci, co si píšu pro sebe, nezveřejňuju. Stejný postup zvolím i tentokrát, takže žádný zdroják ke stažení nebude, ostatně bez zbytku knihoven by byl nanic, ale podělím se s vámi o poznatky, můžete si napsat vlastní (a lepší) knihovnu. :)
Jo a ještě: Neobjevil jsem Ameriku. Jen jsem došel k řešení, které přede mnou zkusili i jiní, a shledal jsem ho použitelným pro svoje účely, a teď o tom píšu v naději, že se třeba moje úvahy budou hodit někomu jinému. I kdyby k tomu, že si potvrdíte, že VAŠE cesta je správná a já se mýlím. Je to možné.
Jak to celé vzniklo?
Nějak před Vánoci se roztrhl pytel s ORM pro Dibi. Dva nebo tři lidé začali psát své vlastní ORM pro Davidův DB layer. Jak už to tak bývá, komunita se rozštěpila na frakce - jedni fandili tomu, druzí onomu, třetí ani jednomu a čtvrtí oběma.
Já do světa kolem Nette a Dibi nevstupuju. Uznávám velký kus práce, co na těchto projektech David odvedl, ale po bližším zkoumání obého jsem dospěl k závěru, že zůstaneme raději jen kamarádi. ;) Ale rád si Davida kdykoli poslechnu a jeho kód pročtu, protože je inspirativní.
Stejně tak byla inspirativní debata o ORM. Navíc přišla v pravou chvíli.
Totiž, abych to zasadil do kontextu: Kdysi v dávnověku se dělaly webové aplikace v PHP tak, že jste mastili SQL dotazy přímo do PHP kódu. Kde data, tam SQL. Což byl problém ve chvíli, kdy si někdo vzpomněl, že chce použít jinou databázi.
Logické tedy bylo použít nějaký DB layer, tedy mezivrstvu, kde z jedné strany byla nějaká standardizovaná sada dotazů, a z druhé strany trčely konektory pro různé databázové stroje (třeba ADODB jako příklad takové mezivrstvy mě napadá). Problém byl v tom, že každá databáze uměla něco trošku jiného, takže pro různé DB bylo třeba najít buď minimální průnik funkcí (tedy vlastně skoro nic), nebo nějak "simulovat" SQL a překládat je v layeru na více či méně optimalizované dotazy pro jednotlivé enginy. Což není sranda, jak dojde každému, kdo se podívá na seznam podporovaných DB.
Takže výsledkem byly více či méně zamaskované SQL dotazy s více či méně inteligentní syntaxí. Jo a taky spousta bezpečnostních děr, zrovna ADODB tím bylo pověstné. Navíc vám aplikace neuvěřitelně nakynula.
Sem, do "DB layerů" patří i zmiňovaný Dibi, který je aspoň malý, dobře navržený a bezpečný a celkem elegantně řeší onen problém "minimálního průniku funkcí".
S rozšířením Konečně Objektového PHP začali koumáci koumat, jak by se dal v PHP napsat rozumný ORM, tedy "objektově-relační mapper", čili mezivrstva, co má na jedné straně relační databázi a na druhé straně třídy a objekty v PHP. To na první pohled nevypadá jako problém, na druhý pohled to problém je; ještě před dvěma lety jsem četl, že "opravdový ORM" v PHP napsat nelze.
Přiznám se, i já jsem s ORM koketoval. Napsal jsem si třídu, co měla potomky "tabulka_XYZ", a v těch potomcích byly přepsány sloupce z databáze (třeba: ID, autor_id, nadpis, text, URL, cas). Pak jsem tam taky označil, co je řetězec a co číslo (to kvůli escapování). Pak jsem v kódu zavolal new Comment(), přiřadil jsem potřebné hodnoty a dal flush(). Hodnoty jsem načítal pomocí univerzálního GetByID() nebo pomocí specifických metod (GetAllByAuthorID() např.)
Výhody to má mnohé - z kódu zmizí SQL dotazy, veškerá práce s databází je na jednom místě, líp se to testuje a ladí, líp se to migruje... Obrovská nevýhoda byla to, že každou změnu ve struktuře DB jsem musel ručně přepisovat do patřičných tříd. Hledal jsem, jak to automatizovat, chvíli jsem přemýšlel o řešení Jakuba Vrány, který si, jestli se dobře pamatuju, ve svém nástroji na generování adminských rozhraní (upřesňující poznámka 7.1.) parsuje SQL a z něj si generuje strukturu aplikace, ale furt se mi to zdálo příliš složité. (Jakub k tomu podotýká)
---
Teď před Vánoci jsem při té diskusi zase přemýšlel a říkal jsem si: Jdu na to špatně! Já vycházím z datového modelu, z něj udělám SQL, udělám z něj i třídy, a relace ze SQL prostupují pak celou aplikací (i když zprostředkovaně). Hledám, jak změny v abstraktním relačním modelu, které jako první udělám v SQL, co nejsnadněji reflektovat do objektového modelu v PHP.
Vydal jsem se z druhé strany. Napsal jsem si třídu "ObjectDataStorage", pak jsem ji přejmenoval, když jsem zjistil, že potomci mají názvy jako třeba "ODS_Comments", a ta třída dělala následující:
Z kódu definice svých potomků (jasně že v každou chvíli vím, jak se dostat k PHP kódu každé třídy - výhoda konvencí) si načetla definice sloupců a relací mezi nimi. Například:
private $author_id; ///belongsTo ODS_Authorprivate $title; ///string(150)
No a z tohodle všeho si třída odvodila správné foreign keys a dokázala vytvořit i CREATE TABLE kód, a dokonce ho dokázala vytvořit rekurzivně pro všechny odkázané třídy, a navíc ve správném pořadí.
A už jak jsem to dopisoval, tak jsem viděl, že to je NAPROSTO ŠPATNĚ. Že jsem ohackoval stejný problém z druhé strany, jen místo distribuce změn SQL=>PHP budu distribuovat změny v opačném směru.
Kudy, když tudy ne?
Pokud budu v aplikaci vycházet z abstraktního datového modelu, budu z něj vždycky dělat relační model pro SQL, objektový pro PHP a cosi na půl cesty pro ORM. Což je způsob, který je sice "průmyslovým standardem", ale má nevýhody:
- ORM je poměrně složitý, protože musí být inteligentní.
- Změna je problém,
- což je problém, protože vývoj je změna.
Je jasné, že existují spousty dobrých důvodů, proč to dělat takto. Kdokoli mi jich vyjmenuje na jeden nádech tucet. Jenže mně to připadá nesmyslné. Jasně, dobré důvody jsou dobré, ale spousta dobrých důvodů se časem ukázala coby zatraceně nesmyslné důvody. Třeba: Je opravdu nutné mít u webové aplikace (třeba typu jdem.cz nebo Clipboardu) relační databázi?
Já to zopakuju ještě jednou, pro ty s pomalejší myslí a s pytlem Dobrých Důvodů: Je OPRAVDU NUTNÉ mít na webovém projektu, který je založený na tom, že si uložím nějaká obecná data a pak je načtu a vrátím návštěvníkovi, relační databázi?
Tak - relační databázi má každý hosting. Jsou známé, lidi s nimi umí (no...) pracovat, seženete radu když nevíte, jsou dostatečně výkonné, "myslet v tabulkách" vás naučej na každé VŠ. Spousta dobrých důvodů, že? Ale ve skutečnosti NEPOTŘEBUJETE RELAČNÍ databázi. Zdůrazňuju to "relační".
Jeden čas jsem to řešil tak, že jsem si nad filesystémem simuloval "key:value" databázi - prostě jsem si ukládal záznamy podle nějakého klíče (na Jdem třeba oné zkratky URL) do adresáře data/, pro jistotu rozdrobeného do struktury tak, že např. "abcde" bylo uloženo v data/a/b/c/abcde. Což rozhodně nedoporučuju, pokud těch souborů bude nějak významně hodně, budou malé a vy se rozhodnete to všechno přestěhovat - FTP na tom umře, zabalení vám v ČR pořádně nikdo nenabídne a procházení pomocí rekurzivního opendir() vám zabije server a výsledek je nejistý. Ale pro řádově tisícovku souborů je to celkem slušný způsob.
Ale co, ony existují Dobré Důvody, proč si třeba obrázky uložit do databáze jako bloby. Přiznám se, že jsem je kdysi i znal, ale od doby, co jsem přestal používat ASP a MSSQL a přešel k PHP/MySQL, jsem je (rád) zapomněl a ukládám obrázky do filesystému. Ale to odbočuju.
Výkonnou relační databázi na jednoduchých webech nepotřebujeme. Používáme je, protože je máme, ale ve skutečnosti je nepotřebujeme. Jen nám přidělávají starosti. (A teď vidím nadzdvihnuté žluče všech těch, co se je naučili a chystají se spustit tirádu na téma "Dobrý SQL model ušetří práci". Jo, ve světě, kde s ním pracujete, vám ušetří práci. Ale pokud pracujete tak, že ho nepotřebujete, ušetří vám mnohem víc práce.)
Cesty jsou v zásadě dvě: naprat logiku aplikace do databáze a nechat ji, aby v podstatě "odpovídala sama" (ne že by to bylo nemožné), nebo naopak nechat logiku v PHP kódu, databázi degradovat na úložiště objektových dat a s relačním modelem se vůbec nezatěžovat.
Na webu lze jen těžko jít první cestou (pokud si myslíte že to je schůdnější, zkuste tak navrhnout redakční systém, který by fungoval každému druhému na běžném hostingu - což je to, co je nejčastěji potřeba; specializované IS postavené na zakázku nad Oracle apod. jsou extrémy). Jako logičtější se jeví druhá cesta, tedy degradace DB na pouhé úložiště. Tomu, že je na webu lákavá, napovídá i fakt, že ji zvolily největší servery, od Facebooku přes Twitter a Amazon až po Google. I když ze zcela jiných důvodů než je přenositelnost - především kvůli snadnější správě, vyššímu výkonu či kvůli snadnému provádění MapReduce algoritmu.
Totiž: Čím jednodušší DB, tím levnější DB. Snazší implementace. Snazší škálování a sharding. A jednodušší databázová vrstva. Co je jednoduché, to se snáz testuje. A snáz se to změní.
Když degraduju DB na úložiště, zjednoduším si ORM (vypadne mi to R) :) Důsledek je ten, že datový model bude mít svůj obraz pouze v objektovém světě PHP. Vůbec se nemusím zabývat SQL. Ještě jednodušeji: Vůbec se nemusím zabývat tím, kde data budou ležet - jestli lokálně, vzdáleně, v databázi, filesystému nebo jestli je bude generovat rychlovarná konvice. Prostě si nadefinuju třídy pro "ukládání dat" jako potomky rodičovské třídy "ORM bez R", která se mi postará o ukládání a načítání obsahu. Navíc každému objektu přiřadí unikátní interní ID (stejně by ho měly).
Jak data ukládám? Jako serializované neorganizované shluky. Doslova to, co vypadne ze serialize($this). Rodičovská třída má k dispozici "storage", který implementuje nějaký interface IStorage. V něm jsou metody Ulož a Načti, které uloží nebo načtou data pro objekt s daným ID. Kromě toho umí uložit a načíst index.
Index je nutný, abych např. našel "odkazy, které zadal autor s daným ID". Index jde napříč všemi třídami a je společný pro všechny atributy se stejným jménem. prostě - jak v nějaké třídě použiju author_id, je to identifikátor pro autora a bude zaindexován v indexu "author_id" - ať je to "článek", "komentář" nebo "odkaz". Při hledání si samosebou mohu vyfiltrovat jen objekty určitého typu.
Protože ukládám jako serializovaná data, nemusím nikomu říkat typ jednotlivých atributů ani vazby, ty si naznačuju konvencí: blabla_id je identifikátor nějakého objektu.
Takže vše, co implementuje metody Ulož, Načti, Přidej do indexu a Vyhoď z indexu, může být použito jako objektové úložiště. Implementace pro filesystém je snadná a jasná, implementace pro MySQL taky. Zabralo mi to tři podvečery, a teď mám velmi flexibilní, efektivní a jednoduchý systém pro ukládání dat - v podstatě wrapper pro NoSQL databázi, který si umí poradit i v případech, kdy NoSQL databáze (CouchDB, Mongo...) na serveru není (což není skoro nikdy).
Výhody? Nemusím se šmrdlat se SQL a s relačním modelem. Bez problémů mi funguje lazy load a lazy write, přitom mohu použít syntaxi jako $clanek->uzivatel->jmeno (uzivatel bude z konvence uložen jako uzivatel_id, na pozadí se při načítání objektu "článek" vytvoří prázdný objekt typu uzivatel, pouze s vyplněmým ID, no a když sáhnu pro data, tak se donačte z úložiště). "ORM", který ORM není, je velmi jednoduchý. Zvonku to vyzerá tak, že objekty se umí uložit a továrna je umí "vyrobit". Úložiště je snadné, dá se krásně implementovat sharding, když na to přijde, a je to dobrý krok k NoSQL databázím.
Nevýhody? Prostě "se to takhle nedělá". Každý student prvního ročníku VŠ vám vysvětlí, proč je špatné zahodit strukturu dat a relace. Jak moc to pomáhá. Jak je dobrý relační model základem aplikace a zbytek je jen omáčka. Víceméně samé Dobré Důvody.
Tedy nic vážného. Nepotřebuji relační databázi. Když nepotřebuji relační databázi, nepotřebuji ani relační model. Relační databáze mi ušetří práci jen tehdy, když potřebuju relační databázi. Jinak mi práci přidělá.
---
A jak taková implementace NoSQL bezschémového zmatku nad MySQL může vypadat prakticky? To se dočtete na Zdrojáku v článku MySQL v roli neschémové databáze, v němž představuje spoluzakladatel a CEO webu FriendFeed řešení, k němuž dospěli při vývoji své služby.