Hlavní navigace

Nový ORM framework pro Kotlin?

26. 1. 2024 19:45 (aktualizováno) Pavel Ponec

Pokud máte averzi na vznik nových frameworků, tohle raději dál ani nečtěte. Ostatní laskavé čtenáře upozorňuji, že zde chystám představit především návrh API pro modelování databázových dotazů v deklarativním stylu se silnou typovou kontrolou jazyka Kotlin. Implementovány jsou jen některé třídy kolem entit, napojení na databázi zatím chybí. V projektu jsem se pokusil zhodnotit své dosavadní zkušenosti a vize. Všechny nápady představené v tomto článku však nejsou zcela nové. Některé jsem čerpal z frameworku Ujorm, pojetí entity bylo inspirováno frameworkem Ktorm. Kód ale nový je. Prototyp je výsledkem mozaiky, která se skládala zvolna a nyní doznala podoby, kterou snad stojí za to prezentovat.

Pokud vás úvod neodradil, přeskočím obecné povídání o ORM a dovolím si přejít k ukázkám kódu. Demonstrační příklady používají dvě tabulky relační databáze. Jedná se o vztah zaměstnance a oddělení (blíže neurčené organizace), kde každý zaměstnanec může (ale nemusí) mít svého nadřízeného. Obě tabulky jsou popsány entitami z následujícího class diagramu:

Kotlin

Předpokládejme, že chceme vytvořit report obsahující unikátní číslo zaměstnance, jeho jméno, název oddělení a jméno nadřízeného (pokud existuje). Zajímají nás přitom jen oddělení s kladným identifikátorem a s názvem oddělení začínajícím písmenem „D“. Report chceme řadit podle názvu oddělení (sestupně) a pak podle jména zaměstnance (vzestupně). Jak by mohl vypadat dotaz (SELECT) postavený na uvedených entitách v prezentovaném API?

Kotlin

Využití DSL v databázovém dotazu dnes už asi nikoho nepřekvapí. Za pozornost však stojí řetězení modelu atributů entity (dále property-descriptorů). Jejich spojením vzniká nový kompozitní property-descriptor, který implementuje stejné rozhraní jako jeho atomické části. Filtr dotazu (WHERE) je popsán objektem sestaveným z elementárních podmínek do jediného binárního stromu.  Kompozitní property-descriptory poskytují informace, ze kterých se odvozují také relace SQL dotazu mezi databázovými tabulkami. Tímto přístupem lze pokrýt snad většinu běžných SQL dotazů včetně rekurzivních. Jistě ale ne všechny. Pro zbývající je třeba použít náhradní řešení. Hrubý návrh je v testech projektu.

Zaměřme se dále na entitu zaměstnance:

Kotlin

Entitu tvoří interface bez dalších závislostí. Výhodou je, že interface se obejde (v ORM) bez binární modifikace kódu, pro získání objektu však bude třeba použít tovární metodu, která dodá implementaci. Alternativou by mohlo být rozšíření nějaké obecné třídy (poskytnuté frameworkem), což mi přišlo zbytečně invazivní. Tovární metodu pro tvorbu nových objektů poskytuje párový objekt metamodelu.

Každá entita zde potřebuje metamodel, který obsahuje informace o jejím typu, atributech a který poskytuje nějaké služby. Atributy metamodelu jsou právě ty výše zmíněné property-descriptory párových entit. Všimněme si, že pro vytvoření property-descriptorů se používá stejná metoda property() – přitom není podstatné, zda se jedná o popis relace (atribut na jinou entitu).  Výjimku najdeme jen v případech, kde typ atribut (entity) akceptuje hodnotu NULL. Pozitivní zprávou je, že chybné použití (toho kratšího názvu metody) ohlásí kompilátor v době překladu. Příklad metamodelu entity zaměstnance přikládám:

Kotlin

Specifické vlastnosti sloupců (databázových tabulek) budou deklarované anotacemi na entitách, třídy metamodelu tak mohou být jednou generované – podle své entity. Data entity se ukládají (interně) v poli objektů. Výhodou jsou menší paměťové nároky – ve srovnání s implementací postavené na třídě HashMap.

Další ukázka demonstruje tvorbu nových objektů a jejich ukládání do databáze (INSERT).

Insert

Třída MyDatabase (která metamodel poskytuje) je tady jedináček (návrhový vzor singleton), obecně to však může být jakýkoliv náš objekt poskytovaný aplikačním kontextem (například). Pokud bychom chtěli využít nějakou službu (třeba klonování objektu entity), stačí rozšířit (toho poskytovatele) pomocí třídy AbstractEntityProvider a využít jeho prostředky. Ukázku doporučeného postupu registrace (tříd metamodelu) společně s dalšími příklady lze najít v testech projektu.

Podmínky

Podmínka (nebo také kritérium) je objekt, se kterým jsme se setkali při prezentaci SELECT příkazu. Využít podmínku však lze i samostatně, například pro validaci hodnot entity či filtrování kolekcí. Pokud by knihovna poskytla podporu pro její serializaci do textového formátu JSON (a zpět), okruh využití by se ještě rozšířil. Pro sestavování následujících podmínek vyjdeme z metamodelu, který už máme uložený v proměnné employees.

Pokud máme objekt zaměstnance v proměnné employee, kritérium zaměstnance lze otestovat následujícím kódem:

Na prvním řádku zaměstnanec kritérium splnil, na druhém řádku nikoli. V případě potřeby (při ladění či logování) lze obsah podmínek vizualizovat do textu, ukázky přikládám:

Další zajímavosti

Property-descriptor nemusí sloužit jen pro modelování SQL dotazů, ale může se účastnit i čtení a zápisu hodnot do objektu. Nejjednodušší cestou je rozšířit interface entity  o rozhraní PropertyAccessor. Pokud máme objekt zaměstnance, pro čtení lze použít kód:

Kotlin

Explicitní deklaraci datových typů proměnných uvádím jen za účelem prezentace, prakticky jsou však nadbytečné a lze je odstranit. Podobně vypadá zápis proměnných do objektu:

setters

Všimněme si, prosím, že čtení a zápis hodnot probíhá bez přetypování také pro NULLABLE hodnoty. Další zajímavou vlastností je podpora čtení a zápisu hodnot pomocí kompozitních property-descriptorů. Jen pro jistotu ujišťuji, že pro běžné použití objektu bude pohodlnější využít standardní API entity – deklarované rozhraním.

Výše uvedená ukázka kopírovala své atributy do proměnných a zpět. Pokud bychom chtěli nějaký objekt naklonovat, lze použít následující konstrukci (mělká kopie):

Kotlin

Během kopírování dat se nevolají žádné metody reflexe, což umožňuje použitá architektura tříd. Další funkční ukázky využití lze najít v testech projektu na GitHub.

Proč

Proč tento projekt vznikl? Na počátku byla snaha naučit se základy jazyka Kotlin. Postupně vznikla halda neuspořádaných poznámek ve formě zdrojového kódu a přitom jsem objevil také prostředky jazyka, které by vedly také ke zjednodušení API frameworku Ujorm. Nalezení hotových ORM knihoven v Kotlinu mě potěšilo. Ze dvou populárních jsem však nedokázal vybrat jednu, která by mi sedla více. Zajímavé vlastnosti jedné jsem postrádal u druhé a opačně. Někde mi přišlo použití API málo intuitivní, jinde jsem narazil na komplikace s rekurzí databázových tabulek. Společným handicapem byla (na můj vkus) zvýšená chybovost při ručním sestavování  metamodelu entity. Tady lze jistě oponovat, že entity lze generovat z databázových tabulek. Nakonec jsem své původní poznámky uspořádal do projektu, kód vyčistil a doplnil tento článek. To je snad vše podstatné.

Závěr

Líbila by se mi integrace s jádrem hotového ORM frameworku, zřejmě nejrychlejší by byla integrace s Ujorm. Jsem si však vědom rizik spojených s jakoukoli integrací a neumím ani vyloučit, že tento projekt žádné reálné uplatnění v oblasti ORM nakonec nenajde. Prototyp je volně k dispozici pod licencí Apache Commons 2. Za konstruktivní komentáře děkuji.

Internetové odkazy

Sdílet