Dnešním článek začíná malá série ve které představím ORM, které je integrované ve framworku Jet.
Ještě než se pustím do ORM, tak se musím k něčemu přiznat. Vím, že to tu čtou lidé co mě znají osobně a někdy i velice dlouho. Před mnoha lety jsem byl alergický jen na slovo ORM 😀 Nechtě jsem o tom ani slyšet …
Proč? Protože práce s databází je nejužší hrdlo každé online aplikace. Webserver a aplikace se dá snadno škálovat vertikálně a hlavně i horizontálně. Nic extra složitého. Ale jak je problém s databází, myšleno když aplikace s databází špatně operuje a není dobře optimalizovaná, tak nepomůže žádné škálování a nepomůže ani svěcená voda, ba ani povolaný exorcista. Teda pokud ten exorcista náhodou není profík na online aplikace a vše co s tím souvisí – tedy i na relační databáze a SQL. A už jsem viděl mnoho aplikací, kde bylo pro vygenerování stránky potřeba stovky dotazů … A už jsem viděl rádu kolegů, kteří to vůbec neřešili. Měli ústa plná jiných „magických pouček“, ale že se jim server potí při každém obyčejném požadavku na obyčejnou stránku jim bylo jedno.
Vždy jsem zastával názor, že dobrý vývojář má relačním databázím a SQL rozumět a to co nejlépe jak je to možné. A tento názor pevně zastávám stále. To že se někdo učí pouze používat nějakou abstrakci nad databází aniž by opravdu věděl co a proč dělá a co se ve skutečnosti děje, jaké dotazy jsou spuštěné a jak jsou/nejsou optimalizované, takový přístup považuji za špatnou cestu, která se na velkém projektu zcela určitě vymstí.
A ORM jsem považoval za něco, co tento přístup (učit se „jak“ a ne se učit chápat „proč?“) podporuje. Pletl jsem se, viděl jsem to moc černobíle a neviděl zjevné výhody.
Tedy ano, nadále tvrdím, že dobrý vývojář má mít co nejlepší znalosti v oblasti databází, SQL a vše co s tím souvisí. Dobrý vývojář má umět vyhledávat problematické dotazy a má umět tyto analyzovat a řešit. Dobrý vývojář má umět přivřít oči a aplikaci si „pustit v hlavě“ – prostě u práce přemýšlet, protože právě to a ne datlování kódu je podstata naší práce. Dobrý vývojář má vědět a hlavně chápat (to především) a ne pouze něco papouškovat. Na tomto mém postoji se nic nezměnilo.
Ovšem k problematice samotné existence ORM jsem názor změnil. Jak jsem již napsal, prostě jsem se mýlil a ve finále jedno ORM sám vytvořil.
Proč? V první řadě nerad dělám dokola stále totéž. To byl jeden ze dvou hlavních důvodů. Jednoho dne už mě to prostře přestalo bavit bavit dělat po staru, ale zároveň jsem různá řešení na půli cesty neshledal jako dostatečná.
Tím se dostáváme k druhému důvodu. Ten druhý důvod byl fakt, že jsem v praxi opakovaně narazil na nutnost tutéž aplikaci provozovat na různých typech relační databáze bez nutnosti celou aplikace / projekt / produkt revidovat a “překopávat”.
Chtěl jsem ORM, které mi pomůže s otravnou a stále se opakující rutinou, ale zároveň mi ponechá kontrolu nad tím co se děje a bude co nejpřímočařejší, pokud možno jednoduché a malé. (No i když … Je to největší část PHP Jetu – má to 1,1MB )
A tak v PHP Jet vzniklo ORM zvané DataModel a později i příslušný nástroj, ze kterého se stalo Jet Studio, které již znáte z videí. A právě ORM DataModel dnes začnu představovat.
Už ani nevím kolik je to let, co se velice intenzivně mluvilo a psalo o NoSQL databázích. Internet byl plný CautchDB, MongoDB a tak dále. Pomalu by se zdálo, že relačním databázím je konec (s nadsázkou). Pochopitelně to tenkrát byl jen další z mnoha „letních IT hitů“ a vše postupně vrátilo do starých dobrých evolučních kolejí. Relační databáze jsou prostě relační databáze. Ale myšlenka dokumentových databází není vůbec špatná. Naopak. Samotné NoSQL databáze mají své místo a využití. Ale jak říkám: na matku příslušný klíč a ne kombinačky. Ale mě nejvíc zaujal ten pohled na data jako na dokument.
V aplikacích je naprosto normální, že entita se skládá z řady tříd, které jsou hierarchicky uspořádané a provázané. Z pohledu relační databáze je to tedy vlastně několik databázových tabulek. Ale na celou takto poskládanou entitu se dá nahlížet jako na jeden celek – tedy jako na onen dokument v přeneseném smyslu slova.
Jako jednoduchý příklad použijme entitu Article – článek/text z ukázkové aplikace PHP Jet. Tato část malé ukázky praktického použití Jet rovnou počítá s tím, že obsah webu (či jiného druhu online aplikace) je vícejazyčný.
A například v administraci prostě potřebuji “natáhnout” celý článek a pracovat s tím jako by to byl jeden strukturovaný dokument a neřešit nějaké tabulky. Třeba takto:
$article = Content_Article::get( $id ); if($article) { $article->delete(); }
To vše bez ohledu na to zda je daný článek v jedné nebo deseti databázových tabulkách.
A toto je jednoduchý prostý článek, pouze vícejazyčný. Třeba taková definice produktu v e-shopu se reálně skládá například z deseti tabulek.
Z těch tabulek je je nutné data skládat do instancí tříd, nebo naopak instance třídy ukládat do tabulek s tím, že bude plně automatizováno propojení dat (provázanost ID záznamů).
Potřebuji něco, co za mě bude řešit otravnou a stále se opakující rutinu při čtení, ukládání či mazání dat.
Potřebuji aby například načítání bylo velice rychlé. To v praxi znamená, že počet dotazů se bude vždy rovnat počtu tříd ze kterých je entita poskládána a nebude odvislý od počtu záznamů. Prostě potřebuji pohodlnou práci jako by to byl jeden dokument. To je pro řadu situací moc fajn.
Ovšem potřebuji řešit i situace, kdy si z databáze potřebuji vytáhnout pouze část dat. Rovněž potřebuji řešit situace, kdy si potřebuji vytáhnout například velké množství dat v surové podobě (bez převodu na instance tříd). V praxi rovněž potřebuji řešit i úpravy velkého množství záznamů opět na úrovni surových dat. Schválně zkuste dělat přepočty cen (například) stovek tisíc záznamů s tím, že budu tahat celé instance zboží (například) … V praxi je prostě nutné volit správný přístup na řešení daného problému – to je univerzální pravidlo.
Potřebuji tedy něco, co mi umožní dělat vlastně cokoliv ať je to práce s entitou jako s (pomyslným) dokumentem, či práce s daty a při tom všem mě to zbaví rutinní práce a odstíní mě to od konkrétního typu relační databáze tak aby aplikace byla přenositelná (Jet nyní podporuje MariaDB/MySQL, SQLite, PostgreSQL a MS SQL, podpora Oracle bude také).
Potřebuji, aby to něco bylo rychlé a celkově efektivní a zároveň ne svazující. A tak jsem postupně stvořil (a v praxi již odzkoušel) to co teď konečně začnu popisovat. Hurá na to.
Ve videích a ukázkách jste již mnohokrát měli možnost vidět jak to funguje, jak se definice entity “naklikává” v k tomu určeném nástroji Jet Studio a tak dále. Celkově jsem toto předváděl několikrát na praktických ukázkách. Tedy teď se zaměřím hlavně na speciality a věci o kterých jsem se dříve zmínil pouze okrajově či vůbec.
Každá třída reprezentující entitu musí dědit od třídy Jet\DataModel.
Každá třída reprezentující subentitu – dílčí část entity – musí dědit od třídy Jet\DataModel_Related_1to1, nebo Jet\DataModel_Related_1toN. Podle toho v jaké relaci je subentita vůči hlavní entitě či nadřazené subentitě.
To jsou základní podmínky definice entit.
Dále každá třída reprezentující entity či její část musí mít definované atributy. Používá se výhradně systém atributů z PHP 8, které si Jet trochu doplňuje o možnost aplikace dědičnosti a přetěžování.
Příklad definice hlavní třídy entity:
#[DataModel_Definition( name: 'article', database_table_name: 'articles', id_controller_class: DataModel_IDController_UniqueString::class, id_controller_options: [ 'id_property_name' => 'id' ] )] class Content_Article extends DataModel { ..... }
Zajímavé jsou jednotlivé hodnoty:
A teď příklad definice subentity, tedy zde lokalizovaných dat článku:
#[DataModel_Definition( name: 'article_localized', database_table_name: 'articles_localized', id_controller_class: DataModel_IDController_Passive::class, parent_model_class: Content_Article::class )] class Content_Article_Localized extends DataModel_Related_1toN { ..... }
Za povšimnutí stojí:
A pochopitelně mohou existovat i subentity subentit vlastně do (teoreticky) libovolné úrovně.
Ale vždy platí, že subentita musí být navázána na nadřazenou entitu a vše (v libovolné úrovni) musí být navázáno na hlavní entitu.
Tedy hlavní entita je úroveň 0. Její subentity jsou úroveň 1 a musí mít vazbu na úroveň 0. Úroveň 2 již musí mít vazbu na úroveň 1, ale i na úroveň 0. Úroveň 3 pak má vazbu na úroveň 2, ale opět i na 0 – na hlavní entitu. Prostě a jednoduše všechny subentity libovolné úrovně musí vědět do jaké hlavní entity náleží.
Jak se to dělá si ukážeme v rámci definice vlastností.
Definice vlastností a jejich mapování na databázi má obodný princip, tedy opět jsou použity atributy z PHP 8. Ukažme si to na příkladu třídy Content_Article:
#[DataModel_Definition( type: DataModel::TYPE_ID, is_id: true )] protected string $id = ''; #[DataModel_Definition( type: DataModel::TYPE_DATE_TIME, )] protected ?Data_DateTime $date_time = null; /** * @var Content_Article_Localized[] */ #[DataModel_Definition( type: DataModel::TYPE_DATA_MODEL, data_model_class: Content_Article_Localized::class )] protected array $localized = [];
Atributů definic existuje samozřejmě více. Viz dokumentace.
Ale pro teď si ukažme to nejzajímavější:
Koukněme se ještě na definici vlastností subentity, tedy zde bude příkladem třída Content_Article_Localized:
#[DataModel_Definition( type: DataModel::TYPE_ID, is_id: true, related_to: 'main.id', do_not_export: true )] protected string|null $article_id = ''; #[DataModel_Definition( type: DataModel::TYPE_LOCALE, is_id: true, do_not_export: true )] protected Locale|null $locale; #[DataModel_Definition( type: DataModel::TYPE_STRING, max_len: 255, is_key: true )] protected string $URI_fragment = ''; #[DataModel_Definition( type: DataModel::TYPE_STRING, max_len: 100, )] protected string $title = ''; #[DataModel_Definition( type: DataModel::TYPE_STRING, max_len: 65536, )] protected string $annotation = ''; #[DataModel_Definition( type: DataModel::TYPE_STRING, max_len: 655360, )] protected string $text = '';
Jistě jste si všimli, že řetězce již mají definovanou maximální délku (max_len), ale to není tak zajímavé.
Zajímavé je toto:
Tolik stručně k definicím. Ostatní definice viz dokumentace.
A teď konečně jak je to s těmi ID a co jsou to ID kontrolery.
Ve světa PHP je hodně lidí navyknutých na MySQL / MariaDB a její auto-increment. To je moc fajn způsob jak identifikovat záznamy tabulky a pro řadu věcí je to dostačující a nejlepší řešení.
Ovšem není to řešení zdaleka na vše.
V praxi můžete mít systém (zažil jsem opakovaně), kde je více databází mezi kterými je nutné přenášet dílčí záznamy entit (třeba pouze jeden jednotlivý článek či jiný prvek obsahu z mnoha) mezi různými databázemi a je nutné minimalizovat možnost kolize záznamů. A tam se číselné sekvence vyloženě nehodí. Je lepší například kód tvořený časovým razítkem a textovým řetězcem s dostatečnou entropií. Prostě tak aby bylo možné data konkrétní entity (fakticky záznamy z více tabulek) přenést z databáze A do databáze B s velmi nízkou pravděpodobností kolize identifikátorů záznamu.
To byl jeden příklad. Druhý příklad je situace, kdy je identifikátor konkrétního záznamu (z pohledu databáze primární klíč) tvořen ne jednou vlastností (z pohledu databáze sloupečkem), ale více vlastnostmi / sloupečky. A ještě ke všemu nastavení hodnot těchto záznamů podléhá čistě aplikační logice a ne databázi. Tak mám například vyřešené verzování obsahu v rámci CMS použitého pro web php-jet.net.
Zkrátka a jednoduše: Ne, auto-increment id v reálné praxi opravdu neřeší vždy vše.
Vraťme se nyní k entitě Article / článek jako celku. To jak je tato ukázka v ukázkové aplikaci PHP Jet koncipována není náhoda, ale čistý záměr demonstrace celého principu.
Připomeňme si, že třída Content_Article má vlastnost $id, ale to není int, je to string.
Třída Třída Content_Article_Localized má pak dvě vlastnosti označené jako ID (is_id:true) a to $article_id a $locale. První jmenovaná vlastnost je řetězec a má definovanou vazbu na vlastnost hlavní entity. Druhá jmenovaná vlastnost $locale je kód lokalizace (v reálu uloženo jako kód lokalizace dle ISO). Nemá definovanou žádnou vazbu. Obě vlastnosti dohromady tvoří identifikátor konkrétního záznamu – primární klíč.
Definici tedy máme. Ale co se musí stát při uložení nového záznamu, tedy při založení nového článku?
ID článku v tomto ukázkovém případě není číselná sekvence přidělená databází. Něco tedy musí vygenerovat onen náhodný řetězec který bude představovat ID článku. To se musí stát ještě před uložením článku.
Vidíte někde ve třídě Content_Article něco takového? Nehledejte, není to tam.
O vygenerování onoho řetězce se postará to čemu říkám ID kontroler. Ta věc na kterou jsme narazili již v kapitole o definicích.
Ovšem ono vygenerování řetězce samo o sobě nestačí. Však i subentita Content_Article_Localized bude nutně potřebovat vědět jaké je ID článku a to ještě před tím než se začne ukládat do databáze.
Opět – tuto logiku v aplikačních třídách nehledejte. I o to se postará ORM v PHP Jet. Vygenerovaná hodnota ID je předána tam kam předána má být. O otravnou rutinu je postaráno.
Dobře, ale co když chcete použít staré dobré číselné sekvence (které jsou v MySQL/MariaDB pořešené dle mého názoru nejelegantněji, ale jsou k dispozici i v jiných relačních DB). Samozřejmě. To není problém. V rámci ukázkové aplikace jsou takto identifikováni například uživatelé. Ukažme si pro názornost například ukázkovou implementaci návštěvníka:
#[DataModel_Definition( name: 'user', database_table_name: 'users_visitors', id_controller_class: DataModel_IDController_AutoIncrement::class, id_controller_options: ['id_property_name' => 'id'] )] class Auth_Visitor_User extends DataModel implements Auth_User_Interface { #[DataModel_Definition( type: DataModel::TYPE_ID_AUTOINCREMENT, is_id: true, )] protected int $id = 0; }
Ovšem v případě ukládání nového záznamu je zde úplně jiná posloupnost.
ID negeneruje nic v aplikaci, ale přiděluje jej databázový systém. Je tedy nutné nejprve hlavní záznam uložit, pak si od databáze převzít přidělené sekvenční číslo. A pozor! Samozřejmě je nutné toto číslo nastavit vazbám subentit ještě předtím, než dojde k jejich ukládání.
Opět je potřeba určitá logika a tentokrát obrácená: nejprve uložit, pak máme ID a to ID opět předat subentitám. A opět tuto logiku v aplikačních třídách nenajdete.
Je to totiž strašná a neskutečná otrava. Tedy zde je prostor pro ORM aby našince oné otravy zbavilo.
Ovšem důležité je, že ORM neříká jak mají záznamy být identifikovány. Ne. ORM pouze říká, že potřebuje znát logiku vazeb. To je v pořádku. Ostatně to je nutné znát i z hlediska přehlednosti aplikace. A v neposlední řadě ta povinnost subentity navázat na hlavní entitu (na onu zmiňovanou úroveň 0) napomáhá optimalizaci aplikace – prostě je možné data nahrávat jednoduše a rychle.
Nic víc ovšem ORM nediktuje, ale naopak poslušně pomáhá.
PHP Jet nabízí systém ID kontrolerů. ID kontrolery jsou třídy, které řídí onu logiku přidělování ID. Tedy stačí pouze definovat jaké vlastnosti a jakého typu budou tvořit ID a jak s ID chceme zacházet. O vše ostatní (o otravno rutinu) se již ORM postará.
Ano, k tomu jsou tyto definice nad třídami, které jste již v článku viděli:
#[DataModel_Definition( id_controller_class: DataModel_IDController_AutoIncrement::class, id_controller_options: ['id_property_name' => 'id'] )]
či
#[DataModel_Definition( id_controller_class: DataModel_IDController_UniqueString::class, id_controller_options: ['id_property_name' => 'id'] )]
nebo
#[DataModel_Definition( id_controller_class: DataModel_IDController_Passive::class, )]
Prostě vývojář určí: toto jsou ID, takto jsou provázána v rámci celé entity a takto se s nimi bude zacházet. To je vše.
A ještě jedna dobrá zpráva. V rámci PHP Jet jsou předchystané tři ID kontrolery:
Ale framework PHP Jet má otevřenou architekturu. Tedy je velice jednoduché udělat si vlastní ID kontroler, který bude dělat přesně to co chcete a jak chcete vy.
Dnes jsem těm co mě znají osobně a čtou to vysvětlil proč jsem začal mít rád ORM – zcela jsem změnil názor.
Ale především jsem ukázal nejen definice entit datového modelu, ale hlavně definice provázání entit a jejich subentit, které dohromady tvoří jednu entitu.
A také jsem naznačil co jsou to ID kontrolery, jaká je jejich návaznost na sekvenci ukládání záznamů a jakou to má souvislost s provázaností subentit.
Jak jsem i zde naznačil, tak práce s databází je velice často nejbolavějším místem online aplikací z hlediska jejich výkonnosti (a také bezpečnosti). Proto se příště zaměřím jednak na princip tvorby dotazů a zejména pak na celou řadu způsobů jak pomocí tohto ORM nahrávat data z databáze – jak nahrávat co nejrychleji, pokud možno jen to co doopravdy potřebuji (například částečné nahrávání instancí a tak dále).
A pochopitelně si také ukážeme vnější relace. Chybět nebude ani práce se surovými daty a tak dále. Tak příště.
Na Jetu se intenzivně pracuje (proto teď nebyl prostor na článek či video každý týden), vyšla další verze 2023.6. Co je nového?
Systém pro snadný vývoj různých přehledů dat a seznamů.
Jedná se o refaktorovaný starý Data_Listing s mnohem lepší architekturou a více funkcemi (převzaté z chystané platformy Jet Shop):
Starý Data_Listing zůstává kvůli zpětné kompatibilitě plně zachován.
Ta slouží k přihlášení uživatele na základě jeho instance.
Auth kontrolery mohou ale nemusí tuto funkci implementovat. Je to vhodné například po snadné přihlášení uživatele po registraci.
Šablona modulu BasicAdmin přejmenována na BasicCRUD pro lepší srozumitelnost.
Integrován nový subsystém DataListing
Nový subsystém DataListing plně integrován
Moduly EventViewer.*: Přidán export CSV (příklad exportu dat pomocí DataListing)
Moduly ManageAccess.*.Users: Přidána hromadná operace zablokování uživatele / odblokování uživatele (příklad hromadné operace DataListing).
Přidán příklad registrace návštěvníků (nový modul a jeho integrace do ukázkového webu).
… a zde vám jej s radostí představuji:
A to už je pro dnešek vše. Za týden či za dva se zase ozvu.
Přeji vám krásné a pohodové letní dny.
Skoda ze jsem se tu vlastne nic nedozvedel. Proc autor zvolil tuto cestu a nesahnul po jiz existujicich reseni (v php treba Doctrine). Predpokladam ze mu v necem nevyhovovaly tzn v cem se od nich lisi.
Jake tvori dotazy, jak resi N+1 problem? Jak resi batch processing, zamykani...
Jak efektivni jsou updaty, updatuje se cely radek nebo se do DB posilaji jen zmeny
Toto jsou veci co by me opravdu zajimaly snad se k tomuto autor vrati. Zatim sem se jen dozvedel jak anotovat model
Přečteno 20 710×
Přečteno 18 551×
Přečteno 17 777×
Přečteno 17 521×
Přečteno 16 219×