Udělej tu nejjednodušší věc, která by mohla fungovat. — Kent Beck, tvůrce extrémního programování a průkopník vývoje řízeného testy.
Rád bych vám dnes představil finální verzi knihovny Ujorm3 s nově napsaným ORM modulem pro práci s objekty typu JavaBean a Record v kontextu relačních databází. Cílem tohoto modulu bylo transparentní řešení bez dalších závislostí, s podporou typově bezpečného sestavení SQL příkazů. Pro rychlou manipulaci s daty doménových objektů si knihovna kompiluje za běhu vlastní bajtový kód, čímž dosahuje výkonu srovnatelného s ručně psaným kódem. Jádro knihovny intenzivně využívá návrhový vzor Typed Key Pattern. Z původní verze knihovny Ujorm byla převzata zejména dvě rozhraní — Ujo a Key. Obě rozhraní aplikovala zmíněný vzor už v roce 2007, tedy ještě dříve, než tento vzor dostal své jméno.
Návrhový vzor Typed Key Pattern představuje techniku pro čtení a zápis hodnot doménového objektu prostřednictvím API jeho klíčů, která nahrazuje tradiční gettery a settery interní mapou vlastností. Klíče jsou typově parametrizované deskriptory nesoucí informaci o datovém typu (případně výchozí hodnotě) a díky generikám zajišťují typovou bezpečnost bez přetypování; zároveň umožňují provádět nad atributy hromadné operace bez reflexe. Z hlediska interní reprezentace odpovídá tento přístup konceptu Typesafe Heterogeneous Container, který byl poprvé systematicky publikován Joshuou Blochem v květnu 2008 ve druhém vydání knihy Effective Java v návaznosti na zavedení generik do jazyka Java.
Dovolím si připomenout některé základní informace z minulého blogu: výchozí mapování atributů entit na databázové sloupce se popisuje anotacemi JPA z balíčku Jakarta. Pro mapování atributů na sloupce se používá anotace @Column, pro mapování výčtového typu se používá @Enumerated. Název tabulky se deklaruje anotací @Table. Protože knihovna nepoužívá žádné binárně modifikované třídy, je pro částečné aktualizace sloupců v příkazu UPDATE třeba dodat seznam názvů modifikovaných atributů (například výčtem String[]) nebo původní objekt, ze kterého si knihovna tento seznam vytvoří sama. Jednu databázovou tabulku může obsluhovat několik tříd — typicky jde o podmnožinu entity.
Jedním z cílů nové knihovny bylo nalézt optimální poměr mezi užitnou hodnotou a minimalistickým zdrojovým kódem. Autor věří, že právě takový přístup má pozitivní vliv na minimalizaci chyb a významně prodlužuje životnost knihovny. Z takového přístupu však plynou i jistá omezení. Například: Knihovna nepodporuje lazy-loading a při mapování relací podporuje pouze vazbu typu M:1.
ORM modul knihovny Ujorm3 nabízí pro databázové příkazy sadu nástrojů s různými úrovněmi abstrakce. Vyberte si řešení, které nejlépe vyhovuje konkrétnímu požadavku:
EntityManager — nejrychlejší způsob, jak zvládat CRUD operace s jednou tabulkou pomocí primárního klíče. Pro takové elementární případy si třída generuje SQL příkaz sama.SelectQuery — nástroj primárně určený pro vyhledávání dat včetně relací. Podporuje filtrování dat pomocí typově bezpečných objektů Criterion, které lze řadit do binárního stromu (pomocí operátorů AND a OR). Alternativou je použití metody bind(). Pro specifické případy lze ručně doplnit začátek a konec generovaného SQL příkazu.SqlQuery — nízkoúrovňová univerzální třída pro obecné požadavky. Umožňuje psaní nativního SQL při zachování bezpečnosti pomocí metod bind(), label() a column(). Pomocí generického mapovače dokáže knihovna automaticky naplnit i relační objekty. Poslední dvě třídy mají společného předka, který může sloužit jako základ pro případné další implementace.Ukažme si jednoduché příklady použití a začněme tím nízkoúrovňovým přístupem. Následující metoda vloží do databázové tabulky město a vrátí identifikátor vloženého řádku.
public Long insertCity(String name, String country) {
return SqlQuery.run(connection(), query -> query
.sql("""
INSERT INTO city
( name, country_code) VALUES
(:name, :countryCode )
""")
.bind("name", name)
.bind("countryCode", country)
.executeInsert(rs -> rs.getLong(1))
.findFirst().orElseThrow());
}
Za pozornost stojí použití pojmenovaných placeholderů ( :name, :countryCode), automatické uzavírání zdrojů a fluent API bez kontrolovaných výjimek. Další ukázka vyhledá zaměstnance a načte jejich data včetně názvu města. Třída ResultSetMapper podporuje vícevláknový přístup.
static final ResultSetMapper EMPLOYEE_MAPPER =
ResultSetMapper.of(Employee.class);
public List<Employee> findEmployees(Connection connection, Long minId) {
return SqlQuery.run(connection, query -> query
.sql("""
SELECT e.id AS ${e.id}
, e.name AS ${e.name}
, c.name AS ${c.name}
FROM employee e
JOIN city c ON c.id = e.city_id
WHERE e.id >= :id
""")
.label("e.id" , MetaEmployee.id)
.label("e.name", MetaEmployee.name)
.label("c.name", MetaEmployee.city, MetaCity.name) // Key Path mapping
.bind("id", minId)
.toStream(EMPLOYEE_MAPPER.mapper()) // Generic mapping including relations
.toList());
}
Třída MetaEmployee je automaticky generovaný metamodel, který vzniká pomocí volitelného APT pluginu ujorm-meta-processor. Mapování však lze zapsat i přímo do příkazu SQL SELECT na pozici jmenovky sloupce ve formátu tečkou oddělených názvů vlastností (properties, například boss.name). Je však třeba jej obalit uvozovkami (případně jinými znaky podle typu databáze). Pokud se název DB sloupce shoduje s atributem, je explicitní mapování zbytečné. Například:
SELECT e.id, e.name, c.name AS "city.name" FROM ...
Poslední ukázka provede podobný SELECT třídou SelectQuery. Metoda columns(true) tady načte všechny sloupce tabulky, včetně cizích klíčů. Metoda column(...) přidá do mapování další sloupec, umí i násobné relace. Typ relace je určen vlastností nullable z JPA anotace: povinný atribut objektu (označený nullable=false) generuje relaci typu INNER JOIN, jinak generuje LEFT JOIN. Začátek SQL příkazu lze (volitelně) změnit metodou sql(), konec lze (volitelně) doplnit metodou tail().
final EntityContext CTX = EntityContext.ofDefault();
final EntityManager<Employee, Long> EMPLOYEE_EM = CTX.entityManager(Employee.class);
List<Employee> select(Connection connection) {
return SelectQuery.run(connection, EMPLOYEE_EM, query -> query
.sql("SELECT") // Optional
.columns(true)
.column(MetaEmployee.city, MetaCity.name) // INNER JOIN
.column(MetaEmployee.boss, MetaEmployee.name) // LEFT JOIN
.where(MetaEmployee.id.whereGe(1L).and(MetaCity.id.whereGe(1L)))
.tail("ORDER BY", MetaEmployee.id)
.toList()
);
}
Kód projektu Ujorm3 je detailně pokrytý jUnit testy. Integrační testy se pouští Bash skriptem (v adresáři bin) proti dockerizovaným databázím: PostgreSQL, MySQL, MariaDB, OracleFree a MSSQL Server. Podrobnější informace o knihovně Ujorm3 jsou uvedeny na domovské stránce projektu — včetně dalších příkladů.
Referenční použití Ujorm3 najdete v projektu PetStore, který využívá pro dependency injection lehkou knihovnu Avaje. Z projektu PetStore pak vznikl prototyp webové aplikace TopMovies pro filmové diváky — tato aplikace doporučuje filmy uživateli podle shody jeho hodnocení s virtuální skupinou s podobnými preferencemi. Data se ukládají do databáze H2, volitelně do PostgreSQL. Projekt sice není veřejně dostupný, nicméně prototyp si můžete volně vyzkoušet na uvedeném odkazu (počet fiktivních uživatelů je omezen).
Přikládám odkaz na jiné zpracování stejného tématu, tentokrát v angličtině:
https://dev.to/pponec/native-sql-in-java-without-jdbc-boilerplate-meet-ujorm3-3ab2
Ja ted pouzivam JDBi v komercnim projektu tak jsem rychle porovnal pro a proti s Ujorm. Nasel jsem jedinou vyhodu Ujorm v deklaraci schema pomoci entit.
Pokud tohle nepotrebujute, a schema definuje nekdo jiny, zvolil bych JDBi ktery ma stejne vyhody a navic muzete psat DAOs pomoci anotovanych interface. (neco jako Feign client)
Myslím, že těch rozdílů je víc než jedna. Ujorm3 generuje při kompilaci (APT procesor) typované Meta třídy s Key<Employee, City> deskriptory – chybný název sloupce nebo špatný typ parametru se odhalí ihned při kompilaci, nikoli za běhu v produkci. JDBI SQL Object API přináší určitou typovou bezpečnost přes anotované rozhraní, ale samotné názvy sloupců a cesty k atributům zůstávají řetězce.
Z toho plyne i druhý rozdíl: SelectQuery generuje FROM a JOIN klauzule automaticky podle cest v metamodelu ( MetaEmployee.city, MetaCity.name), takže JOIN není potřeba psát ručně. Metoda columns() pak vygeneruje výčet všech sloupců primární tabulky přímo z metamodelu – v JDBI lze sice použít select("*"), ale jde o prostý řetězec bez typové vazby. Třetí věc je typově bezpečný Criterion pro WHERE klauzule – podmínky se skládají programově jako binární strom objektů, ne jako SQL fragmenty. Čtvrtý bod: partial UPDATE – jde buď explicitně předat sadu Key objektů označující sloupce k aktualizaci, nebo poslat původní objekt a Ujorm3 si seznam změněných sloupců odvodí porovnáním sám; v JDBI obojí vyžaduje ruční SQL. Za páté, Java Records jsou podporovány jako first-class citizen včetně automatického mapování M:1 relací. Za šesté, na PostgreSQL jsou výsledky smíšené: Ujorm3 vede v operacích UPDATE a paměťové efektivitě, zatímco JDBI je rychlejší například u batch insertu nebo čtení entit; na H2 Ujorm3 konzistentně vyhrává prakticky ve všech metrikách. A za sedmé, Ujorm3 nemá žádné externí závislosti a jeho JAR má velikost 0,27 MB, což je přibližně pětina velikosti JDBI (1,34 MB); obě hodnoty jsou měřeny bez JDBC driveru.
JDBI samozřejmě vyhrává šíří ekosystému – pluginy pro Guava, Spring, Vavr, lepší pokrytí stored procedures a větší komunita. Anotované DAO rozhraní ve stylu Feign jsou pohodlná věc. Ale „stejné výhody" to není – záleží hodně na tom, jak moc vadí, když se chyba v dotazu projeví až za běhu.
> Jak se zachová mapování, když volitelná relace (optional) nevrátí žádná data?
Při mapování platí obecné pravidlo: pokud žádný sloupec z připojené tabulky nenese nenullovou hodnotu, hostitelský objekt se nevytvoří — příslušné pole zůstane null. Toto chování je nyní zdokumentováno v JavaDoc třídy ResultSetMapper v sekci Null-object Rule.
Načítání přes řetězec vztahů mandatory optional se tedy chová přirozeně: povinná strana se vždy vyřeší, zatímco volitelná strana vrátí null, pokud neexistuje odpovídající záznam — tedy když LEFT JOIN vrátí pro danou tabulku samé nullové hodnoty.
Pokud je třeba při mapování vytvářet relační objekt bez ohledu na null hodnoty, je třeba přidat identifikátor.
K dispozici je nová verze 3.0.3. Ve třídě SelectQuery je opraveno generování příkazu SQL INSERT při použití třetí řetězené relace v kombinaci s LEFT JOIN. Opravena byla také implementace metody pro negaci podmínek typu Criterion. Nová verze výrazně zrychlila operace typu INSERT v případech, kdy není třeba získávat generovaný klíč ID, což má pozitivní vliv zejména na výkonnostních testech PostgreSQL. Knihovna nyní automaticky načítá konfiguraci logování ze souboru ujorm-config.properties, díky čemuž se SQL dotazy v JUnit testech vypisují do terminálu bez jakéhokoliv dodatečného nastavení.
Postřehy ze světa open-source.
Přečteno 40 055×
Přečteno 23 469×
Přečteno 19 319×
Přečteno 18 042×
Přečteno 16 765×