ČESKÁ ZEMĚDĚLSKÁ UNIVERZITA FAKULTA PROVOZNĚ EKONOMICKÁ Obor systémové inženýrství a informatika
BAKALÁŘSKÁ PRÁCE Téma: Mapování dat mezi objektovými programovacími jazyky a relačními databázemi
Vypracoval:
Jan Tauchmann
Vedoucí bakalářské práce:
doc. Ing. Vojtěch Merunka Ph.D.
Praha 2007
PROHLÁŠENÍ
Prohlašuji, že jsem bakalářskou práci na téma: Mapování dat mezi objektovými programovacími jazyky a relačními databázemi zpracoval/a samostatně za použití uvedené literatury a po odborných konzultacích s doc. Vojtěchem Merunkou.
V Praze dne 28. 4. 2007
.....................................
PODĚKOVÁNÍ Děkuji tímto panu Doc. Merunkovi za odborné vedení a rady při zpracování bakalářské práce. Zároveň děkuji panu Mgr. Julinkovi a jeho zaměstnancům za ochotu při poskytování potřebných podkladů.
ABSTRAKT Práce se v prvních dvou kapitolách zabývá problematikou proč vlastně potřebujeme objektovou persistenci a srovnává ji s jejími relačními a souborově orientovanými alternativami. Ve 3. kapitole se dozvíme, jaké máme možnosti pokud bychom se rozhodli postavit svoji aplikaci právě na přístupu relační databáze + ORM. Kapitoly z pořadovými čísly 4, 5 a 6 se zabývají implementací ORM a na příkladech nástroje FORIS.ORM, jehož tvůrcem je autor této práce, se dozvíte něco o technikách a algoritmech, které je možné při tvorbě ORM použít. V poslední, sedmé kapitole definitivně přejdeme od teorie k praxi a ukážeme si praktické využití ORM frameworku na informačním systému knihovny.
KLÍČOVÁ SLOVA Persistence, objekt, databáze, programování, modelování, návrh, SQL
Obsah 1
2
3
4
5
6
Cíl práce a metodika ................................................................................................ 7 1.1 O čem je tato práce? ......................................................................................... 7 1.2 Úvod do problematiky ..................................................................................... 7 1.3 Objektová persistence dat ................................................................................. 9 Porovnání souborových, relačních a objektových přístupů k persistenci ................. 11 2.1 Definice datového modelu.............................................................................. 11 2.2 Vyhledávání a čtení dat .................................................................................. 12 2.3 Změny dat ...................................................................................................... 13 2.4 Úpravy existujícího datového modelu ............................................................ 14 2.5 Transakce....................................................................................................... 16 Možnosti ORM v současných programovacích jazycích ........................................ 17 3.1 Požadavky na databázi a programovací jazyk ................................................. 17 3.2 Umístění definice modelu .............................................................................. 18 3.2.1 Definice modelu pomocí DDL příkazů SQL ........................................... 18 3.2.2 Definice modelu pomocí externího souboru............................................ 18 3.2.3 Definice modelu pomocí anotací a atributů ............................................. 20 3.3 Společný předek pro všechny persistentní objekty? ........................................ 21 3.4 Údálostmi řízené programování ..................................................................... 23 3.4.1 Triggery ................................................................................................. 23 3.4.2 Validátory .............................................................................................. 23 3.5 ORM aplikační server .................................................................................... 24 Základy ORM mapování........................................................................................ 27 4.1 Definice objektového modelu ......................................................................... 27 4.2 Projekce modelu do relační databáze .............................................................. 28 4.3 Třída Session – páteř každého O-R Mapperu.................................................. 30 4.4 Mapování jednoduché třídy ............................................................................ 31 4.5 Vyhledání záznamu ........................................................................................ 32 4.6 Mapování referencí ........................................................................................ 33 4.7 Mapování kolekcí .......................................................................................... 34 4.8 Object Reader ................................................................................................ 35 Implementace cachování a zpožděného načítání dat ............................................... 38 5.1 Základy cachování dat v ORM ....................................................................... 38 5.1.1 Cachování primárních klíčů .................................................................... 40 5.1.2 Cachování indexů ................................................................................... 40 5.1.3 Cachování referencí................................................................................ 41 5.2 Zpožděné načítání kolekcí .............................................................................. 42 5.3 Zpožděné načítání referencí na objekt ............................................................ 44 5.4 Uvolňování paměti ......................................................................................... 45 Mapování dědičnosti.............................................................................................. 47 6.1 Dědění persistentních objektů ........................................................................ 47 6.2 Typy dědění ................................................................................................... 48 6.2.1 Table-per-class ....................................................................................... 49 6.2.2 Table-per-hierarchy ................................................................................ 50 6.3 Vrstva oddělující logický a fyzický datový model .......................................... 51
6.4 Nepersistentní dědění ..................................................................................... 52 7 Praktické využití ORM frameworku ...................................................................... 55 8 Závěr ..................................................................................................................... 56 9 Seznam literatury ................................................................................................... 58 10 Přílohy ................................................................................................................... 59 Příloha A - Ukázka implementace generátoru kódu ................................................... 59 Příloha B - Příklad definice modelu knihovny pomocí FORIS.ORM ......................... 61 Příloha C - Implementace Regex valitátoru ................................................................ 66
1 Cíl práce a metodika 1.1 O čem je tato práce? Moje práce na téma „Mapování dat mezi objektovými programovacími jazyky a relačními databázemi“ se v prvních dvou kapitolách zabývá problematikou proč vlastně potřebujeme objektovou persistenci a srovnává ji s jejími relačními a souborově orientovanými alternativami. Ve 3. kapitole se dozvíte, jaké máte možnosti pokud se rozhodnete postavit svoji aplikaci právě na přístupu relační databáze + ORM. Kapitoly z pořadovými čísly 4, 5 a 6 se zabývají implementací ORM a na příkladech frameworku FORIS.ORM, jehož tvůrcem je autor této práce, se dozvíte něco o technikách a algoritmech, které je možné při tvorbě ORM použít. Sedmá kapitola srovnává několik vybraných ORM frameworků, které jsou v současnosti dostupné, snaží se vypíchnout jejich výhody a nevýhody a doporučit specifická řešení pro specifické úkoly. V poslední, osmé kapitole definitivně přejdeme od teorie k praxi a ukážeme si praktické využití ORM frameworku na informačním systému knihovny.
1.2 Úvod do problematiky Problém trvalého ukládání dat existuje ve světě IT snad už od jejího úplného počátku. Do popředí se však patrně dostal až díky rozmachu evidenčních systémů v 80. letech (evidence majetku, hromadné zpracování mezd aj.), kdy se ukázalo, že dosavadní přístupy založené na primitivní serializaci struktur do souborů nejsou pro tento účel nejvhodnější . Bylo tomu tak především z těchto důvodů: Ve strukturách se velmi obtížně hledalo. Pro urychlení hledání musely vznikat další pomocné soubory, dnešní indexy. Mnoho dat v souborech se opakovalo (např. jméno zákazníka bylo obsaženo v souboru klientů, objednávek i faktur) což zabíralo zbytečně mnoho místa. Ještě horší byla potom úprava těchto redundantních dat. Pokud se např. měnila adresa zákazníka, musel daný systém složitě dohledávat a měnit všechny záznamy, jež tuto adresu obsahovaly. Problémy nastávaly i se současným (paralelním) zpracováním více požadavků v jednom okamžiku, kdy do daného souboru potřebovalo zapisovat v jednom okamžiku více procesů. V neposlední řadě bylo na programátorovi, aby správně navrhnul a napsal mechanismy zajišťující konzistenci dat – tzv. referenční integritu. Což se v drtivé většině případě případů nepovedlo. Všechny výše uvedené důvody byly impulsem ke vzniku relačních databází a tím i radikální změně chápání ukládaných dat. Namísto struktur, které existují v paměti počítače (kniha, zaměstnanec) a které pouze odkládáme na disk za účelem zajištění jejich
persistence jsme začali pojem DATA chápat tak, jak nám to definovali tvůrci databázových systémů: Do relační tabulky (obdoba souboru na disku) ukládáme kartézské součiny atributů dané tabulky a jejich hodnot Podmnožiny těchto kartézských součinů pak můžeme číst nebo měnit pomocí jazyka SQL, kde sloupce (atributy) tabulky je identifikujeme jejich názvem a řádky pomocí hodnot tzv. primárního klíče dané tabulky. Abychom se vyvarovali budoucím problémům s redundancemi a složitými, někdy až protichůdnými změnami přes více tabulek, měl by správný relační návrh vyhovovat tzv. normálním formám. V praxi to pak většinou zjednodušeně znamenalo to, že programátor psal aplikaci, která interně generovala příkazy SQL, které následně posílala databázovému stroji (RDBMS) k vykonání. Všechny relační databáze vracely výsledky čtení dat velmi podobně – jako kartézský součin sloupců a jejich hodnot tj. dvourozměrné pole, se kterým pak aplikace dále pracovala. Tato architektura bývá často označována jako klient-server.
DB
Vykonej SQL Zpracuj ResultSet
Generuj SQL Vrať / zobraz výsledek
Požadavek z venku
Obrázek 1
Ačkoliv objektové jazyky i vývojová prostředí existovala už podstatně dříve, velký komerční BOOM tzv. objektově orientovaných nástrojů pro vývojáře (PowerBuilder, Delphi) nastal až někdy v polovině devadesátých let. Ty s sebou sice přinesly elegantní řešení návrhu prvků uživatelského rozhraní (s využitím OOP), ale vlastní implementaci business logiky (bloky označené ve schématu jako Generuj SQL a Zpracuj ResultSet ) psal programátor povětšinou stále strukturálně. S datovými entitami se nepracovalo jako s objekty, nýbrž jako s dvourozměrným polem (přesně tak, jak je vracel RDBMS).
A právě zde nastal asi největší rozpor. Objektu Book totiž můžeme poslat zprávu LendTo() (vypůjčit), ale poli hodnot vlastností patřící určité knize nikoliv. 1: LendTo(aReader)
Krteček2:Book
emp1:Employee
4433 | Krtecek | Zdenek Miller | 1977 |
emp2:Employee
Není možné
Obrázek 2
A právě problematikou, jak z pole hodnot, které nám vrátí relační databáze, udělat skutečný objekt, kterému mohou ostatní objekty posílat zprávy (a naopak jak zapsat objekt v paměti do databáze) se zabývají technologie označované jako ORM – Object to Relational Mapping – Mapování objektů do relační databáze.
1.3 Objektová persistence dat Podívejme se nyní ještě jednou na obě doposud probrané možnosti persistence, které jsem popsal v úvodu a položme si otázku: Který z těchto dvou přístupů je jednodušší pro vývoj a snáze pochopitelný? Původní, souborově orientovaný (nerelační) – data jsou definována jako bajty v paměti představující určitou strukturu (záznam, objekt). Tato struktura má specifické atributy (jméno, datum narození atd) a lze je nějakým způsobem “odložit” na určité persistentní úložiště (disk, CD, karta, …) Příklad ukládání: Book myBook; myBook.ISBN = "123456"; myBook.Title = "Krtecek"; byte[] data = Serialize(myBook); File.WriteAllBytes("book.db",data);
Příklad načítání dat: int position = ID*sizeof(Book); //physical position in file byte[] data = File.Read("book.db", position, sizeof(Book)); Book myBook = Deserialize(data);
Relační – data již onen zmíněný kartézský součin atributů a hodnot primárního klíče a jsou uložena v relační databázi. V aplikaci se nimi pracuje jako s dvourozměrným polem.
Vložení knížky: string SQL = "INSERT INTO BOOK(ISBN,TITLE) VALUES ('" + isbn + "', '" + title + "')"; sqlcon.ExecuteSQL(SQL);
Načtení z DB: string SQL = "SELECT * FROM BOOK WHERE ID=" + id; DataSet ds = sqlcon.ExecuteDataSet(SQL);
Každý z přístupů má své pro a proti: Relační databáze nám sice poskytuje robustní datové úložiště, kontrolu referenční integrity, podporuje indexování dat, transakce a automatické sestavení optimálního prováděcího plánu, ale za cenu toho, že s databází budeme komunikovat pomocí SQL a co je horší, výsledek dotazu nebudou objekty jako v prvním případě, ale ResultSet – třída, která je společná pro všechny výsledky volání příkazu SELECT. Naopak souborový přístup, který je služebně starší, nás nutí, abychom si většinu funkčností, které RDBMS už obsahuje, napsali sami. S daty ale pracujeme jako s instancemi struktur (v našem příkladě Book), které jsou klientskému jazyku daleko bližší, než nějaké ResultSety a SQL. Vlastní kód pracující s takovými strukturami je pak jednodušší a lépe čitelný, než kód aplikace využívající relační přístup. Jak správně tušíte, objektový způsob persistence dat používá od každého něco. Na klientovi pracujete s daty podobně jako se strukturami u souborového přístupu: Book book = new Book(); //create book in memory book.Title = "Krtecek"; //set some attributes book.Isbn = "123456"; sess.Save(book); //store to databaze
Avšak na pozadí je robustní databázový stroj, který si většinou sám ohlídá referenční intergritu, indexování, transakce atd. Tento stroj může být buď přímo objektový, pak mluvíme o nativní objektové databázi (GemStone, Caché), nebo relační (Oracle, DB2, MSSQL…) s nadstavbou od třetích stran, kde potom mluvíme o tzv. „Objektově-relačním mapování“. Oné nadstavbě třetích stran, která převádí objekty v paměti na řádky v DB budeme říkat object to relational framework, nebo zkráceně ORM. O objektových databázích se v této práci zmíním pouze okrajově, musím však podotknout, že ačkoliv implementace objektového databázového stroje se od relačního velmi významně liší, způsob práce s oběma řešeními je dosti podobný. Databáze, o kterých jejich výrobce uvádí, že jsou tzv. Objektově orientované, zde probírat nebudeme vůbec, neboť se většinou jedná o klasické relační DB, pouze s podporou ukládání různých objektů v jejich binární formě. Atributy těchto binárních objektů pak pochopitelně není možné indexovat ani jinak speciálně zpracovávat v rámci daného RDBMS a výraz „Objektově orientovaný“ je v tomto případě opravdu pouze dílem specialistů na marketing.
2 Porovnání souborových, relačních a objektových přístupů k persistenci V této kapitole se pokusím shrnout rozdíly mezi třemi základními přístupy k datům. Pod souborovým způsobem persistence si nemusíme nutně představit pouze rozsáhlé úložiště souborů nějakého zastaralého účetního programu, ale například i práci s konfiguračním souborem aplikace, zápis stavu oblíbené počítačové hry (save) nebo dokonce ukládání této bakalářské práce na pevný disk pomocí aplikace MS Word. Pod objektovým přístupem mám na mysli objektové databáze či kombinaci RDBMS a ORM.
2.1 Definice datového modelu U aplikací využívající relační přístup k datům neexistuje přímá vazba mezi verzí aplikace a verzí datového modelu. Jinými slovy musíme hlídat, aby to, co je napsáno v kódu aplikace (např. že pro entitu Book existuje atribut Title) byla skutečně pravda i v databázi. Datový model máme tím pádem vlastně definovaný duplicitně na dvou místech. Problém si ukážeme na jednoduchém příkladu. Nechť v databázi existuje tabulka BOOK definovaná například takto : CREATE TABLE BOOK ( ID int not null, TITLE varchar(200), ... )
V aplikaci nám žádný mechanismus není schopen zabránit, abychom omylem zaměnili název atributu TITLE za NAME : sqlCon.ExecSQL("SELECT NAME FROM BOOK"); //"NAME" should be TITLE!!!
U souborového přístupu definujeme strukturu dat pouze aplikaci, takže k podobnému problému docházet nemůže. Objektový způsob persistence má podobně jako souborový pouze jeden zdroj metadat a tím je definice persistentních tříd v klientském programovacím jazyce. Pokud využíváme relační databázi a ORM framework, je ORM zodpovědné za udržení konsistence mezi definicí persisteních tříd a schématem v databázi. U objektového modelu definujeme provázanost objektů pomocí skládání a dědění, v relačním pomocí referenční integrity.
2.2 Vyhledávání a čtení dat Co se týče způsobů dotazování se na data, musím konstatovat, že možnosti relačního i objektového přístupu jsou v tomto směru plně dostačující. V relačním světě využíváme standardních nástrojů jazyka SQL jakými jsou selekce (WHERE) a spojení (JOIN), v objektovém hledáme data pomocí operací přímo nad kolekcemi. Ve většině objektových systémů sice existuje také dotazovací jazyk podobný SQL, ale narozdíl od relačního přístupu, tento jazyk v žádném případě netvoří rozhranní mezi aplikací a databází.
Objektová DB
Relační DB
ormSess.Get
(5); ormSess.GetAllInstances(); ormSess.GetByReferences(5)
Aplikace
SELECT * FROM BOOK WHERE ID=5 SELECT * FROM BOOK SELECT * FROM LOAN WHERE BOOK=5
Aplikace2
Obrázek 3
Čtení dat se u relačního a objektového přístupu se výrazně liší. Souborový a objektový přístup pracuje se strukturami, které jsou už v době kompilace známé, zatímco relační přístup využívá ResultSet, na jehož položky se odkazujeme dynamicky - většinou pořadovým číslem nebo pomocí stringových literálů. Relační: //query for data string sql = "SELECT * FROM BOOK"; DataSet ds = sqlcon.ExecuteDataSet(sql); //print all titles foreach (DataRow row in ds.Tables[0].Rows) { Console.Out.WriteLine(row["TITLE"]); }
Objektový: //query for data List books=ormSession.GetAllInstances(); foreach (Book book in books) { Console.Out.WriteLine(book.Title); }
Na uvedeném příkladu vidíme, že v případě relační databáze se na data dotazujeme select * from ... i čteme row[”TITLE”] pomocí stringových literálů, jejichž správnost kompilátor pochopitelně není schopen ověřit.
U objektového přístupu jsou atributy dotazu na data, i čtení výsledků ověřeneny během kompilace.
2.3 Změny dat Pokud aplikace využívá souborový přístup persistence dat, provádí většinou zápis celého souboru při jeho každé změně. Existují i výjimky, ale jejich realizace s sebou přináší skoro vždy řadu dalších problémů a nevýhod. Proto se také v dnešní době používá souborový přístup pouze pro databáze malé velikosti a složitosti. Z pohledu robustnosti je mezi souborovým a relačním přístupem obrovský rozdíl. V dobře navrženém relačním modelu se data nejen mnohem snadněji vkládají, upravují a mažou, ale navíc nad každou takovouto operací „sedí“ RDBMS, který kontroluje, jestli se naše aplikace nepokouší dělat něco, co by mohlo narušit integritu dat. Bohužel ve většině aplikací využívajících relačního přístupu úplně chybí informace o tom, že to co do databáze ukládáme, je vlastně objekt. Spousta programátorů v tom vidí pouze hodnoty prvků uživatelského rozhranní, které na stisk tlačítka „Zápis“ jejich aplikace pouze doplní jako potřebné parametry SQL příkazu INSERT a odešle databázi.
INSERT INTO BOOK (TITLE, AUTHOR) VALUES ('Krteček','')
SQL databáze
Obrázek 4
Bohužel právě na změnu dat se ve naprosté většině aplikací vážou různé další úkoly jako je jejich validace (více kap. 3.4.2), či dovytvoření dalších potřebných záznamů v jiných entitách a jejich naplnění hodnotami. Jakmile se některý z těchto požadavků dodatečně objeví (málokterý zadavatel je schopen požadavky na validace dat formulovat již při návrhu), programátora většinou napadnou 2 možnosti, kam tuto funkčnost implementovat:
Kamsi do kódu – obvykle si vybere to nejhorší místo – např. handler pro tlačítko „Update“
public void button1Onclick(object sender) { if(textBox1.Text=="") { MessageBox.Show("Title is required item!"); return; } //insert data into database
... }
Do uložené procedury (či triggeru) pro update příslušné tabulky
CREATE TRIGGER t_book_insert BEFORE INSERT as begin if inserted.TITLE is NULL then reaiseerror('Title must be specified!') end
Implementace business logiky na straně RDBMS (trigger) je obecně špatná, neboť uzavírá možnost distribuovat v budoucnu systém na více aplikačních serverů, zvyšuje závislost na platformě a drobí kód obsahující business pravidla na část aplikační a databázovou, což velmi komplikuje přehlednost. Handler, který se spouští při nějaké akci v GUI je druhý, stejně špatný, extrém. Pokud bude docházet k úpravám téhož objektu nejen pomocí daného tlačítka, ale i v jiném případě užití – např. z jiného okna, či přes nově dodělaný web interface - povede toto řešení ke psaní duplicitního kódu. Správné místo pro implementaci business logiky proto, dle mého názoru, může být instanční metoda persistentního objektu, na kterém definujeme dané pravidlo. [DataEntity] class Book { public void ValidateMe() { if(string.IsNullOrEmpty(title)) { throw new Exception("Title must be specified!"); } } .. .. }
Persistentní objekty automaticky vznikají pouze při použití objektového přístupu. Pochopitelně, že problém popsaný výše lze řešit i neobjektově, např. pouhým refaktorováním extract method, ale objektové řešení je snazší a lépe čitelné pro ostatní.
2.4 Úpravy existujícího datového modelu Datový model upravujeme tehdy, shledáme-li současný model vzhledem k aktuálním požadavkům na rozšíření stávající aplikace nedostatečný. V našem příkladě s knihovnou by jím mohl např. být požadavek na poskytovaní důchodcovských slev z registračních
poplatků v dané knihovně. K implementaci takovéto funkčnosti potřebujeme znát věk čtenáře, který ovšem stávající systém neeviduje. Musí tedy dojít rozšíření datového modelu o datum narození čtenáře. U souborového způsobu persistence obtížnost takové změny závisí na její konkrétní implementaci. Např. pro persistenci založenou na XML by taková změna neměla představovat vůbec žádný problém a XML soubory s původní a novou verzí databáze by spolu byly dokonce oboustranně kompatibilní... Pokud ale pod souborovou persistencí chápeme spíše jednoduchý binární soubor záznamy určité struktury soubory jednotlivých verzí mezi sebou kompatibilní nebudou a spolu s upgradem aplikace musíme provádět i migraci dat. Migrační utilitu navíc musíme vytvořit „vlastními silami“. Během migrace je aplikace nedostupná pro uživatele. Migrace (z 1.0.0.0 na 2.0.0.0) reader2.db
reader.db
Obrázek 5
U relačních databází je situace jednodušší v tom, že k úpravě datového modelu není třeba vlastní migrační aplikace. Místo toho můžeme použít příslušného DDL příkazu, který migraci všech položek provede za nás.
ALTER TABLE READER ADD BIRTH_DATE DATE
Relační DB
Integrátor
Obrázek 6
Pomocí SQL příkazu INSERT...SELECT dokonce můžeme migrovat data i do úplně jiných struktur. Např následující příkaz rozšíří evidenci čtenářů i o všechny zaměstnance dané knihovny: INSERT INTO READER (FIRST_NAME, LAST_NAME, BIRTH_DATE) SELECT FNAME, LNAME, BIRTH_DATE FROM EMPLOYEE
Podstatnou nevýhodou obou přístupů (souborového i relačního) je ale fakt, že aplikace a datový model o sobě de facto „nevědí“ a vlastní upgrade aplikační a databázové části musíme provádět odděleně. Pokud si nenaprogramujeme vlastní kontrolní mechanismus, může velmi jednoduše (např nepozorností migrátora) dojít k situaci, že aplikaci máme v jiné verzi než databázi. Systém se pak jeví jako relativně funkční a chybu poznáme většinou až po několika dnech provozu. Vzhledem k tomu, že přístup z využitím ORM definuje pouze jeden deskriptor datového modelu a tím je vlastní definice persistentních tříd, k podobných chybám docházet nemůže. Aplikace si jednoduše při startu ověří jestli definice databázových tabulek odpovídá třídám persistentních objektů (dále PO) a pokud tomu tak není, provede kroky potřebné k úpravě modelu v databázi. Mechanismus projekce objektového modelu do relační
databáze je popsán v kapitole 4.2. Způsob jakým provádí ORM framework úpravy existujícího datového modelu tak, aby vyhovoval objektovému není součástí této práce.
2.5 Transakce Transakcí rozumíme jako sled určitých operací nad daty, které se vnímají jako jedna atomická akce. Laicky řečeno, vše co běží v jedné transakci se musí provést „v jeden okamžik“. Pokud dojde k výjimce během transakce, všechna data se vrací do původního stavu (rollback). Pokud bychom psali například bankovní systém, určitě bychom použili transakce pro převod peněz z jednoho účtu na druhý. Operace odepsání částky z účtu A a připsaní na účet B musí proběhnout buď obě najednou nebo žádná z nich. Není možné, aby existovala situace, kdy částka existuje na obou účtech nebo naopak na žádném z nich. Aplikace využívající souborový přístup persistence dat prakticky transakce nevyužívají a pokud přece jen, zamykají většinou celý soubor (všechny záznamy dané entity) nebo používají zámky pro určité operace – např. zajišťují, že nelze spustit 2 účetní závěrky najednou. Relační databáze jsou v tomto směru podstatně dál, zamykají většinou pouze jednotlivé záznamy (nebo stránky), které se v rámci dané transakce mění a umožňují definovat různé úrovně izolace, tj. operace, které lze ještě nad daty jež jsou součástí jedné transakce provádět v rámci jiné transakce. Transakce nad objekty lze provádět velmi podobně, pouze s tím rozdílem, že ORM framework běží na aplikační úrovni a umožňuje vývojáři dané aplikace řešit konflikty se změnami záznamů uvnitř transakce individuelně pro každou entitu. Důležitou možností při programování na aplikačním serveru je i možnost doprogramování podpory distribuovaných transakcí, tj. transakcí, kterých se zúčastňuje více (povětšinou heterogenních) systémů. Distribuované transakce obvykle řídíme přes produkt třetí strany – tzv. distributed transaction coordinator. Vzhledem k omezenému rozsahu této práce se o transakcích přes ORM zmíním pouze okrajově ve vybraných kapitolách
3 Možnosti ORM v současných programovacích jazycích V následující několika krátkých kapitolách se pokusím představit základní možnosti ORM a vlastnosti, ve kterých se od sebe jednotliví dodavatelé ORM frameworků liší.
3.1 Požadavky na databázi a programovací jazyk Využívat výhod objektové persistence je sice teoreticky možné ve všech objektově orientovaných prostředích, nicméně její implementace by v některých z nich představovala velmi složitý úkol, několikanásobně přesahující rozsah této práce. Uveďme si proto alespoň základní požadavky na vývojové prostředí a relační databázi, kterou chceme pro účely ORM využívat: Požadavky na jazyk a prostředí: Objektově orientovaný jazyk Základní podpora přístupu do databáze (ODBC, JDBC, ADO.NET...) Podpora reflexe pro čtení i zápis Podpora meta atributů výhodou Podpora generických typů výhodou Požadavky na databázi: Základní podpora ANSI SQL Podpora alespoň jednoho kompatibilního rozhranní pro přístup z vybraného programovacího jazyka (ODBC, JDBC....) Podpora uložených procedur views výhodou Pokud bychom použili např. jazyk bez reflexe, museli bychom implementovat pro každý PO ještě metody pro serializaci a deserializaci do DB což by velmi zdržovalo a drobilo vývoj. V případě nekompatibilního rozhranní jazyk-databáze bychom dokonce museli implementovat vlastní JDBC driver či ADO provider! Jako nejvýhodnější se v současné době jeví použití jedné ze dvou pro vývojářskou komunitou dobře známých a věčně soupeřících technologií. Technologií postavených na kompilaci do byte kódu a vykonávání programu pomocí virtuální stroje - Javy či .NETu Obě platformy vyhovují všem výše uvedeným požadavkům a existuje pro ně již mnoho produktů třetích stran, které podporu ORM implementují. Nutno ovšem poznamenat, že v současné době není možné obě technologie nějakým rozumně jednoduchým způsobem kombinovat. Ve zbylé části tohoto textu proto budu pod pojmem programovací jazyk myslet především Javu nebo některý z jazyků rodiny .NET. Příklady budu uvádět v jazyce C#.NET, který je dobře čitelný jak pro vývojáře Javovské i .NET komunity, tak i pro ostatní programátory disponující alespoň základní znalostí C++ či podobného jazyka.
Co se týče databáze, tak tu si vybírá většinou zákazník (resp. už má většinou nějakou koupenou:-), takže zde na výběr moc nemáme. Naštěstí v dnešní době snad všechny relační databáze nějakým způsobem implementují ANSI SQL i standardní aplikační interface, takže s DB by neměl být problém. Konkrétní příklady SQL uvedené v této práci jsou psané pro Oracle 9i.
3.2 Umístění definice modelu Volba vhodného umístění definice datového modelu bývá jedno z prvních rozhodnutí, které musíme učinit, rozhodneme-li se pracovat s ORM. Různé ORM framoworky podporují různé typy přístupů (nebo jejich kombinace). Na výběr však máme v podstatě tyto tři: definice databázového schématu (SQL), externí XML a meta atributy.
Databáze
?
[DataEntity] public class Book { [DataAttribute] public string Title .. .. }
? Model.XML
Obrázek 7
3.2.1 Definice modelu pomocí DDL příkazů SQL Způsob, který je vhodný pro případ, že chceme vytvořit ORM vrstvou nad již existujícím schématem. Většina ORM frameworků má v sobě zabudovanou podporu generování zdrojových kódů persistentních tříd případně mapovacího XML s již existujícího schématu. Vzhledem však k tomu, že relační databáze by neměla být primárním deskriptorem datového modelu (viz především kapitola 2.4), měla by být tato možnost použita pouze pro prvotní vygenerovaní deskriptoru jednoho z alternativních přístupů uvedených níže. Následné změny schématu v relační databázi by už měl provádět pouze ORM framework na základě neshody definice PO a databázového schématu. Příklad vytvoření třídy book pomocí DDL: CREATE TABLE BOOK ( ID int not null PRIMARY KEY, TITLE varchar(255) not null, AUTHOR_ID int FOREIGN KEY REFERENCES AUTHOR )
Pomocí tohoto přístupu nevytváříme skutečný objektový model, ale model relační, k němuž pak přistupujeme pomocí ORM. 3.2.2 Definice modelu pomocí externího souboru Spočívá v tom, že model definujeme v externím, zpravidla XML souboru. Z tohoto XML souboru pak generujeme schéma to databáze a můžeme generovat i vlastní kód reprezentující třídu v daném jazyce.
Mapovací XML jednoduše popisuje, jaký sloupeček v jaké tabulce se váže na který atribut jaké třídy. Příklad: <mapping> <param name="sequence">S_BOOK <property name="Title" column="TITLE" type="java.lang.String" /> <property name="Author" column="AUTHOR_ID" type="FORIS.ORM.Example.Author" />
Všimněte si zejména, že cizí klíč Author zde již není typu INT, nýbrž FORIS.ORM.Example.Autor, což je entita reprezentující autora. Generovaný CREATE TABLE je pak stejný jako v příkladě u kapitoly 3.2.1 a generovaná definice PO může vypadat následovně: public class Book { public string Title; public Author Author; }
Tento přístup je používán zejména v jazycích, které neumožňují definovat tzv. meta atributy (viz dále). Příkladem takového jazyka, který sice splňuje požadavky definované v kapitole 3.1, ale pojem meta-atribut nezná na třeba Java až do verze 1.4. Řešení pomocí externího mapovacího souboru má dva podtypy: Kód pro třídy PO generujeme pokaždé po změně mapovacího XML Kód pro PO třídy generujeme pouze pro nové entity a všechny následné úpravy PO provádíme dvojmo - jak ve vlastních objektech tak i v mapovacím XML. Mezi hlavní nevýhody prvního podtypu patří především nemožnost jakkoliv zasahovat do generovaného kódu PO. Tj. PO mohou obsahovat pouze položky odpovídající jednotlivým sloupečkům v DB. Toto řešení je natolik omezující, že od něj každý vývojář dříve nebo později upustí. Nevýhodou druhého podtypu je potom roztříštěnost změn a nutnost provádět s každou změnou definice PO i změny v mapovacím souboru. Zajímavou alternativou k těmto dvěma silně omezujícím řešením jsou projekty jako například XDoclet. XDoclet si můžeme zjednodušeně představit jako program, který analyzuje zdrojový PO a na základě speciálních značek v komentářích u jednotlivých prvků třídy generuje kód mapovacího XML.
/** * @persistent-class BOOK /* public class Book { /** * @column /* public string Title .. ..
XDoclet engine
Generování mapovacího souboru
<mapping> <param name="sequence">S_BOOK <property name="TITLE" column="name" type="java.lang.String" />
Obrázek 8
Pro jazyky, které meta-atributy podporují je však jednoznačně výhodnější neudržovat ani negenerovat žádný externí mapovací soubor, ale využít právě anotací či atributů. 3.2.3 Definice modelu pomocí anotací a atributů Anotacemi (podle javovské terminologie) či atributy (dle .NETu) myslíme doplňkové meta-informace, kterými můžeme rozšířit definici třídy nebo její libovolné položky. Jedná se de facto o speciální druh komentáře podobného tomu, který jsem popsal v kapitole 3.2.2, když jsem vysvětloval Xdoclet. Narozdíl od klasického komentáře má však meta-atribut 2 velmi důležité vlastnosti: Kompilátor ověřuje syntaxi meta atributu stejně jako jakékoliv jiné položky třídy Meta atribut je (pomocí reflexe) přístupný i uvnitř zkompilovaného kódu a stává se součástí třídy, ve které je definován. Pro použití v ORM má meta atribut velmi cennou funkci – umožňuje nám definovat mapování jazyk-DB aniž bychom potřebovali jakýkoliv další externí mapovací soubor, neboť všechny potřebné údaje můžeme získat pomocí reflexe. Příklad definice třídy Book pomocí meta atributu: [DataEntity("BOOK", PK="ID")] public class Book { [DataAttribute] [DataIndex(“i_book_title”)] public string Title; [DataAttribute] public Author Author; }
Pochopitelně, že žádné duplicitní údaje jako název či datový typ položky, u které požadujeme persistenci už uvádět explicitně v atributu nemusíme, neboť náš ORM framework je schopen získat tyto údaje právě pomocí reflexe. Navíc máme informace, které spolu logicky souvisí pěkně u sebe jednom souboru – objekt a popis jakým způsobem se „serializuje“ do databáze. Přitom všem máme danou definici objektu plně „pod kontrolou“ a můžeme do ni libovolně přidávat vlastní metody i další, nepersistentní položky. Výhody meta atributů nepřímo ocení i pracovníci provádějící deployment u zákazníka, neboť jim odpadne nutnost „udržovat“ další externí XML soubor.
Nezbývá mi nic jiného, než toto řešení doporučit všem, kteří používají .NET nebo Javu 1.5 a vyšší. Pochopitelně existují i vyjímky, které potvrzují pravidlo. Představme si např., že máme CORE aplikaci, která umí vyhledávat knížky podle názvu. Nad touto aplikací vyvíjíme customizaci pro zákazníka, který si přeje také vyhledávat podle jména autora. Vlastní vývoj probíhá tak, že se od základních tříd dědí třídy custom modelu:
Author + Name : string
Compiled assembly
CustomAuthor + BirthDate : System.DateTime
Obrázek 9
Potřebovali bychom přidat atribut [DataIndex] ke třídě Author. Jak ale vidíte na schématu, sestavení, ve kterém se daná třída nachází je již zkompilované (customizační tým nemá možnost měnit CORE části) a tutíž přidání daného atributu není možné. V takovém případě je asi nejlepším řešením povolit kombinaci mapování - pomocí atributů i externího XML souboru. Ve vlastních custom třídách pak můžeme datový model dále definovat pomocí meta atributů a mapování tříd v CORE části bude možné „poupravit“ právě pomocí externího XML, popsaného v kapitole 3.2.2.
3.3 Společný předek pro všechny persistentní objekty? Existují O-R mappery, které vůbec nevyžadují, aby byly persistentní třídy zděděny od nějakého objektu (např. Hibernate) ale naopak, viděl jsem i implementace (třeba microOrm), u nichž už jen pouhý fakt, že třída byla potomkem nějakého předka automaticky znamenal, že bude persistentní. Obě tato extrémní řešení svá pro i proti: Pokud u PO nevyžadujeme, aby dědily ze společného předka ani implementovaly nějaký společný interface, musí nutně všechny metody pracující s PO přijímat nebo vracet object. To může působit problémy hlavně programátorům, kteří daný framework teprve začínají používat. Navíc např. není možné definovat instanční proměnné a metody, které by byly metody společné pro všechny objekty. Naopak pokud budeme systém nutit, aby každou podtřídu určité třídy frameworku chápal automaticky jako persistentní zavřeme si cestu k tzv. “nepersistentní dědičnosti” (viz 6.4).
V našem ORM frameworku jsme zvolili nejrestriktivnější přístup. Aby byla třída persistetní, musí být odvozena od určitého předka ale zároveň musí o obsahovat atribut [DataEntity] informující framework o její persistenci. DBObject i i i i
BeforeSave AfterSave BeforeLoad AfterLoad
: : : :
int int int int
[DataEntity]
[DataEntity]
[DataEntity]
AnyPersistentClass1
AnyPersistentClass2
AnyPersistentClass3
Obrázek 10
Objekt, ze kterého každá třída PO dědí má kromě povinného PK a i několik virtuálních metod (s prázdnou implementací), které ORM provolává pří určitých akcích – např. jako triggery: public class DBObject { /// <summary> /// Executed before real saving into DB /// /// <param name="user">Identification of user who requested given update /// true if saving is handled inside BeforeSave method, otherwise false public virtual bool BeforeSave(UserIdentifier user) { return false; } /// <summary> /// Executed after object is stored into DB /// Useful for modifications after object is saved /// public virtual void AfterSave(bool inserted, UserIdentifier user) { } … .. }
3.4 Údálostmi řízené programování Další velmi příjemnou a často neprávem opomíjenou možností, kterou nám ORM nabízí je tzv. „inversion of control“ neboli také „event driven programming“ – událostmi řízené programování. Vývojář v tomto případě nevytváří hadler pro konkrétní akci či use case, ale pouze definuje, co se má stát, pokud nastane něco, co danou událost vyvolá - např. dojde-li k zápisu knížky do DB. V našem ORM budeme rozlišovat 2 druhy zpracování událostí: triggery a validace. 3.4.1 Triggery Podobně jako u databázových triggerů se i aplikační triggery spouští při nějaké akci nad objektem ve vztahu k jeho persistenci. Tou akcí může být buď načtení objektu (select) nebo zápis jeho stavu (insert, update, delete). Vlastní kód triggeru napíšeme přímo do objektu jako přepsaní metody předka: public override void AfterSave(UserIdentifier user) { base.AfterSave(user); //call ancestor LogWriter.Log("User " + used + " saved!"); }
Aplikační triggery mají oproti svým databázovým kolegům nejméně 2 nesporné výhody: Kód triggeru „zná“ stav aplikace. Pod pojmem stav si můžeme představit všechny dostupné identifikátory z daného aplikačního kontextu (statické položky, singletony či vlastní stav objektu). Tyto identifikátory mohou obsahovat nejrůznější možné informace jako např. údaje o přihlášeném uživateli, jeho opravněních a spoustu dalších aktuálně cachovaných hodnot z DB. Kód triggerů může číst i volat externí zdroje jako jsou externí systemy přístupné přes aplikační rozhranní či soubory lokálně uložené na filesystému. O dalších výhodách triggerů na aplikační úrovní pojednává kapitola 2.3. 3.4.2 Validátory Validační kód bychom sice mohli provádět v triggerech (a složitější validace dokonce dál musíme), ale v případě jednoduchých, často opakujících se validací se jedná o tak specifickou funkčnost, že pro ni většina O-R mapperů implementuje vlastní podporu. Mějme rozhranní IValidator definované například takto: /// <summary> /// Every validators specified in [Validation(validatorType,params)]
must implements this interface. /// public interface IValidator { /// <summary> /// If validation fails, an exception should be thrown, otherwise nothing should be done.
/// /// <param name="value">value of specified property void Validate(object value); /// <summary> /// parameter(s) for validator (such as reference table name or regexp). Write only property initialized when validator is created. /// object[] Parameters { set; } }
A například třídu RegexValidator, která implementuje validaci položky PO proti standardnímu regulárního výrazu: IValidator + Parameters : object[] + Validate (object obj) : void
RegexValidator
+ <> Validate (object obj) : void
Obrázek 11
Pak můžeme validovat jednotlivé položky PO pomocí regulárního výrazu pouhým přiřazením atributu [Validation]: //author's name must not be longer than 50 characters [Validation(typeof(RegexValidator),"^.[0-50]$")] [DataAttribute] public string Name
Atribut [Validation] zajistí provolání příslušného validátoru při každém pokusu o uložení objektu do DB. V případě že validace selže, objekt se nezapíše a klient to pozná prostřednictvím vyhozené výjimky. Kompletní implementaci Regex validátoru naleznete v příloze.
3.5 ORM aplikační server Až do této kapitoly části vlastně popisovali jakýsi ORM framework aniž bychom specifikovali, kde daný kód, který zajišťuje ORM mapování, vlastně běží. Vlastní ORM framework není de facto nic jiného než sada knihoven, které přidáme do projektu stejným způsobem jako jakoukoliv jinou knihovnu třetí strany, jež nám umožní používat public třídy definované uvnitř.
Pokud bychom tedy chtěli vyvíjet aplikaci typu obyčejný klient-server, nic nám nebrání vytvořit takovouto architekturu: sestavení knihovna-klient.exe
SELECT * FROM BOOK WHERE ID=5
Relační databáze
ORM Framework
ormSession.Get(5);
Aplikace "Knihovna"
Obrázek 12
V reálném světě však každý zákazník požaduje, do systému mohlo připojovat více klientů. Každý takový klient, by pak ale používat vlastní instanci ORM s vlastní konfigurací a vlastní cache, což pochopitelně nechceme. Jedním řešením by mohlo být pouhé upřesnění daného diagramu, že aplikace „knihovna-klient.exe“ je webová aplikace. Pokud by ovšem tomu tak nebylo, bylo by ideální mít pouze jeden aplikační server, který by se choval jako objektová databáze: sestavení knihovna-server.exe
sestavení knihovna-klient.exe service.GetBook(5);
Aplikač ní server
Aplikace "Knihovna" ^BookDto
^Book
ormSession.Get(5);
ORM Framework
^ResultSet
SELECT * FROM BOOK WHERE ID=5
Relač ní databáze
Obrázek 13
Na schématu nám především přibyla komponenta Aplikační server.
Abychom mohli pokračovat dále ve výkladu, musíme si nejprve popsat, co všechno se při danám požadavku na knížku s ID=5 děje: 1. Klient vytvoří požadavek a zavolá aplikační server přes nějaký standardní interface pro distribuovaná volání (Remoting, RMI...) - service.GetBook(5) 2. Aplikační server požadavek přijme a „přeloží“ jej na volání ORM frameworku, který běží jako referencovaná knihovna ve stejném aplikačním kontextu. – ormSession.Get(5)
3. ORM framework zjistí jestli nemá již příslušný objekt v cache. Pokud ano, vrátí jej a pokračuje krokem 6. Pokud ne, zavolá databázi, aby mu vrátila ResultSet obsahující daný objekt. – SELECT * FROM BOOK WHERE ID=5 4. Databáze najde příslušný záznam a vratí ho. - ^ResultSet 5. ORM ResultSet přetransformuje do určité instance persistentní třídy a vrátí aplikačnímu serveru. - ^Book 6. Aplikační server přijme objekt třídy Book. Ten však není možné vrátit volajícímu systému, protože obsahuje spoustu implementací různých metod využívajících reference na sestavení, která klientská aplikace nemá k dispozici. Překopíruje tedy všechny hodnoty persistetních atribututů třídy Book do jednoduché struktury BookDto (DTO=Data Transport Object), která je součástí interface mezi serverem a klientem a vrátí jej – BookDto 7. Z pohledu klienta se celá operace jeví jako jednoduché volání funkce GetBook(id), která vrací BookDto.... Kroky 3 a 5 tedy provádí ORM framework. Co ale s kroky 2 a 6? A jak vůbec vytvářet interface pro aplikační server a objekty DTO, když primárním deskriptorem datového modelu jsou třídy PO? Je téměř jasné, že v tak přísně typových jazycích jako je Java nebo C# se v tomto případě bez generovaného kódu neobejdeme. Pokud tedy chceme používat vybraný ORM framework jako aplikační server, měli bychom se také podívat, jestli podporuje alespoň generování interface pro aplikační server, v lepším případě i jeho implementace, tj kód prováděný v krocích 3 a 5. Vlastní vývoj konkrétní aplikace s využitím ORM se pak rozpadá do několika málo částí: Tvorbu datového modelu, pomocí definice persistentních tříd Implementaci business logiky pomocí triggerů (kap. 3.4.1) a validátorů (kap 3.4.2) Vygenerování DB schématu Vygenerování API aplikačního serveru (interfacové i implementační části) Tvorbu klienta využívajícího serverové API
4 Základy ORM mapování V této části si vysvětlíme princip ORM mapování a techniky, jakými lze dosáhnout požadovaného chování.
4.1 Definice objektového modelu O definici objektového modelu jsem toho napsal dost již v předchozích kapitolách. Kapitola 2.1 pojednávách o výhodách definice modelu v objektové formě oproti relační, kapitola 3.2 zas rozebírá různé možnosti, jak lze daný definovat. My nyní upustíme od syntaxe, jakou lze daný model definovat, ale zaměříme se na vlastní implementaci metamodelu, tj. objektového modelu, kterým je po načtení konfigurace reprezentován konkrétní model dané aplikace v paměti. Metamodel je základem každého ORM frameworku a měl by obsahovat minimálně následující údaje: Všechny persistetní entity a jejich mapování na tabulky Všechny atributy PO a jejich mapování na sloupce v DB Všechny cizí klíče pro dané atributy a jejich mapování na příslušné kolekce či reference objektového modelu Všechny klíče (indexy) podle kterých je možné vyhledávat Všechna sestavení (assembly) ze kterých byl daný model vytvořen a kolekce entit, které toho sestavení definuje
AssemblyProperty
IndexProperty
1..1
0..* Index columns
0..* 1..1
1..*
1..* SchemaProperty
EntityProperty
1..1
ColumnProperty 1..1
0..*
0..* 1..1 FkColumn
1..1 0..* FkProperty
Obrázek 14
0..*
Instance metamodelu vzniká zpravidla při startu aplikace, konkrátně při načítání (parsování) definice daného modelu. Metamodel slouží jako cache pro definici metadat a zároveň odděluje fyzickou definici modelu (DDL, XML soubor, meta-atributy) od logické. To znamená že implementace ORM frameworku pracuje vždy pouze s metamodelem a nikdy nečte žádný externí XML soubor ani atributy persistentních tříd. Naopak o naplnění modelu se starají speciální třídy tzv. MetamodelBuilder, jež mají za úkol „naparsovat“ metadata z konkrétních vstupů (DDL, XML...). Jedná se návrhového vzoru Builder. IMetamodelBuilder + Source : object + + + +
XmlMetamodelBuilder
+ + + +
<> <> <> <>
BuildEntities () BuildAttributes () BuildReferences () BuildInheritance ()
BuildEntities () BuildAttributes () BuildReferences () BuildInheritance ()
: : : :
void void void void
ServiceMetamodelBuilder
AttributeMetamoderBuilder : : : :
void void void void
+ + + +
<> <> <> <>
BuildEntities () BuildAttributes () BuildReferences () BuildInheritance ()
: : : :
void void void void
+ + + +
<> <> <> <>
BuildEntities () BuildAttributes () BuildReferences () BuildInheritance ()
: : : :
void void void void
Takes metadata from any external system - global data calalogue
Obrázek 15
Za zmínku stojí tzv. ServiceMetamodelBuilder, což je třída, která narozdíl od svých sourozenců nehledá definice persistentních objektů v žádném z lokálních zdrojů, ale pomocí speciálního interface se připojí ke službě, která globálně definuje všechna metadata pro více systémů.
4.2 Projekce modelu do relační databáze Pokud máme veškerá metadata naparsovaná v jednom metadata modelu, můžeme je využívat k různým účelům. Jedním z nich je např. projekce modelu do relační databáze, tj. generování DDL příkazů, které v DB vytvoří potřebné tabulky, views a uložené procedury, jež bude dále náš ORM framework využívat. Vlastní generátor bychom mohli napsat přímo v použitém programovacím jazyce, ale brzy bychom zjistili, že generování vlastního (navíc platformově závislého) SQL kódu není nic víc, než různé, relativně složité iterování přes kolekce metamodelu a spojování řetězců a proměnných. string result = ""; foreach (EntityProperty entity in Entities) { result += "CREATE TABLE " + entity.Name + " ("; foreach (ColumnProperty column in entity.Columns) { result = column.Name+" "+column.Type+" "+column.Constraints+", \r\n"; }
… }
Kód se tak stane brzy velmi nepřehledným a náchylným k chybám. My si zde ale úkážeme implemetaci jednoduchého šablonové orientovaného deklarativního jazyka, pomocí něhož můžeme definovat jednoduché a přehledné šablony pro veškerý SQL kód a dokonce i kód aplikačního serveru popsaný v kapitole 3.5. Jazyk zná pouze 2 konstrukce: $[xxxx] bude nahrazena hodnotou atributu “xxxx” aktuálního objektu $[yyyy [ opakovanyKod ]] zajistí iteraci přes všechny prvky kolekce “yyyy” patřící aktuálnímu objektu. Idenfitikátor “xxxx” či “yyyy” může být buď jednoduchý název vlastnosti aktuálního objektu, nebo složitější cesta přes ukazatele k podobjektům daného objektu zapsaná pomocí známe tečkové notice. Tj např. $[foreignKey.SourceEntity.Name] představuje název zdrojové entity cizího klíče (viz schema v kapitole 4.1) Každá iterace mění kontext aktuálního objektu na objekt, přes který právě iterujeme. Vše co není uvozeno pomocí konstrukce $[xxx] se kopíruje na výstup jako prostý text. Takže celý zdrojový soubor generátoru vlastně představuje jakousi šablonu, podobně jako tomu je např. jazyku XSLT. K tomu, abychom mohli psát šablony generátoru potřebujeme kromě dvou výšeuvedených definic znát už jen metadatový model popsaný v kapitole 4.1. Kód, který dělá to samé, co ukázka C# kódu na generovaní „CREATE TABLE“ v úvodu této kapitoly by v našem deklarativním jazyce mohl vypadat např. takto: $[Entities[ CREATE TABLE $[Name] ( [$Column[ $[Name] $[Type] $[Constraints], ]]) ... ]]
V obou případech by bylo výsledkem něco jako: CREATE TABLE BOOK ( ID int not null PRIMARY KEY, TITLE varchar(255) not null, AUTHOR_ID int FOREIGN KEY REFERENCES AUTHOR )
pouze s tím rozdílem, že ve druhém případě je kód výrazně jednodušší pro zápis i čtení. Navíc se nám jako vedlejší efekt podařilo „vytáhnout“ závislost na platformě (v tomto případě na DB Oracle) do externího souboru.
4.3 Třída Session – páteř každého O-R Mapperu Většina příkladů kódu na využití ORM, které jsem použil v této práci využívají jistý objekt třídy Session (často označovaný jako sess). Tento objekt vlastně představuje vlastní pomyslný můstek mezi vaší aplikací a ORM. Jeho význam je velmi podobný objektům Connection ze světa aplikací, jež využívají relačných databází přímo. Ve třídě Session je vlastně definováno API celého ORM. Jak se ukážeme v následujích kapitolách, objekt třídy Session nám umožňuje hledat, načítat i naopak zapisovat stavy persistentních objektů z/do DB. Každý objekt třídy Session v sobě interně drží jednu instanci připojení (Connection) do DB. Konfigurace toho, ke které konkrátní DB se máme připojit se náčítá z konfiguračních souborů. Proces vytváření session:
Načti konfigurace orm.config
Vytvoř připojení Connection pool
Obrázek 16
Aby si aplikace postavená nad ORM nemusela předávat objekt Session ve všech objektech či metodách jako parametr, bylo by dobré ji zpřístupnit nějak globálně. Otázkou je ale jak, neboť programujeme většinou aplikační server a tam naše aplikace často běží ve více vláknech (threadech). Jedno přípojení do databáze však dokáže v jeden okamžik obsloužit pouze jeden požadavek, použití návrhového vzoru Singleton tedy nepřichází v úvahy, neboť by mohlo docházet ke konfliktům mezi požadavky od různých vláken. Tento problém řeší návrhový vzor thread local, někdy známý také jako thread scope variable (více na http://www.codeproject.com/threads/threaddata.asp) Princip použití je velmi jednoduchý. Třída Session má statickou property Current, jejíž accessor (get metoda), nejprve ověří, jestli jsme v daným vlákně již Session nepoužívali. Pokud zjistí, že ano, vratí již použitý objekt, v opačném případě Session (i s připojením do databáze) nově vytvoří. Z pohledu klienta je pak použití stejné jako by bylo v případě, kdybychom použili singletonu. Session je všude dostupná pomocí statické vlastnosti Current. Session.Current.Save(anObject);
Implementace Session.Current
[Ano]
Již použita v rámci threadu?
Použij stávající Session
[Ne]
Vytvoř novou Session
Viz předchozí schéma
Obrázek 17
A dokonce platí: Session a=Session.Current; Console.Out.Write(Session.Current==a); //vrátí true
4.4 Mapování jednoduché třídy Pokud bychom měli nějakou primitivní persistetní třídu, která nemá žádné reference na jiné PO, odpovídala by jí právě jedna tabulka v DB se stejným počtem i názvy atributů. Třída: Book
[DataEntity("BOOK")] public class Book: DBObject { [DataAttribute] public string Title; }
+ Title : string
Definice tabulky v DB: CREATE TABLE BOOK ( ID int not null PRIMARY KEY, TITLE varchar(256) not null )
BOOK ID Number <M> TITLE Characters (256) PK
Každé instanci tatovéto třídy pak bude v DB odpovídat jeden záznam (řádek) v tabulce odpovídající příslušné entitě. Např. pro volání: Book book = new Book(); book.Title = "Krtecek"; sess.Save(book); Console.Out.WriteLine("Vlozeno pod ID:"+book.Id);
by v DB mohl odpovídat záznam: ID 5
TITLE Krtecek
Jak již bylo uvedeno v kapitole 3.3, náš O-R mapper používá pro všechny persistentní třídy stejný název atributu pro primární klíč – ID. Pokud objekt předávaný metodě sess.Save() má hodnotu tohoto atributu nezadanou (null), provede vložení nového záznamu do DB a následně přiřadí hodnotu databázového ID i položce „Id“ příslušného objektu. V případě, že objekt předaný funkci sess.Save()již existuje, dojde pouze ke změně záznamu s příslušným ID. Tj. pokud bychom hodnotu persistentního objektu změnili např tímto způsobem: (pokračování kódu výše – v book.Id je hodnota 5) book.Title = "Cipisek"; sess.Save(book);
Změní se i příslušný záznam v DB: ID 5
TITLE Cipisek
Pomocí hodnoty ID a fce sess.Get(long id) můžeme i načítat stav objektu: Book book = sess.Get(5); Console.Out.WriteLine("Knizka z ID 5 ma nazev: "+book.Title);
Objekty, které ještě nemají svůj obraz v databázi, tj. ty objekty, které mají své ID=0 budeme nazývat transientní a naopak objekty, které již v DB existují budeme označovat jako persistetní. Je pochopitelné, že pesistetními se mohou stát pouze ty objekty, které jsou instancemi některé z persistetních tříd.
4.5 Vyhledání záznamu V předchozí kapitole jsme si ukázali, jak načíst objekt z databáze do paměti (metoda sess.Get) pomocí jeho primárního klíče. To však bohužel většinou nestačí. Velmi často se totiž setkáváme s případy, že potřebujeme nalézt objekt podle jiného atributu, než je jeho ID. Obsluha potřebuje např. ve své GUI aplikaci nalézt knížku podle jejího názvu, ISBN, jména autora atd. V takovém případě hledáme data podle určitého indexu. Ve světě
ORM je index téměř to samé co u RDB. Jediný podstatný rozdíl je v tom, že u RDB o použití (či nepoužití) indexu rozhodoval databázový stroj automaticky na základě daného SQL příkazu, v našem ORM musíme název indexu specifikovat explicitně. Index definujeme přímo na položce persistentní třídy: [DataEntity("BOOK")] public class Book { [DataAttribute] [DataIndex(“book_title”)] public string Title; }
Pak můžeme vyhledávat danou knížku podle názvu takto: List books=sess.GetByIndex("book_title", "Krtecek"); Console.Out.WriteLine("V databazi existuje "+books.Count+" knizek z nazev 'Krtecek'");
Pochopitelně, že index lze obdobným způsobem možné definovat i na více položkách. Pak jednoduše použijeme meta atribut [DataIndex] se stejným názvem vícekrát v rámci jedné třídy.
4.6 Mapování referencí Pokud máme v systému 2 nebo více entit zpravidla tyto entity neexistují v bázi jen tak odděleně, ale můžeme mezi nimi definovat určité vazby. Kdybychom například evidovali autory a knížky, které napsali, mohl by relační model vypadat např. takto: BOOK AUTHOR
Author
ID Number <M> NAME Characters (256) PK
ID Number <M> NAME Characters (256) AUTHOR_ID Number PK
Tj. každá knížka v našem modelu má právě jednoho autora. V objektovém světe zpravidla nebývá tato skutečnost reprezentována tak, že by objekt Book měl nějakou číselnou položku „ID autora“, nýbrž namísto AUTHOR_ID obsahuje přímo referenci na příslušný objekt Author: [DataEntity("BOOK", PK="ID")] public class Book {
[DataAttribute] [DataIndex(“i_book_title”)] public string Title; [DataAttribute] public Author Author; }
Nespornou výhodou tohoto přístupu je, že pokud potom v aplikačním kódu potřebujeme jméno autora získáme jej velmi jednoduše z objektu Book: Book book = sess.Get(5); Console.Out.WriteLine("Autor knizky s ID=5 je: "+book.Author.Name);
Nevýhodou je potom nutnost rekurzivně načíst všechny takovéto reference už v momentě získávání objektu (ve funkci sess.Get). Pokud tedy např. klient potřebuje znát pouze jméno knížky, nikoliv autora, vytváří se instance třídy Auhor zcela zbytečně. Tento problém však relativně elegantně řeší kapitola 5.3 – zpožděné načítání referencí.
4.7 Mapování kolekcí Podívejme se ještě jednou na příklad datového modelu z předchozí kapitoly. Ukázali jsme si v ní, že ve světě objektů se cizí klíče nemapují jako číselné položky, ale skutečné reference na objekt. Z objektu třídy Book jsme tedy byli schopni se dostat na objekt Author pomocí stejnojmenné položky třídy Book. Představme si však nyní situaci opačnou. Známe autora a chtěli bychom k němu znát i knížky, které napsal. U aplikací postavených na relační DB bychom tento problém pravděpobně řešili pomocí SQL dotazu podobného tomuto: SELECT * FROM BOOK WHERE AUTHOR_ID=XXX
ORM framework však tento dotaz (včetně napamování výsledků dotazu na objekty) provádí automaticky. Jediné, co je v tomto případě potřeba, je definování příslušné kolekce na tříde Author: [DataEntity("AUTHOR")] public class Book { [DataAttribute] public string Name; [DataCollection] public Book[] Books; }
Vlastní práce s kolekcí Books je již stejná jako práce s jakýmikoliv ostatními kolekcemi: Author author = sess.Get(10); Console.Out.WriteLine("Author s ID=10 napsal tyto knihy:"); foreach (Book b in author.Books) { Console.Out.WriteLine(b.Name); }
Stejně jako reference i kolekce obvykle implementujeme se zpožděným načítáním (lazyinitializing) – kapitola 5.2 Kromě vzahů 1:N lze pomocí kolekcí definovat i vzahy 1:1 a M:N, těmi se ale však vzhledem rozsahu této práce a faktu, že každou vazbu M:N lze realizovat jako 2 vazby 1:N, nebudeme podrobněji zabývat. U atributu [DataCollection] nemusíme uvádět žádné další parametry, protože ORM si pomocí reflexe zjistí, že třída Book obsahuje právě jednu referenci na třídu Author. Kdyby jich existovalo více (např. Author a CoAuthor), museli bychom expliocitně specifikovat, o kterou referenci se jedná. [DataCollection(“AUTHOR”)] public Book[] Books;
Vazba mezi dvěma objekty (např. Author a Book) může být tedy definována třemi způsoby: 1. Jako reference – pak každý objekt třídy Book „ví“ o „svým“ objektu Author, ale pokud známe Autora nemůžeme pomocí něj jednoduše přistoupit k instancím knížek, které napsal. 2. Jako kolekce - pak každý objekt Author obsahuje kolekci objektů třídy Book představující knížky napsané daným autorem, ale z objektu Book se nedostaneme k Autorovi. 3. Tzv. oboustranná (bidirectional) reference, což je vlastně situace, kdy pro danou vazbu existují odkazy z obou objektů. Pro 1:N je to vždy z jedné strany reference a z druhé kolekce. Každý z těchto 3 způsobů má svá pro i proti a ani jeden z nich nelze doporučit obecně, pro všechny případy a záleží pouze na člověku, který daný model navrhuje, pro ktrerou variantu vazby se rozhodne. Oboustranné reference jsou sice nejflexibilnější z hlediska použitelnosti, ale zase jejich realizace je bývá nejsložitější a tím i nejvíce náchylná k chybám.
4.8 Object Reader Do této podkapitolky jsem se rozhodl shrnout několik implementačních detailů, které právě se základy ORM mapování souvisí. Název „Object reader“ jí nedal náhodou.
Většina pokročilejších ORM frameworku totiž právě vzor Reader (nebo také Iterátor) implementuje pro účely vlastní konverze řádků ResultSetu na persistetní objekty. Reader není nic víc, než objekt, který nám umožňuje iterovat sekvenčně nějaká vstupní data bez toho, abychom dopředu znali velikost těchto dat či měli možnost vracení se a přeskakování záznamů. Reader disponuje většinou dvojící method typu HasNext()/Next() nebo Next()/GetCurrent(), jež zajišťují vlastní iteraci po položkách. A objekt, který nám vrácí databázové API jako výsledek operace SELECT (v tomto textu označovaný jako ResultSet) má tyto metody také. DataReader
+ HasNext () : bool + GetValue (int index) : object
Pokud bychom rozšířili třídu DataReader o metodu DBObject GetObject(), mohli bychom kód zajišťující dané mapování umístit přímo tam a naše implementace Get(), GetReferences() a GetByIndex() by už pracovaly pouze s ObjectReaderem. DataReader
+ HasNext () : bool + GetValue (int index) : object
ObjectReader
+ GetObject () : DBObject
Kámen úrazu je ovšem v tom, že DB API má ve své implemetaci povětšinou „zaharcodováno“, že má vytvářet instance objektů DataReader a ne ObjectReader a abychom to změnili, museli bychom jej decompilovat a upravit příslušný řádek obsahující new DataReader() na new ObjectReader(), což není povětšinou možné. Naštěstí to není nutné, neboť pro tento účel můžeme využít návrhového vzoru Decorator (neboli Wrapper - obal), a „obalíme“ původní DataReader tak, že delegujeme všechny zděděné public metody původního objektu DataReaderu do nového objektu ObjectReader, který má s původním DataReaderem dokonce i společný interface. public class ObjectReader:DataReader { private DataReader reader; public ObjectReader(DataReader reader) { this.reader = reader; public override HasNext() { return reader.HasNext(); } public override GetValue(int index) { return reader.GetValue(index); }
}
public DBObject GetObject() { //here is a GetObject implementation using reflection } }
Z pohledu klientského kódu to pak vypadá tak, že klient pouze použije původní DataReader vrácený pomocí DB API v konstruktoru ObjectReaderu: ObjectReader reader=new ObjectReader(originalReader);
Používaní vlastního ObjectReaderu zároveň poskytuje určitou abstraktní vrstvu mezi DB API (jeho součástí původní DataReader nepochybně je) a implementací ORM frameworku a mohli bychom jej za určitých okolností (např při používání více vzájemně nekompatibilních DB API) použít i např. jako adaptér.
5 Implementace cachování a zpožděného načítání dat Pokud programujeme naši aplikaci nad nějakou relační databází, dost často řešíme dilema: „Mám tyto data objekt držet v paměti nebo raději opakovaně načítat z DB při každém požadavku?“. Obě varianty totiž mají své nevýhody: Pokud držíme data v paměti, vždy musíme řešit otázku kdy a jak často máme tuto cache vyprázdnit, pro případ, že by data v DB někdo jiný změnil. Pokud bychom necachovali vůbec může se zas stát, že se naše aplikace bude do DB dotazovat příliš často a začneme mít performance problémy. Budeme-li se na data dotazovat aplikačního serveru s ORM frameworkem (viz 3.5), můžeme celou cachovácí logiku implementovat právě zde.
5.1 Základy cachování dat v ORM Jak bylo již uvedeno v kapitolách 4.4, 4.5 a 4.6 náš ORM framework podporuje 3 základní způsoby dotazování se na data. Přímo pomocí primárního klíče – ID objektu (Get) Pomocí jiného klíče – indexu (GetByIndex) Pomocí cizího klíče (většinou řešeno pomocí kolekcí a přímých referencí na objekt) K těmto třem klíčovým funkcím patří analogicky i tři druhy cache, které si rozebereme v následujících podkapitolách. Než se všech do toho pustíme zmíním se alespoň okrajově o tom co to vlastně taková cache je. Představme si, co se stane, když načteme objekt z DB do paměti, tj zavoláme např. Book book = sess.Get(5);
Daná volání Get() nejprve generuje SQL příkaz, ten se spustí nad DB, DB vrátí ResultSet ten se transformuje na objekt a vrátí. Z pohledu uživatele ORM se jednoduše přiřadí proměnné book instance nějakého objektu. Stav po provedení daného příkazu Get() bez cachování bychom si tedy mohli znázornít v např takto: book5:Book lokální proměnná "book"
Je zřejmé, že v tomto případě se náš objekt Book po ukončení platnosti lokální proměnné book stane nadále nepoužitelným a při nejbližší aktivaci garbage collectoru dojde k jeho uvolnění.
Aby si naše aplikace „pamatovala“, že pod ID 5 se nachází objekt, který byl již načten některou z předchozích operací, vytvoříme pro každou entitu jednu hash tabulku, kde klíčem bude ID objektu a hodnotou reference na daný objekt. Tj. stav po volaní metody Get může být např. takovýto:
book5:Book
BookCache:Dictionary Key Value
lokální proměnná "book"
=5 =
Každý následující dotaz na objekt s ID=5 pak způsobí, že ORM framework vrátí (díky kontejneru BookCache) namísto nové instance třídy Book již existující objekt z paměti. Book book = sess.Get(5); Book theSameBook = sess.Get(5);
Způsobí: lokální proměnná "book"
book5:Book
lokální proměnná "theSameBook"
BookCache:Dictionary Key Value
=5 =
Tento přístup nám mimo již zmiňovaného zvýšení výkonu také zajistí, aby nedocházelo k situaci, kdy dvě různé proměnné v naší aplikaci ukazovají na dvě různé instance objektu Book s ID 5 a nemohlo tak docházet k přepsání nových změn starými jako na následujícím příkladě. public void UpdateLastAccess(int id) { DBObject obj=sess.Get(id); obj.LastModification = DateTime.Now; sess.Save(obj); } public void UpdateBookTitle(int id, string title) { Book aBook=sess.Get(id); aBook.Title = title; UpdateLastAccess(id);
sess.Save(aBook); //if we didn’t use CACHE, information about LastAccess would be overwritten!! }
Na uvedené ukázce vidíme, že funkce UpdateLastAccess zapisuje příslušnému objektu Book informaci o posledním přístupu. Tato hodnota by byla ovšem v případě, že by ORM nepoužíval pro objekty třídy Book cachovaní, přepsána svoji vlastní starou hodnotou z objektu aBook. 5.1.1 Cachování primárních klíčů Cachování primárních klíčů je jedním ze základních stavebních kamenů FORIS.ORM. Její implementace je poměrně jednoduchá. Každý objekt EntityProperty (viz metamodel 4.1) vlastní právě jeden objekt PkCache, jež je triviální hash slovník Dictionary (příklad viz výše v kap 5.1). V něm se při každém požadavku na objekt podle daného Id zjišťuje, jestli objekt již nebyl načten do paměti pomocí některé z předchozích operací a teprve pokud tomu tak nebylo, ORM se dotáže DB pomocí příkazu SELECT. Složitější je však vlastní údržba PkCache. Kromě již uvedeného volaní sess.Get(), je nutné se na danou PK cache dotazovat i při zpracovávání ResultSetu po volání GetByIndex a GetReferences, neboť nemůžeme připustit, aby nám např. tato dvě volání volání: Book b1=sess.GetByIndex("book_title", "Krtecek")[0]; a Book b2=sess.Get(5);
v případě, že by se knížka “Krtecek” měla ID=5 vrátilo 2 rozdílné objekty (tj. b1!=b2). 5.1.2 Cachování indexů Princip cachování indexů je podobný jako cachování primárních klíčů. Je však třeba zohlednit dva podstatné rozdíly:
Index nemusí být unikátní. Tj. jednomi klíči (např. jménu čtenáře) může odpovídat více hodnot. Např. může být ve vašem systému evidováno více různých čtenářů s jménem Jana Nováková.
Hodnota indexového klíče se může v průběhu živatnosti objektu dynamicky měnit. Např. naše čtenářka Jana Nováková se vdá a změní si jméno na Jana Novotná. V takovém případě pak musíme odebrat objekt z původní kolekce záznamů odpovídajícím klíči „Nováková“ a naopak zařadit jej do kolekce pro klíč „Novotná“ pokud je tento klíč obsažen v cache.
Pro každou entitu pak existuje kolekce několika index cache (pro každý index jedna), která je definována jako hash slovník indexového klíče a kolekce objektů tomuto klíči odpovídající: IndexProperty
EntityProperty
IndexCache
0..1
0..1
0..*
0..*
0..1 0..* IndexKey
0..1
0..* DbObject
IndexValues 0..1 0..*
Dictionary
Samotná třída IndexKey pak představuje jednu nebo více (v případě složených indexů) primitivní hodnotu daného indexového klíče. Třída IndexValues je de facto kolekce typu List, která v sobě obsahuje všechny objekty, jež odpovídají danému indextovému klíči.
5.1.3 Cachování referencí Narozdíl od index cache a PK cache, které fungují velmi podobně je vlastní cachování referencí, tj navigace ve stromu objektů od nadřízeného k podřízeným, držena přímo v daných nadřízeních objektech, nikoliv v metakatalogu jejich třídy. Jinými slovy, každá instance určité persistentní třídy má svojí vlastní cache na podobjekty. Toutou cache většinou bývájí přímo kolekce jiných persistentních objektů, které daný persistetní objekt obsahuje jako datové položky (viz 4.7). Book + ISBN : string + Author : Author
Author 0..* 0..1
+ Name : string + Books : List
Na uvedenám příkladě vidíme oboustrannou (bidirectional) vazbu mezi třídami Book a Author. Daná relace je realizována pomocí kolekce Author.Books a položky Book.Author. Největší problém při implementaci cachování referencí se vyskytuje právě u oboustranných odkazů a spočívá v nutnosti jejich synchronizace po změně jednoho z odkazů.
Než se do něčeho takového pustíme, měli bychom si položit několik důležitých otázek typu: Opravdu pro řešení daného problému potřebujeme oboustranný odkaz? Musí se oba konce použít i pro zápis? Tj. má metoda sess.Save() opravdu pracovat s oběma stranami? Má se druhá strana po úpravě první změnit okamžitě, nebo až po provolání metody sess.Save()? FORIS.ORM oboustranné odkazy podporuje, nutnou synchronizaci si však podstatně zjednodušuje tím, že s kolekcemi pracuje pouze jako s read-only položkami a žádné konflikty tak rešit nemusí.
5.2 Zpožděné načítání kolekcí Jak jsem již uvedl v kapitole 4.7, vztahy typu 1:N implementujeme v ORM pomocí kolekcí. Když se podívání ještě jednou na příklad s autorem a knížkou v kapitole 5.1.3, vidíme, že pro třída Author obsahuje kolekci: List books Tato kolekce může být naplněna ve chvíli, kdy vytváříme objekt třídy Author. Problém ale spočívá v tom, i kniha může (a v praxi navíc většinou má) nějaké svoje kolekce (např. seznam čtenářů), objekty těchto kolekcí mohou mít svoje další kolekce atd. Výsledkem by mohlo být postupné rekurzivní načtení celé databáze do paměti hned při prvním načtení jednoho autora (viz příklad). Zdeně Miller:Author
List Books
Krteček & kalhotky:Book
List Readers
Karel:Reader
Krteček & autíčko:Book
List Readers
Jana:Reader
Pepa:Reader
List ReadBooks Book1:Book
Book2:Book
Book3:Book
Book4:Book
Přitom v dané chvíli z databáze nechceme načíst jiný objekt, než právě onoho jednoho autora. Pokud ale budeme v některé z následujících operací chtít přes danou kolekci iterovat nebo např. zjistit počet jejich prvků, bylo by vhodné, aby si kolekce „sama“ automaticky potřebná data dotáhla. Řešení spočívá v použití kolekce s tzv. zpožděnou inicializací. List je konkrétní třída. Kdybychom však namísto List použili rozhranní IList, jež třída List implementuje, můžeme pro účely persistentních kolekcí v rámci našeho ORM používat vlastní třídu LazyList, jež požadované zpožděné načítání „naučíme“, ale klient o třídě LazyList nemusí nic vědět, protože s ním bude pracovat pomocí dobře známého rozhranní IList. IList + Indexer : T + Count : int + GetEnumerator () : Enumerator + Add (T obj) : void + Remove (int position) : void
List + Indexer : T + Count : int + <> GetEnumerator () : Enumerator + <> Add (T obj) : void + <> Remove (int position) : void
LazyList + + -
Indexer Count InnerCollection FkToLoad
: : : :
T int List FkProperty
+ <> GetEnumerator () + <> Add (T obj) + <> Remove (int position) r Initialize ()
: : : :
Enumerator void void int
Vlastní implementace třídy LazyList je přitom velmi jednoduchá. Stačí jen aby si kolekce „pamatovatovala“, který typ objektů a s jakou omezovací podmínkou má v případě potřeby inicializace načíst. Typ objektu je přitom dán již staticky pomocí generického atributu „” a omezující podmínku, můžeme předat už přímo konstruktoru naší kolekce při vytváření instance jejího vlastníka. Vlastní inicializace kolekce (tj. naplnění) se pak provede automaticky při prvním zavolání indexeru, getteru property Count nebo žádosti o navrácení enumeratoru a spočívá v naplnění kolekce InnerCollection objekty z DB. Implementace všech metod a vlastností pak spočívá pouze v kontrole, zde byla již kolekce inicializována (InnerCollection!=null) a následné delegaci na stejnojmennou vlastnost či metodu InnerCollection. Příklad implementace vlastnosti Count: public int Count { get { if(InnerCollection==null) Initialize();
return InnerCollection.Count; } }
Poznámky: Podobně jako jsme implementovali třídu ILazyList, bychom mohli zpožděné načítání implementovat i pro ostatní typy kolekcí (Set, Map, Bag) bylo by to potřeba. K vlastní inicializaci potřebuje kolekce objekt Session. Ten však díky vzoru Thread Local (viz 4.3) není třeba do kolekce předávat. Implementace inicializace se na ni odkazuje pomocí konstrukce Session.Current. Navíc pak nehrozí riziko, že by předaná Session, na kterou by si kolekce musela držet referenci přestala platit před inicializací kolekce.
5.3 Zpožděné načítání referencí na objekt Na podobném principu jako zpožděné načítání kolekcí funguje i načítání přímých odkazů z jednoho objektu na druhý. Tím může být třeba položka Book.Author z příkladu v kapitole 5.1.3. Řešení je ovšem oproti kolekcím o něco komplikovanější: 1. Jednak se naše persistentní objekty obvykle nedělí na interfacovou a implementační část jako tomu bylo u kolekcí 2. Operátory ==, is a as nemusí fungovat správně 3. Mezi persistentními objekty může existovat vztah inheritance (viz 6.4), který naše implementace pomocí vzoru proxy není schopna spolehlivě zohlednit. Jinými slovy, pokud pracujeme s transparentní proxy nadtřídy nějakého objektu, není možné tuto nadtřídu přetypovat na jejího potomka, i když skutečný potomek daného typu je. Řešení prvního problému má dvě varianty: Složitější, avšak objektové čistší (používá např. Hibernate), která spočívá v tom, že ze všech persistentních objektů extrahujeme interface a vygenerujeme proxy třídy. Tj. každá business entita bude reprezentována dvěma třídami a jedním interface. IDog
DogProxy
Dog 0..1 0..1
Nevýhodou tohoto řešení jak pak bezpodmínečná nutnost práce s objekty pouze pomocí jejich rozhranní. Rovněž všechny reference a kolekce v persistentním objektovém modelu musí s objekty pracovat pomocí jejich rozhranní, jinak použití vzoru proxy není možné. Druhé, ne tak čistě objektové, ale zato podstatně jednodušší řešení se opírá o možnost vlastní implementace accessorů pro jednotlivé položky persistentních tříd a pracujeme tak pouze s 1 třídou. Vlastní inicializaci objektu pak spustí pouhé dotázání se na některou z datových položek objektu. Např. kdyby třída Dog měla položku Name, její implementace by mohla vypadat takto: public string Name { get { if(!IsInitialized) Initialize(); return xxxxx; } set { if(!IsInitialized) Initialize(); xxxxx=value; } }
Druhý a třetí problém můžeme v jazycích C++ nebo C# vyřešit pomocí přetížení příslušných operátorů, nebo universálněji, definovat vlastní operátory jako statické metody nějaké pomocné třídy a pak na všech místech využívat této třídy místo původních operátorů: Původně myAnimal is Dog Dog d=(Dog)myAnimal myAnimal==myDog
V rámci persistentních objektů Operator.Is(myAnimal) Dog d=Operator.Cast(myAnimal) Operator.AreEqual(myAnimal, myDog)
5.4 Uvolňování paměti Pro systémy s omezeným rozsahem dat se uvolňováním paměti nemusíme zabývat. Pokud ale dojde k situaci, že se velikost dat držená v operační paměti začne blížit její velikosti, je nutné aby některá data byla z paměti uvolněna. Algoritmů k určení, která data se mají z cache vyřadit je hned několik. Jejich základní přehled je k dispozici např. na stránkách wikipedia: http://en.wikipedia.org/wiki/Cache_algorithms
My jsme zvolili LRU (least recently used) – nejméně používaný z hlediska posledního přístupu. Implementace je velmi jednoduchá – u každého objektu v paměti si držíme datum posledního přístupu a proces, který má pak čistění na starosti iteruje přes všechny cachované instance všech entit a vyřazuje záznamu podle určité podmínky. Informace jako „od kdy“ a „jak často“ se má čistící proces spouštět jsou můžeme vytáhnout do konfiguračního souboru ORM, stejně jako hloubku záběru, tj. podmínku jak dlouho může musí v paměti „ležet“ nepoužívaný objekt, aby mohl být čistícím procesem odstraněn.
Podle uvedeného příkladu by se čistící proces poprvé spustil po alokaci 100M a pak periodicky každé 2 minuty a mazal by objekty, které se kterými se nepracovalo po dobu 5ti a více minut. Vlastní odstranění objektů z paměti neprovádí čistící proces sám o sobě (neboť v jazyk se řízenou správou paměti to ani neumožňuje), ale stane se tak při první následující iteraci garbage collectoru. Aby garbage collector mohl daný objekt odstranit, nesmí na něj existovat reference ze žádného dalšího objektu, který nebude při v rámci dané iterace garbage collectoru také odstraněn. Vlastní „uvolnění“ objektu tedy provedeme tak, že: Zrušíme záznam o objektu v PK cache příslušné entity Odstraníme všechny klíče v index cache které se odkazují na daný objekt Všechny přímé reference z cizích objektů na objekt, který uvolňujeme uvedeme do stavu uninitialized Všechny výskyty uvolňovaného objektu v kolekcích vyměníme za proxy objekt se stavem uninitialized Vlastní instanci mazaného objektu nastavíme příznak „IsDisposed“, díky němuž znemožníme jeho možné pozdější předání metodě sess.Save() za předpokladu, že by na objekt existovala ještě nějaká reference z aplikace a nemohl být odstraněn. Pokud vše proběhne v pořádku, objekt bude při následující iteraci garbage collectoru odstraněn.
6 Mapování dědičnosti V této kapitole přímo navážeme na základy ORM mapování popsané v kapitole 4 a ukážeme si, co se děje pod pokličkou O-R mapperu rozšíříme-li náš datový model o možnost dědění.
6.1 Dědění persistentních objektů Vztah persistentní inheritance mezi dvěma a více třídami zavádíme tehdy, pokud mezi potomkem a jeho základní třídou, od které je zděděn existuje vztah který můžeme vyjádřit slovem JE. Např. pes JE zvíře, zaměstnanec JE osoba, čtenář JE osoba a zároveň s každou ze zděděných entit pracujeme v našem systému alespoň trochu odlišně. Např. nemá smysl zavádět dědičnost zvíře-pes, pokud vyvíjíme aplikaci pro evidenci psů. Na druhou stranu nevytváříme konkrétní podtřídy pro objekty, které nemají žádné speciální atributy, ani nevyžadují speciální chování. Např. dědičnost zvíře-pes by se určitě nehodila ani v systému pro evidenci zvířat, které žijí v lese, ačkoliv tam pes klidně žít může. Situace, ve kterých bychom naopak dědičnost měli použít, jsou ty kdy ve vašem systému existují vazby jak na předky tak i na potomky použitých tříd. Ukážeme se nyní trošku komplexnější příklad modelu knihovny, jehož řešení by bylo bez dědičnosti velmi nepřehledné a složité. LoanItem
Readable + Title : string
Loan 0..*
Person {abstract}
0..*
- ActionDate : DateTime
1..1
0..*
+ LendTo () : bool
+ Name : string
0..* LentBy
Magazine + IssueDate : DateTime
Book + Author : string 1..1 Employee + Login : string + Password : string + Salary : string
Reader + RegNum : string
V našem objektovém modelu knihovny si může půjčovat tiskoviny jak čtenář (Reader), tak zaměstnanec (Employee), přitom každý typ objektu má úplně jiné atributy. Naopak půjčovat knížky může pouze zaměstnanec (nikoliv čtenář), proto je vztah agregace LentBy mezi Loan a Employee (nikoliv Loan-Person) Jestě markantnější je vztah LoanItem – Readable. Každá osoba si totiž může půjčit jak Časopis (Magazine) tak knížku (Book), oba typy objektů však v reálném světě evidujeme
zvlášť a i jejich atributy se od sebe výrazně liší (např. časopis má datum vydání, ale nemá ISBN). Při použití persistentní dědičnosti můžeme s objekty pracovat pomocí jejich skutečného typu nebo pomocí jejich nadtypu (super class), od třídy, ze které jsou přímo nebo nepřímo odvozeny. Pokud budeme hledat osobu (Person) podle jména (Name), je dost možné, že v kolekci, kterou nám ORM framework vrátí budou „namíchány“ jak objekty typu Employee tak i několik objektů typu Reader. Pokud bychom ovšem podle jména hledali místo osob (Person) přímo zaměstnance (Employee), ve výsledné kolekci by už žádný čtenář nebyl. Poznamenejme zde pouze, že odfiltrovat objekty určité podtřídy od objektů nadtřídy (tj. např. hledat všechny osoby, které NEJSOU zaměstnanci) je operace velmi obtížná a většina ORM frameworků ji nepodporuje. Navíc pokud zjistíme, že daný typ hledání opravdu potřebujeme, je to jeden z prvních signálů, že bychom měli náš stávající objektový model refaktorovat.
6.2 Typy dědění V této podkapitole si probereme 2 základní typy dědění. Oba zde uvedené typy implementací persistetní inheritance jsou vzájemně kompatibilní, tj. se změnou typu persistence inheritance určité hierarchie se nemění objektový náš model. Jediné co se v tomto případě změní je model datový. Typ dědění nám tedy definuje, jakým způsobem budeme ukládat naši objektovou strukturu do struktury relační. Příklady jednotlivých implementací dědičnosti se budou odkazovat na tuto část modelu: Loan
Person 0..*
- ActionDate : DateTime
{abstract} 1..1
+ Name : string
0..* LentBy
1..1 Employee - Login : string - Password : string - Salary : string
Reader + RegNum : string
6.2.1 Table-per-class Při dědění table-per-class máme pro každou persistetní třídu právě jednu databázovou tabulku. V tabulkách připadajících podtřídám už neopakujeme atributy z obsažené v nadtřídě, ale pouze přidáváme ty atributy, o které daná podtřída svého předka rozšiřuje. ER diagram odpovídající schématu z části 6.2 by mohl vypadat takto: PERSON
LOAN PERSON_ID
LENT_BY
ID Number NAME Characters (256)
ID
EMPLOYEE LOGIN Characters (20) PASSWORD Characters (20) SALARY Number
ID
READER REG_NUM Characters (20)
Atributy EMPLOYEE.ID a READER.ID budou definovány jako primární klíč, ale zároveň i cizí klíč na odkazující se na entitu PERSON. Tím je už na databázové úrovni zajištěno, že pro jeden záznam v tabulce PERSON může existovat nejvýše jeden záznam v tabulce EMPLOYEE a READER. Vzhledem k tomu, že tabulky EMPLOYEE a READER obsahují pouze atributy, o které jejich třídy rozšiřují třídu Person, abychom dostali kompletní data, musí ORM framework při čtení entity EMPLOYEE (nebo READER) tabulku PERSON připojit. Dotaz do databáze pro vyhledání zaměstnanců určitého jména může tedy vypadat takto: SELECT p.*, e.* FROM EMPLOYEE e INNER JOIN PERSON p ON p.ID=e.ID WHERE NAME='XXXXX'
Vnitřní (INNER) join si v tomto případě můžeme dovolit, protože ke každému záznamu v tabulce EMPLOYEE musí existovat i záznam stejného ID v tabulce person (zajištěno pomocí cizího klíče). Zároveň se však nemusíme obávat, že by nám JOIN na entitu PERSON množil řádky, protože položka ID, je i primálrním klíčem (jehož hodnota se pochopitelně v rámci jedné entity nesmí opakovat). Složitější situace však nastane, pokud (např. opět podle jména) nyní nebudeme hledat určité zaměstnance, ale osoby (PERSON). ORM framework totiž musí načíst každý objekt do paměti celý, abychom se mohli z naší aplikace dotazovat na jeho typ a případně jej i přetypovat. Tj. typ objektu musí být znám už při čtení kolekce. Toho dosáhneme tak, že připojíme všechny tabulky možných konkrétních podtříd (v tomto případě dvě).
SELECT p.*,r.*,e.* FROM PERSON p LEFT OUTER JOIN EMPLOYEE e ON e.ID=p.ID LEFT OUTER JOIN READER r ON r.ID=p.ID WHERE NAME='XXXXX'
V tomto případě musíme použít OUTER JOIN, protože minimálně jeden z odpovídajících záznamu v tabulkách EMPLOYEE či READER nebude existovat. Podle přítomnosti pole ID na daných pozicích je potom ObjectReader (kapitola 4.8) schopen určit, jakou konkrétní persistentní třídu má použít pro vytvoření objektu. Pokud bude tedy výstup daného SQL např. takovýto: Person p ID NAME 22 JAN 54 PETR
Reader r ID REG_NUM 22 123456 NULL NULL
ID NULL 54
Employee e LOGIN PASS NULL NULL PETR heslo
SALARY NULL 45000
Bude vytvořena kolekce dvou objektů typu Person o dvou prvcích. Prvním prvkem (ID 22) bude objekt typu Reader a druhým (ID 54) objekt typu Employee. Výhodou implementace table-per-class je čistota návrhu. Už samotný datový model nám mimo jiné sám zajišťuje, že např. pro vytvoření objektu Loan musí existovat nějaký objekt Employee (knihovník, který nám knížku půjčí). Danou vlastnosti nám zajišťuje cizí klíč Loan.LentBy. Zápis objektu EMPLOYEE má pak záznamy ve 2 tabulkách - PERSON a EMPLOYEE (se stejným ID). Nevýhodou table-per-class je nutnost vždy připojovat tabulky všech podtříd, což může značně zpomalovat práci. Další problém může nastat pokud bychom chtěli např. pro entitu EMPLOYEE vytvořit složený index na dvojici sloupečků NAME a LOGIN. Datový model nám to totiž vzhledem k tomu že NAME se nachází v jiné tabulce (PERSON) než LOGIN (EMPLOYEE) neumožní.
6.2.2 Table-per-hierarchy Přístup table-per-hierarchy využívá pouze jednu tabulku pro celou hierarchií persistetních tříd. Daná tabulka musí obsahovat sjednocení množiny všech atributů všech tříd dané hierarchie. Schéma ze sekce 6.2 bychom v něm napamovali takto: LOAN
PERSON PERSON_ID
LENT_BY
ID DISCRIMINATOR NAME REG_NUM LOGIN PASSWORD SALARY
Number Characters (1) Characters (20) Characters (20) Characters (20) Characters (20) Number
Každý objekt třídy Person nebo její podtřídy se zapíše do tabulky PERSON tak, že se vyplní všechny atributy, které daný objekt má a ostatní zůstanou NULL. Aby ObjectReader (kapitola 4.8) poznal, o objekt které třídy se jedná, je zde další pole, tzv „Discriminator“, který v sobě obsahuje kód dané konkrétní třídy. Zápis dvou záznamů ID 22 a 54 by pak vypadal v tabulce PERSON takto: ID 22 54
DISCRIMINATOR P E
NAME JAN PETR
REG_NUM 123456 NULL
LOGIN NULL PETR
PASS NULL heslo
SALARY NULL 45000
Výhodou tohoto řešení je fakt, že data celé hierarchie (tj. objektů spolu souvisejících) máme „po hromadě“ v jedné tabulce a nepotřebujeme je spojovat se žádnou další tabulkou, což může znamenat i jisté zvýšení výkonu naší aplikace. Rovněž nebudeme mít problém vytvořit index na dvojici sloupečků rozdílných podtříd jako tomu bylo u table-per-class. Nevýhodou pak ovšem určitě bude nemožnost definovat cizí klíč přímo na určitou podtřídu. Např. Cizí klíč LENT_BY v tomto případě ukazuje na tabulku PERSON (nikoliv EMPLOYEE), což má za následek, že přirozené databázové mechanismy nemohou zabránit stavu, kdy položka LENT_BY nebude obsahovat ID objektu Employee, ale např. Reader. Další nevýhodou je pak spoustu nadbytečných položek s hodnotou NULL, které zabírají místo a navíc nesouvisí s daným primárním klíčem tabulky (ID), což odporuje normálním formám. Dodejme ještě, že některé ORM frameworky používají z důvodu kompatibility interních ResultSetů namísto discriminatoru sloupec ID před začátkem sady atributů odpovídající určité třídě přesně tak, jako domu bylo u příkladu ResultSetu pro table-per-class ( kap. 6.2.1)
6.3 Vrstva oddělující logický a fyzický datový model Kromě dvou výše uvedených způsobů implementace persistentní inheritance existuje i několik dalších (jako je např. table-per-concrete class, nebo různé kombinace již zmíněných typů). Aby mohl ORM framework podporovat všechny typy persistence pro všechny databázové stroje, je vhodné si vytvořit další aplikační vrstvu přímo na úrovni dané DB. Jedná se o API, které poskytuje pouze základní metody pro čtení a zápis dat do DB (nikoliv O-R mapping) nezávisle na použitým typu dědičnosti. Pro čtení dat se jako nejlepší kanditát osvedčily pohledy (VIEWS), pro zápis dat pak uložené procedury. Pro každou persistentní třídu tedy bude existovat právě jeden pohled (V_XXXX) a jedna uložená procedura (SET_XXXX). Ani jeden typ objektu pochopitelně nemusíme vytvářet
ručně, ale vytvoříme si na ně šablonu stejným způsobem jako jsme v kapitole 4.2 vyvářely kód pro vytvoření schématu. View V_XXXX pak obsahuje SELECT příkaz, jehož výstup je pro danou nezávislý na způsobu persistence hierarchie PERSON: Pro table-per-class: CREATE OR REPLACE VIEW V_PERSON AS SELECT * FROM PERSON t LEFT OUTER JOIN EMPLOYEE ON EMPLOYEE.ID=t.ID LEFT OUTER JOIN READER ON READER.ID=t.ID
Pro table-per-hierarchy (z ID před každou skupinou sloupců místo discriminatoru): CREATE OR REPLACE VIEW V_PERSON AS SELECT * FROM PERSON t
Podobným způsobem pak můžeme naimplementovat i uložené procedury SET_XXXX, jejíž vstupní parametry budou očekávané nové hodnoty daného objektu: PROCEDURE SET_EMPLOYEE ( p_id IN OUT int, p_name IN varchar2, p_login IN varchar2, p_password IN varchar2, p_salary IN int ) IS BEGIN //zjednodusena verze – pouze insert INSERT INTO PERSON VALUES (S_PERSON.nextval, p_name) RETURNING ID INTO p_id; INSERT INTO EMPLOYEE VALUES (p_id , p_login, p_password, p_salary); END ;
Aplikační část ORM pak nemusí o použité metodě řešení inheritance nic vědět. Další nespornou výhodou tohoto přístupu je, že pokud se někdo někdy rozhodně přistupovat k do naší aplikace pomocí SQL, je díky pohledům odstíněn od implementačních detailů a pokud bude zapisovat pouze pomocí SET_XXXX procedur, je daleko menší pravděpodobnost, že v DB vytvoří data, která budou pro ORM nekonzistentní. Zdůrazněme však, že všechny externí aplikace, které komunikují se systémem založeným na ORM by měly by tak měly činit výhradně prostřednictvím aplikačního rozhranní (kapitola 3.5). Přístup přímo do databáze pomocí SQL by měl být využit pouze v opravdu vyjímečných případech, kdy již pro danou situaci neexistuje žádné další alternativní řešení (XML, WebService).
6.4 Nepersistentní dědění Nyní se přesuňme zpět ze světa databází k naší aplikační části ORM.
Jak jsme si již uvedli v kapitolách 3.3 a 4.1, třída je ve FORIS.ORM chápána jako persistentní pokud splňuje obě požadovaná kritéria: Je potomkem třídy DBObject Obsahuje vlase atribut [DataEntity] Pokud třída nesplňuje některé z těchto kritérií, je chápána jako nepersistentní a její instance nemohou být uloženy do databáze pomoci metody sess.Save(). Co když ale oddědíme od nepersistentní třídy třídu persistentní? DBObject
DBObject
IAnimal
Animal + Race : string
[DataEntity]
[DataEntity]
Dog
Dog
+ Name : string
+ Name : string
Na uvedeném schématu vidíme, že třída Animal, ani rozhranní IAnimal nemají atribut [DataEntity] a proto nejsou chápány jako persistentní. Je tedy zřejmé, že pro třídu Animal (ani rozhranní IAnimal) nebude existovat v DB žádná speciální entita. Oproti tomu třída Dog by svůj „obraz“ v databázi mít měla. Vztah mezi nepersistentní třídou, které je nepersistentní a persistentní třídou nazýváme nepersistentní dědičnost. Především druhý způsob nepersistentní dědičnosti (implementace určitého rozhranní – např. IAnimal) má svoje opodstatnění. Dovoluje nám totiž definovat náš objekt v aplikačním kontextu jako součást ještě jiné hierarchie než DBObject (ze kterého dědit musíme). Navíc pokud nám dané rozhraní definuje nějakou vlastnost (property) můžeme si pro každou jeho implementující třídu zvolit, zda bude daná vlastnost persistentní, či se bude pouze počítat z nějakých již existujících atributů. V aplikaci pak můžeme pracovat s objekty, které vůbec nejsou součástí stejné persistentní hierarchie tak, jako by byly.
Je však třeba si uvědomit, že drtivá většina ORM frameworků v rámci persistentního objektového modelu nedovoluje odkazovat se na nepersistentní položky jako je tomu na tomto příkladě:
DBObject
[DataEntity] Animal + Race : string
Person 0..* 0..1
- Name : string
[DataEntity] Dog + Name : string
Na uvedeném příkladu je třída Animal nepersistentní, nemá tedy svůj obraz v databázi a proto se na ní nemůže odkazovat žádná persistentní třída (v našem případě Person).
7 Praktické využití ORM frameworku V této kapitole si uvedeme příklad návrhu objektového modelu knihovny a jeho realizaci pomocí nástroje FORIS.ORM. Zároveň si ukážeme si DDL kód, který ORM framework vygeneruje za nás. Vycházet budeme z komplexního modelu knihovny z kapitoly 6. LoanItem
Readable + Title : string
Loan 0..*
Person {abstract}
0..*
- ActionDate : DateTime
1..1
0..*
+ LendTo () : bool
+ Name : string
0..* LentBy
Magazine + IssueDate : DateTime
Book + Author : string 1..1 Employee + Login : string + Password : string + Salary : string
Reader + RegNum : string
Knihovna vede informace o 2 typech tiskovin (Readable) – Knížku (Book), pro kterou eviduje její název (Title) a jméno autora (Author) a časopis (Magazine), u kterého eviduje název (Title) a datum vydání (IssueDate). Na druhém konci systém „zná“ zaměstnance (Employee) a čtenáře (Reader). Sjednocením všech zaměstnanců a čtenářů dostáváme další množinu – Osoba (Person), obsahující společný atribut jméno (Name). Každá osoba může několikrát navštívit knihovnu (každá návštěva = instance třídy Loan) a půjčit si několik knížek (vazba M:N přes asociační třídu LoadItem) Systém musí umět vyhledávat čtenáře podle jména a registračního čísla a tiskoviny podle názvu, proto by bylo vhodné na příslušných sloupcích povytvářet indexy. U třídy Book můžete vidět využití validátoru, který je rozebrán v kapitole 3.4.2. Vlastní kód v C#, který je potřeba k vytvoření výše uvedeného modelu je
8 Závěr Téma své bakalářské práce „Mapování dat mezi objektovými programovacími jazyky a relačními databázemi“ jsem si zvolil proto, abych demonstroval rozdíly relačními a objektovými přístupy k datům při vytváření informačního systému. Pro mnoho vývojových týmů představují objektové technologie zajímavou, nicméně zbytečně složitou a výkonově náročnou alternativu k již dobře známých a „zaběhnutým“ přístupům pomocí jazyka SQL. Síla objektového návrhu se ovšem neprojeví u malých aplikací typu „redakční systém internetového magazínu“, ale ve velkých podnikových systémech, na jehož vývoji se podílí velké množství lidí. Tito lidé totiž nemusí být nutně programátoři, ale přesto pro svoji práci potřebují znát alepoň hrubý model naší aplikace. Relační model je velmi vzdálen od skutečnosti a pro zaměstnance, který nad žádnou databází nikdy nic neprogramoval (např. testera nebo project managera) je velmi obtížné jej pochopit. Naproti tomu objektový model je mnohem bližší skutečnosti, obsahuje daleko méně různých pomocných entit a tím je i pro zaměstnance „neprogramátory“ srozumitelnější. Otázkou však zůstává, kdy je výhodnější použít stávající technologie, a kdy se pustit do vývoje něčeho nového. Pokud byste se někdy ocitli v podobné situaci, doufám, že vám bude moje bakalářská práce přínosem. Většina ORM frameworků funguje velmi podobně a dokonce pokud byste se rozhodli decompilovat jejich zdrojové kódy, nejspíš byste zjistili, že i jejich vnitřní objektová struktura je dosti podobná. Původně jsem měl v úmyslu tuto práci více postavit na srovnání již existujících ORM frameworků a vypíchnutí jejich silných a slabých míst, ale zjistil jsem, že takových textů je na internetu opravdu dost (jako např. http://www.howtoselectguides.com/dotnet/ormapping/) a vytvářet něco, co již existuje se mi nechtělo. Kdyby však přece jen někoho zajímalo, který konkrétní ORM framework podporuje kterou jakou feature, v kapitole „Použitá literatura“ URL adresy webů, které dané tabulky obsahují. Mým cílem však bylo vysvětlit kdy ORM framework využijeme a jakým způsobem zhruba funguje. Ukázali jsme se na příkladech jaká úskalí nás mohou při vývoji aplikace postavené na relačním datovém modelu čekat a proč bychom se měli přímým přístupům pomocí SQL v dnešních systémech spíše vyhnout. Probral jsem i několik alternativ, kde můžeme náš objektový model držet a popsal jsem i způsob, jak z něj vygenerovat databázové schéma nezávisle na použitém RDBMS. Vysvětlil jsem i jak vám může ORM framework usnadit práci cachováním dat a podívali jsme se „pod pokličku“ zpožděné inicializace kolekcí a odkazů na persistentní objekty. V závěru jsem zavedl pojem persistentní dědičnost a ukázal dva základní způsoby jejího namapování do relačního modelu. V textu jsem se snažil klást spíše důraz na praktické problémy, se kterými se každý tvůrce ORM musí vypořádat, než vysvětlovat základní pojmy z teorie relačních datábází a objektového programování, které jsou však pro pochopení některých kapitol nezbytné.
Bohužel mi již nezbyl prostor pro některá pokročilejší témata jako objektový dotazovací jazyk či automatická synchonizace datového modelu po změně objektového. Tato a další témata si nechám pro svoji diplomovou práci. Jako svůj největší úspěch mohu ohodnotit skutečnost, že O-R mapper, na jehož vývoji jsem se během psaní této bakalářské práce značnou měrou podílel byl úspěšně nasazen u zákazníka jako součást řešení telekomunikačního systému FORIS NG 4.1.
9 Seznam literatury Bauer, C., King, G. Hibernate in Action. Manning Publications, březen 2004. 400 s. ISBN: 193239415 Merunka, V. Datové Modelování. 1.vyd. Praha: Alfa Publishing, 2006. 180 s. ISBN 8086851-54-0 Kraval, I. Objektové modelování v praxi za pomoci UML, elektronické vydání Gamma, E., Helm, R., Johnson, R.,Vlisside, J. Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley, říjen 1995, ISBN 0201633612 Webové zdroje: www.hibernate.org – projektová dokumentace + zdrojové kódy www.howtoselectguides.com/dotnet/ormapping/tables/ www.sweb.cz/pichlik portal.acm.org/citation.cfm?id=1216263&dl=ACM&coll=portal&CFID=15151515&CF TOKEN=6184618 www.objectmatter.com/vbsf/docs/maptool/ormapping.html www.theserverside.com
10 Přílohy Příloha A - Ukázka implementace generátoru kódu Následující metoda v jazyce C# provede „rozvinutí“ šablony generátoru podle metadat objektového modelu tak jak je to popsáno v kapitole 4.2. /// <summary> /// Evaluates specified template for generator /// /// <param FkName="obj">current root object of meta model /// <param FkName="template">template to evaluate /// public static string EvaluateTemplate(object obj, string template) { string expression; while ((expression = GetFirstExpression(template, "$")) != null) { template = ReplaceFirst(template, "$[" + expression + "]", EvaluateExpression(obj, expression)); } return template; } /// <summary> /// Possible expressions: /// - Name /// - Name[ExpressionToRepeat] /// - Name/conditionalAttribute[ExpressionToRepeat] /// /// <param name="obj"> /// <param name="exp"> /// public static string EvaluateExpression(object obj, string exp) { ExpressionLanguage el = ExpressionLanguage.GetInstance(); string repeat = GetFirstExpression(exp, ""); if (repeat == null) return el.GetObjectValue(obj, exp); string itemName = exp.Substring(0, exp.IndexOf("[")); string condition = null; int conditionStart = itemName.IndexOf("/"); bool invertCondition = false; if(conditionStart>0) { condition = itemName.Substring(conditionStart + 1); itemName = itemName.Substring(0, conditionStart); if(condition.StartsWith("^")) { condition = condition.Substring(1); invertCondition = true; } } if(itemName.StartsWith("^")) { invertCondition = !invertCondition; itemName = itemName.Substring(1); } string separator = exp.Substring(exp.LastIndexOf("]") + 1); object evaluated = el.GetObject(obj, itemName);
if (evaluated is IEnumerable) { IEnumerable collection = (IEnumerable)el.GetObject(obj, itemName);
StringBuilder sb = new StringBuilder(); bool first = true; if(collection is IDictionary) { collection = new Hashtable((IDictionary) collection).Values; } foreach (object innerObj in collection) { if (condition != null) { bool passes = Convert.ToBoolean(EvaluateExpression(innerObj, condition)) ^ invertCondition; if (!passes) { continue; } } if (!first) sb.Append(separator); if (repeat.Length > 0) sb.Append(EvaluateTemplate(innerObj, repeat)); else sb.Append(innerObj.ToString()); // first = false; } return sb.ToString(); } else { return Convert.ToBoolean(evaluated)^invertCondition? EvaluateTemplate(obj, repeat):""; } } public static string ReplaceFirst(string source, string original, string replacement) { int i = source.IndexOf(original); if (i < 0) throw new Exception("Expression '" + original + "' hasn't been found in '" + source + "'"); return source.Substring(0, i) + replacement + source.Substring(i + original.Length); } public static string GetFirstExpression(string template, string prefix) { int start = template.IndexOf(prefix + "["); if (start < 0) return null; start += 1 + prefix.Length; int endIndex = start; int level = 1; while (level > 0) { if (endIndex >= template.Length) throw new Exception("Parse error in '" + template.Substring(start prefix.Length - 1) + " - terminating ']' character is missing."); char ch = template[endIndex++]; if (ch == '[') { level++;
} if (ch == ']') { level--; } } return template.Substring(start, endIndex - start - 1); }
Příloha B - Příklad definice modelu knihovny pomocí FORIS.ORM namespace FORIS.ORM.Example { [DataEntity] public abstract class Person:DBObject { private string lastName; [DataAttribute] [DataIndex("PERSON_NAME")] public string LastName { get { return lastName; } set { lastName = value; } } private string firstName; [DataAttribute] [DataIndex("PERSON_NAME")] public string FirstName { get { return firstName; } set { firstName = value; } } private DateTime dateOfBirth; public DateTime DateOfBirth { get { return dateOfBirth; } set { dateOfBirth = value; } } } [DataEntity] public class Reader : Person { private string registrationNumber; [DataAttribute] [DataIndex("REG_NUMBER")] public string RegistrationNumber { get { return registrationNumber; } set { registrationNumber = value; } } } [DataEntity]
public class Employee : Person { private private private private
string employeeNumber; string position; int salary; long parentId;
[DataAttribute(typeof(Employee))] public long ParentId { get { return parentId; } set { parentId = value; } } [DataAttribute()] [DataIndex] public string EmployeeNumber { get { return employeeNumber; } set { employeeNumber = value; } } [DataAttribute()] public string Position { get { return position; } set { position = value; } } [DataAttribute()] public int Salary { get { return salary; } set { salary = value; } } } [DataEntity] public class Loan : DBObject { private long personId; private long lentBy; [DataAttribute(typeof(Person),ExpirationType.CUSTOM)] public long PersonId { get { return personId; } set { personId = value; } } [DataAttribute(typeof(Employee),ExpirationType.CUSTOM)] public long LentBy { get { return lentBy; } set { lentBy = value; } } } [DataEntity] public class LoanItem : DBObject { private long loanId; private long readableId ;
[DataAttribute(typeof(Loan),ExpirationType.CUSTOM)] public long LoanId { get { return loanId; } set { loanId = value; } } [DataAttribute(typeof(Readable))] public long ReadableId { get { return readableId; } set { readableId = value; } } } [DataEntity] public class Readable:DBObject { private string title; private int price; [DataAttribute(DefaultValue = "unknown")] [DataIndex("TITLE")] public string Title { get { return title; } set { title = value; } } } [DataEntity] public class Book : Readable, INotifyHandler { private string author; private string isbn; private int notified = 0; public int Notified { get { return notified; } }
//author's name must not longer than 50 characters [Validation(typeof(RegexValidator),"^.[0-50]$")] [DataAttribute] public string Author { get { return author; } set { author = value; } } [DataIndex("BOOK_ISBN")] [DataAttribute] public string Isbn { get { return isbn; } set { isbn = value; } } private static Random rnd = new Random();
public void Notify(DBObject obj) { notified++; } } [DataEntity(Notify=true)] public class Magazine : Readable { private DateTime dateIssued; [DataAttribute(IsMutable = false)] public DateTime DateIssued { get { return dateIssued; } set { dateIssued = value; } } } } Generovaný SQL kód: -------------------------------------------- Generated schema for CM ---------------------------------------------------------------------------------- Generated schema for class Book --------------------------------------CREATE TABLE BOOK ( ID int not null CONSTRAINT pk_BOOK PRIMARY KEY , AUTHOR varchar2(255) , ISBN varchar2(255) ); ---------------------------------------- Generated schema for class Employee --------------------------------------CREATE TABLE EMPLOYEE ( ID int not null CONSTRAINT pk_EMPLOYEE PRIMARY KEY , PARENT_ID int , EMPLOYEE_NUMBER varchar2(255) , POSITION varchar2(255) , SALARY int ); ---------------------------------------- Generated schema for class Loan --------------------------------------CREATE TABLE LOAN ( ID int not null CONSTRAINT pk_LOAN PRIMARY KEY , START_DATE date NOT NULL, END_DATE date , PERSON_ID int , LENT_BY int ); ---------------------------------------- Generated schema for class LoanItem --------------------------------------CREATE TABLE LOAN_ITEM ( ID int not null CONSTRAINT pk_LOAN_ITEM PRIMARY KEY , START_DATE date NOT NULL, END_DATE date , LOAN_ID int ,
READABLE_ID int ); ---------------------------------------- Generated schema for class Magazine --------------------------------------CREATE TABLE MAGAZINE ( ID int not null CONSTRAINT pk_MAGAZINE PRIMARY KEY , DATE_ISSUED date ); ---------------------------------------- Generated schema for class Person --------------------------------------CREATE TABLE PERSON ( ID int not null CONSTRAINT pk_PERSON PRIMARY KEY , START_DATE date NOT NULL, END_DATE date , LAST_NAME varchar2(255) , FIRST_NAME varchar2(255) ); ---------------------------------------- Generated schema for class Readable --------------------------------------CREATE TABLE READABLE ( ID int not null CONSTRAINT pk_READABLE PRIMARY KEY , START_DATE date NOT NULL, END_DATE date , TITLE varchar2(255) ); ---------------------------------------- Generated schema for class Reader --------------------------------------CREATE TABLE READER ( ID int not null CONSTRAINT pk_READER PRIMARY KEY , REGISTRATION_NUMBER varchar2(255) ); -------------------------------------------- Generated foreign keys and indexes ------------------------------------------ALTER TABLE EMPLOYEE add CONSTRAINT fk_EMPLOYEE_PARENT_ID FOREIGN KEY (PARENT_ID) REFERENCES EMPLOYEE (ID); CREATE INDEX i_EMPLOYEE_PARENT_ID on EMPLOYEE(PARENT_ID); ALTER TABLE LOAN add CONSTRAINT fk_LOAN_PERSON FOREIGN KEY (PERSON_ID) REFERENCES PERSON (ID); CREATE INDEX i_LOAN_PERSON on LOAN(PERSON_ID); ALTER TABLE LOAN add CONSTRAINT fk_LOAN_LENT_BY FOREIGN KEY (LENT_BY) REFERENCES EMPLOYEE (ID); CREATE INDEX i_LOAN_LENT_BY on LOAN(LENT_BY); ALTER TABLE LOAN_ITEM add CONSTRAINT fk_LOAN_ITEM_LOAN FOREIGN KEY (LOAN_ID) REFERENCES LOAN (ID); CREATE INDEX i_LOAN_ITEM_LOAN on LOAN_ITEM(LOAN_ID); ALTER TABLE LOAN_ITEM add CONSTRAINT fk_LOAN_ITEM_READABLE FOREIGN KEY (READABLE_ID) REFERENCES READABLE (ID); CREATE INDEX i_LOAN_ITEM_READABLE on LOAN_ITEM(READABLE_ID); ALTER TABLE MAGAZINE_COMPOSITE add CONSTRAINT fk_MAGAZINE_COMPOSITE_MAGA201 FOREIGN KEY (MAGAZINE_ID) REFERENCES MAGAZINE (ID); ----------------------------------------------------- Foreign keys between tables joined by inheritance ---------------------------------------------------ALTER TABLE BOOK add CONSTRAINT fi_BOOK FOREIGN KEY (ID) REFERENCES READABLE (ID); ALTER TABLE EMPLOYEE add CONSTRAINT fi_EMPLOYEE FOREIGN KEY (ID) REFERENCES PERSON (ID);
ALTER TABLE MAGAZINE add CONSTRAINT fi_MAGAZINE FOREIGN KEY (ID) REFERENCES READABLE (ID); ALTER TABLE READER add CONSTRAINT fi_READER FOREIGN KEY (ID) REFERENCES PERSON (ID); -------------------------------------------------------- Generated sequences for base tables ---------------------------------------------------------------------CREATE SEQUENCE S_LOAN ; CREATE SEQUENCE S_LOAN_ITEM ; CREATE SEQUENCE S_PERSON ; CREATE SEQUENCE S_READABLE ; CREATE INDEX i_BOOK_ISBN on BOOK(ISBN); CREATE INDEX i_EMPLOYEE_NUMBER on EMPLOYEE(EMPLOYEE_NUMBER); CREATE INDEX i_PERSON_NAME on PERSON(LAST_NAME,FIRST_NAME); CREATE INDEX i_TITLE on READABLE(TITLE); CREATE INDEX i_REG_NUMBER on READER(REGISTRATION_NUMBER);
Příloha C - Implementace Regex valitátoru /// <summary> /// Regular expression validator. Checks specified value against given regular expression /// public class RegexValidator:IValidator { private string Expression; public object[] Parameters { set { if (value.Length != 1 || !(value[0] is string)) { throw new OrmException("Regexp validator requires exacly one string parameter (expression), but set " + value.Length); } Expression = (string)value[0]; } } public void Validate(object value) { if(value==null) //nullable fiels allways pass { return; } if(!Regex.IsMatch(value.ToString(), Expression)) { throw new ValidationException(string.Format("'{0}' does not match pattern '{1}'",value,Expression)); } } }