česká zemědělská univerzita

Transkript

česká zemědělská univerzita
Č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<Book>(5);
ormSess.GetAllInstances<Book>();
ormSess.GetByReferences<LoadItem, Book>(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<Book> books=ormSession.GetAllInstances<Book>();
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>
<class name="FORIS.ORM.Example.Book" table="BOOK">
<id name="Id" column="ID" type="System.Int32">
<generator class="sequence">
<param name="sequence">S_BOOK</param>
</generator>
</id>
<property name="Title" column="TITLE" type="java.lang.String" />
<property name="Author" column="AUTHOR_ID"
type="FORIS.ORM.Example.Author" />
</class>
</mapping>
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>
<class name="FORIS.ORM.Example.Book"
table="BOOK">
<id name="Id" column="ID"
type="System.Int32">
<generator class="sequence">
<param name="sequence">S_BOOK
</param>
</generator>
</id>
<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
/// </summary>
/// <param name="user">Identification of user who requested
given update</param>
/// <returns>true if saving is handled inside BeforeSave
method, otherwise false</returns>
public virtual bool BeforeSave(UserIdentifier user) {
return false;
}
/// <summary>
/// Executed after object is stored into DB
/// Useful for modifications after object is saved
/// </summary>
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
<code>[Validation(validatorType,params)]</code> must implements this
interface.
/// </summary>
public interface IValidator
{
/// <summary>
/// If validation fails, an exception should be thrown,
otherwise nothing should be done.
/// </summary>
/// <param name="value">value of specified property</param>
void Validate(object value);
/// <summary>
/// parameter(s) for validator (such as reference table name or
regexp). Write only property initialized when validator is created.
/// </summary>
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
+ <<Implement>> 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<Book>(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<Book>(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<Book>(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
+
+
+
+
<<Implement>>
<<Implement>>
<<Implement>>
<<Implement>>
BuildEntities ()
BuildAttributes ()
BuildReferences ()
BuildInheritance ()
BuildEntities ()
BuildAttributes ()
BuildReferences ()
BuildInheritance ()
:
:
:
:
void
void
void
void
ServiceMetamodelBuilder
AttributeMetamoderBuilder
:
:
:
:
void
void
void
void
+
+
+
+
<<Implement>>
<<Implement>>
<<Implement>>
<<Implement>>
BuildEntities ()
BuildAttributes ()
BuildReferences ()
BuildInheritance ()
:
:
:
:
void
void
void
void
+
+
+
+
<<Implement>>
<<Implement>>
<<Implement>>
<<Implement>>
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
<pi> Number
<M>
TITLE
Characters (256)
PK <pi>
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<T>(long id) můžeme i načítat stav objektu:
Book book = sess.Get<Book>(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<Book> books=sess.GetByIndex<Book>("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
<pi> Number
<M>
NAME
Characters (256)
PK <pi>
ID
<pi> Number
<M>
NAME
Characters (256)
AUTHOR_ID
Number
PK <pi>
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<Book>(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<Author>(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<Book>(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<Book>(5);
Book theSameBook = sess.Get<Book>(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<T>(int id)
{
DBObject obj=sess.Get<T>(id);
obj.LastModification = DateTime.Now;
sess.Save(obj);
}
public void UpdateBookTitle(int id, string title)
{
Book aBook=sess.Get<Book>(id);
aBook.Title = title;
UpdateLastAccess<Book>(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<long,
DBObject> (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>("book_title", "Krtecek")[0];
a
Book b2=sess.Get<Book>(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<IndexKey, IndexValues>
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<Book>
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<Book> 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<Book> Books
Krteček & kalhotky:Book
List<Reader> Readers
Karel:Reader
Krteček & autíčko:Book
List<Reader> Readers
Jana:Reader
Pepa:Reader
List<Book> 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<T> je konkrétní třída. Kdybychom však namísto List<T> použili rozhranní
IList<T>, jež třída List<T> implementuje, můžeme pro účely persistentních kolekcí
v rámci našeho ORM používat vlastní třídu LazyList<T>, 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<T>.
IList<T>
+ Indexer : T
+ Count
: int
+ GetEnumerator ()
: Enumerator<T>
+ Add (T obj)
: void
+ Remove (int position) : void
List<T>
+ Indexer : T
+ Count
: int
+ <<Implement>> GetEnumerator ()
: Enumerator<T>
+ <<Implement>> Add (T obj)
: void
+ <<Implement>> Remove (int position) : void
LazyList<T>
+
+
-
Indexer
Count
InnerCollection
FkToLoad
:
:
:
:
T
int
List<T>
FkProperty
+ <<Implement>> GetEnumerator ()
+ <<Implement>> Add (T obj)
+ <<Implement>> Remove (int position)
r
Initialize ()
:
:
:
:
Enumerator<T>
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 „<T>” 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<Dog>(myAnimal)
Dog d=Operator.Cast<Dog>(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.
<add name="LastAccess" cleanerActivation="100M"
cleanerDepth="0.00:05:00" cleanerPeriod="0.00:02:00" />
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
/// </summary>
/// <param FkName="obj">current root object of meta model</param>
/// <param FkName="template">template to evaluate</param>
/// <returns></returns>
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]
/// </summary>
/// <param name="obj"></param>
/// <param name="exp"></param>
/// <returns></returns>
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
/// </summary>
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));
}
}
}

Podobné dokumenty

certificate - Profiltr.cz

certificate - Profiltr.cz Brno.s.r.o. Jihlavsk6 2,664 41 Troubsko, CzechRepublic processes forthefollowing r

Více