Objektum perzisztencia és objektum relációs modell
Konstantinusz Kft. © 2009
Tartalomjegyzék Bevezetés ....................................................................................................3 Mi a perzisztencia? ........................................................................................3 Aktív és passzív programok ............................................................................4 Miért gond az objektum perzisztencia megvalósítása? ........................................5 Perzisztencia formái ......................................................................................9 Megvalósítás formái: interfész-objektum vs betöltés/mentés .............................12 Hibernálás ..........................................................................................................13 Perzisztens tárolás ...............................................................................................22
Utószó .......................................................................................................29
Bevezetés Gyakori, és sokszor problémás téma a programozás világában az a terület, ami az objektumorientált modellek perzisztenciájával foglalkozik. Gyakori amiatt, hogy szinte alig lehet objektum-orientált rendszereket megvalósítani úgy, hogy ne találkoznánk ezzel az akadállyal, és problémás abból a szempontból, hogy legtöbb programozási nyelv nem, vagy csak közvetett módon nyújt eszközöket a kezelésére. Koncepciók, keretrendszerek... ahány rendszer, szinte annyi megoldás. Ez persze a legfájdalmasabban a kezdő programozót érinti, aki óhatatlanul, és gyanútlanul belefut ebbe a problémába. A gond az, hogy az objektum perzisztencia sokkal problémásabb dolog, mint azt a legtöbben gondolják. A helyzetet csak rontja, hogy kézzel fogható, egyértelmű megoldás nem nagyon kínálkozik, de többnyire még a probléma természete sem körvonalazódik igazán azok előtt akik először szembesülnek vele. Ezért szeretnék ebben a tanulmányban betekintést, és áttekintést nyújtani ebben a témában, beleértve azt területet is, ami az objektumok adatbázisban való tárolásával foglalkozik, vagyis az Objektum Relációs Modell (röviden ORM). Olyan olvasóknak nyújthat új ismereteket, akik már magabiztosan használják az objektumorientált programozást, és jártasak a relációs adatbázisokban is. Főként ajánlom ezt a témát a webes programozás iránt érdeklődőknek, mivel ott az objektum perzisztencia problémája elkerülhetetlenül, és sokkal kifejezettebben jelentkezik, mint bárhol máshol.
Mi a perzisztencia? A szó jelentése megmaradás, megmaradó. A mi esetünkben ez az objektumok "megmaradása". Ezzel problémával akkor kell foglalkoznunk, ha az objektumainknak a program futásán túl (vagy legalábbis az objektum modell élettartamán túl) is meg kell maradniuk, majd később újra visszaállíthatónak kell lenniük. Ahhoz hogy egy objektum, illetve egy egész objektum modell maradéktalanul visszaállítható legyen, több információt is tudnunk kell róla.
Az első, az állapota. Ez gyakorlatilag az összes attribútumának az aktuális értékét jelenti. A második, az identitása, vagyis hogy melyik osztálynak melyik példányáról van szó. A harmadik, amit a legtöbben elfelejtenek, vagy összekevernek az objektum állapotával, az objektum kapcsolatai, illetve függőségei. Ez a legtöbb nyelvben, implementációs szinten úgy jelenik meg, mint az objektumok közti referenciák. Ha egy objektumnak van olyan attribútuma, amely referencia egy másik objektumra, akkor az egy függőség, vagy kapcsolat. Ezt mind ki kell tudni menteni és vissza is kell tudni állítani. Nyilván kimentés alatt azt értjük, hogy ezeket az adatokat kiírjuk lemezre, vagy bárhol máshol, a programon kívül tároljuk, majd felhasználjuk amikor újra kell építeni az objektumokat. De mikor van szükség perzisztencia megvalósítására? Milyen helyzetekben szükséges ez? A helyzet az, hogy több fajtája is van az objektum perzisztenciának, és nagyon fontos, hogy értsük a különbséget ezek között, mivel teljesen más koncepcionális problémákat oldanak meg, és megvalósításukban is vannak komoly különbségek. Mielőtt ezt részletesen megvizsgálnánk, nézzünk meg egy lényeges szempontot ami nagy mértékben befolyásolja, hogy milyen típusú perzisztencia problémákkal találkozhatunk.
Aktív és passzív programok Osszuk a programokat két csoportra. Ezekkel már nyilván mindenki találkozott, legfeljebb csak nem gondolt rá ebben a formában. Aktív és passzív programok. A kettő közül az aktív programok a legtriviálisabbak mindenki számára. Ezek azok a programok amik folyamatosan futnak (vagy tudnak futni), és eközben folyamatosan fogadnak bemenetet, produkálnak kimenetet. Ide tartozik gyakorlatilag bármilyen asztali (desktop) alkalmazás, amit szoktunk használni, vagyis szövegszerkesztő, médialejátszó, illetve ami a talán a legegyértelműbb példa: játékok. Ezekben az esetekben a perzisztencia a munkamenet, vagy az éppen szerkesztett projekt, vagy dokumentum kimentéseként jelentkezik. A másik csoport a passzív programok. Ezek kevésbé vannak szem előtt, közvetlenül nem is nagyon találkozunk velük az asztali alkalmazások között. Különbség az aktívakhoz képest az, hogy ezek programok nem hivatottak határozatlan ideig futni. Van egy véges feladatuk, azt elvégzik, esetleg produkálhatnak egy egyszeri kimenetet, aztán véget ér a futásuk. A világ, ami teljes mértékben ilyen programokra épül, az a Web. Itt ha tetszik ha nem, muszáj lesz számolni a perzisztenciával, már csak abból az okból kifolyólag is, hogy a webes szkriptek passzívak.
Emiatt webes alkalmazásoknál a munkamenet állapotát (ha van) menteni kell minden lekérés után. Az is szinte evidens, hogy a webes alkalmazások adatbázisokban tárolják az adataikat.
Miért gond az objektum perzisztencia megvalósítása? A példa kedvéért vegyünk egy aktív programot, mondjuk egy autós játékot. Itt elég kézenfekvő, hogy miben nyilvánul meg a perzisztencia. Egy játékot el kell tudni menteni, aztán később visszatölteni, hogy ugyanonnan folytassuk. Ehhez persze mindent menteni kell, tehát hogy hol van az autónk, merre megy, milyen sebességgel, be van-e kapcsolva a rádió, le van-e húzva az ablak, és így tovább minden objektumot amiből a világ áll. Ha eljárás-orientáltan gondolkodnánk, akkor írni kellene egy algoritmust, ami végigmegy mindenen amit ki kell menteni, összeszed minden adatot, és valamilyen formában kiírja lemezre. Betöltésnél pedig egyszerűen beolvassuk, és minden változóba, adatstruktúrába visszaírjuk az adatokat. Viszont az OO világban nem ilyen egyszerű a helyzet. Tegyük fel, hogy egy autót akarunk épp kimenteni. Nézzünk meg ennek az autónak, mint objektumnak egy lehetséges kódját. A nyelv, amit használni fogunk végig a tanulmányban a példák bemutatására, egy C-szerű fiktív nyelv, ezért csak a koncepcionális megoldásokat hivatott bemutatni, nem egy konkrét nyelvi szintaktikát. Tehát itt az autónk: class Autó { . . Vektor hely; float sebesseg; Motor motor; Rádió radio; Klíma klima; . . . } Az autónknak van többek közt helye, sebessége, és alkatrészei mint motor, rádió, klíma.
Eljárás-orientált szemlélet szerint lenne egy mindentudó, mindenható kód ami tudná, hogy pontosan miből épül fel az autó, kiolvasná az autó állapotát, és az autó elemeinek az állapotát. Ez viszont ellentmond az OO filozófiának. Az OO világban nincs mindenható és mindentudó kód, vagy objektum. Helyette minden objektumnak saját magát kellene tudnia kimenteni. Ezt nem is probléma megvalósítani, ez csak kis mértékben változtat a helyzeten. Adjunk az autónak egy kiment() metódust, ami visszatér az autó állapotával egy adatcsomag formájában, illetve egy betolt() metódust is, ami egy kapott adatcsomagból visszaállít mindent: class Auto{ . Adatcsomag kiment(){ Adatcsomag csomag=new Adatcsomag(); . . return csomag; } . . void betolt(Adatcsomag csomag){ . . } } Most már az autó nyilván tudja saját magáról, hogy milyen attribútumai vannak, tudja miket és hogyan kell kimenteni. Az egyszerű attribútumok (hely, sebesség) kimentése, majd visszaállítása nem is jelent gondot. A gond az autó részeit alkotó objektumok lesznek. Ugyanis az OO világnak a leglényegesebb jellemzője az absztrakció, és a polimorfizmus. Az autónak van például motorja. Tegyük fel hogy a "Motor" osztály csak absztrakt osztály, hiszen motorból többféle típus létezik: class Benzinmotor extends Motor{ . . . } class Dieselmotor extends Motor{ . . . } Ekkor az autó nincs közvetlenül tisztában azzal, hogy a motor valójában milyen motor. Neki elég annyi, hogy rendelkezik a motorok szabványos tulajdonságaival.
Bár végül is, ez nem jelent gondot, ha motor is saját magát menti ki, az autó pedig csak megkéri a motort, hogy mentse ki magát. Így nem kell az autónak tudnia, hogy miből áll egy adott típusú motor: class Auto{ . Adatcsomag kiment(){ Adatcsomag csomag=new Adatcsomag(); . . csomag.hozzaad(motor.kiment()); . . return csomag; } . . } Ez már majdnem jó is, csak még egy dolog hiányzik. Ahhoz, hogy a motort is vissza lehessen állítani (példányosítani), tudni kell annak a konkrét osztályát is. Tegyük fel, hogy azt is lekérdezzük a motorról, és azt is az adatcsomagba mentjük. Viszont ki fogja visszaállításnál a motort újra példányosítani? Az autó? class Auto{ . . void betolt(Adatcsomag csomag){ . motor= new ????(????); Motoradat motoradat=csomag.get("motoradat"); motor.betolt(motoradat); . . } } Itt megint egy problémába ütköztünk. Honnan tudja az autó, hogy a motort hogyan kell példányosítani, a konstruktorát hogyan kell meghívni, felparaméterezni? Hiszen nem ismerheti a konkrét motor típusokat. Nos, ide kell valami olyan segédobjektum, vagy rendszer, ami azért lesz felelős, hogy "legyártsa" a különböző motor típusokat.
Szóval innentől kezdve már sokszorosan bonyolultabb ez az egész téma, mint amilyennek elsőre tűnt. Ha azt gondolnánk, hogy ennyivel megúsztuk, menjünk még egy lépéssel tovább. Nézzük meg a következő helyzetet: class Auto { . . Személy tulajdonos; . . } Az autónak van tulajdonosa. A tulajdonos egy személy objektum. De a személy nem az autó része, hanem az autó csak hivatkozik rá. Ez csak egy kapcsolat. Sőt, egy tulajdonosnak több autója is lehet, tehát több autó is hivatkozhat rá. Sőt, ez a kapcsolat változhat is idővel. Ezt a hivatkozást is ki kell tudni menteni, de ez több szempontból is gondot okoz. Ugyanis, ha több hivatkozás is van ugyanarra a tulajdonosra, akkor az objektumok visszaállításánál nem kell minden autóhoz egy új tulajdonost létrehozni, csak vissza kell állítani a hivatkozásokat ugyanarra az egy személyre. Ehhez tudnunk kell az objektumokat azonosítani valami olyan módon, ami nem csak az élettartamuk alatt egyedi, hanem azután is megmarad, hogy az objektumokat kimentettük, és visszaállítottuk. Ehhez szintén szükség lesz valami rendszerre, ami ezeket az azonosítókat menedzseli, valahogy így: class Szemely{ . int id; . . } class Auto { Személy tulajdonos; void betolt(Adatcsomag csomag){ . . int tulajdonos_id=csomag.get("tulajdonos_id"); tulajdonos=PerzisztenciaMenedzser.get(tulajdonos_id); . . } }
Láthatjuk tehát, hogy egy jó objektum perzisztenciához kell egy külön keretrendszer, ami a színfalak mögött az objektumaink azonosítását, példányosítását, mentését, visszaállítását kezeli. Viszont annak ellenére, hogy minden perzisztencia keretrendszer ugyanazokat a problémákat kell hogy megoldja, mégis a perzisztenciának többféle formája is létezik, amiknek a koncepciója és a megvalósítása is eltér. Ezeket a különbségeket nagyon fontos megérteni mielőtt fejest ugranánk egy ilyen rendszerbe.
Perzisztencia formái Két nagy terület van, amiben megnyilvánul a perzisztencia. A megvalósítás ugyan hasonló, és különbségek nem olyan szembetűnőek, viszont teljesen eltérő filozófiát képviselnek. Sajnos mindkettőre az objektum perzisztencia kifejezést használják, viszont hogy megkülönböztessük őket, most adjunk nekik nevet. Ezek lesznek a hibernálás, és a perzisztens tárolás. Utóbbit, ha adatbázissal valósítjuk meg, akkor hívjuk ORM-nek. Elsőnek vegyük a hibernálást, és vegyünk példának egy játékot. Egy játékban le kell tudni menteni a játék aktuális állapotát, aztán később visszatölteni. Ez azt jelenti, hogy az objektumaink létrejönnek a memóriában, aztán életük végéig ott élnek, és közben változik is az állapotuk, ahogy műveleteket végzünk velük. Az egész objektum-modell végig a memóriában él és működik. Amikor kimentjük a játékot, akkor abból a célból tesszük, hogy felfüggesszük a működését, hogy később ugyanonnan tovább folytatódhasson a működése. Éppen ezért találó elnevezés a hibernálás: "lefagyasztjuk" hogy később "kiolvasztva" tovább használjuk. Ennek viszont egy nagyon fontos filozófiai jelentősége van. Ugyanis, amikor kimentem az objektumokat, akkor lemezen léteznek, illetve amég a programot le nem állítom, addig a memóriában és a lemezen is egyszerre. A kérdés, amit itt fel kell tennünk az az, hogy melyik "az igazi"? Vagyis hol van az igazi, az "élő objektum"? Valószínűleg itt mindannyian egyetértenénk abban, hogy az igazi, az élő objektum az az, ami a memóriában van. Ami a lemezen van, az csak egy képe az objektumnak, de nem ott "él".
Ha általánosítani szeretnénk, akkor azt mondhatjuk, hogy a hibernálás az az eset, ahol a munkamenet állapotát mentjük ki, ami lényegében többé-kevésbé magának az alkalmazásnak az állapota, amit épp futtatunk. Azt is elmondhatjuk, hogy lényegében itt a teljes objektum-modellt egyszerre mentjük ki és állítjuk vissza, vagy ha nem is az egészet, de legalábbis jól elkülönülő részeit. Az viszont biztos, hogy nem lenne értelme darabonként módosítgatni a már mentett állapotot, vagy csak darabjait visszatölteni. Akárhogy is, a hibernáció az alkalmazás, vagy a munkamenet állapotának a kimentése, visszaállítása. Perzisztens tárolás egy egész más probléma, egész más világ. Ez a dolog az adatbázisokhoz kötődik. Gondoljunk egy olyan alkalmazásra, ami adatbázissal dolgozik. Gondoljunk valami adminisztrációs rendszerre, mint például egy banki rendszer. Ügyfelek, számlák, tranzakciók, stb. Egy adatbázis már önmagában is lényegében perzisztensen tárolja ezeket az adatokat, hiszen ez is a célja. Viszont, ha jobban megnézzük, akkor ezek az "adatok" koncepcionális szinten valójában objektumok. Ezt szeretném hangsúlyozni, mivel a legtöbb embernek ez nem egyértelmű. A legtöbb embert megtéveszti, ha adatbázissal dolgozik, ugyanis egy adatbázis (többnyire relációs adatbázis) csak adatokkal foglalkozik, illetve azok struktúrájával, de semmi más. A viselkedésmodell nincs ott. Ezért a legtöbben nem objektumokra gondolnak, ha egy ilyen rendszert kell megtervezni. A legtöbben óhatatlanul is eljárás-orientáltnak látják ezt a helyzetet, vagyis hogy van az adatbázis, ami az adatmodell, és van a program ami dolgozik vele, az pedig az eljárás modell. De ez nem igaz. Elég fura lenne, ha ilyen rendszereket mindenképp eljárásorientáltan kellene megírni. Ha elvonatkoztatunk az adatbázistól, akkor például az Ügyfél, a Számla egy-egy osztály lenne, amiknek vannak attribútumaik, kapcsolataik, függőségeik, viselkedésük, identitásuk. Amikor, például egy számlán egy tranzakciót akarok végrehajtani, akkor egy számla objektumnak a metódusait akarom meghívni. Szóval itt is tisztességes objektum modellről van szó, amit perzisztensen tárolunk egy adatbázisban. Csakhogy a különbség a hibernációhoz képest az, hogy itt nem lehet az egész objektum modell egyszerre a memóriában, amikor dolgozni akarok vele. Fizikailag lehetetlen, hiszen több millió objektumról is lehet szó. Sőt, egy ilyen rendszernek több felhasználója is lehet, tehát nem létezhet minden felhasználónak a gépén egy-egy példánya az objektum modellnek, mert az inkonzisztenciához vezetne.
Szóval, amire itt szükség van az az, hogy egy objektumot csak ideiglenesen "élesztek fel" az adatbázisból, amíg műveleteket végzek rajta, aztán már mentem is vissza. Ezután viszont az objektumot el is dobom. Tehát itt, a hibernációval ellentétben egyenként manipulálom az adatbázisban lévő objektumokat. Az igazi koncepcionális különbség viszont akkor válik tisztává, ha itt is megkérdezzük magunktól, hogy melyik az "igazi" objektum? Az ami az adatbázisban van, vagy ami a memóriában? Hol "él" igazából az objektum? Az adatbázisban vagy a memóriában? A meglepő válasz pedig az, hogy az adatbázisban. Itt az adatbázis képviseli az igazi objektum modellt. Az adatbázis az "élő" modell, mivel közvetlenül innen kérünk le adatokat, és közvetlenül ide realizálódik minden művelet eredménye. Az adatbázis képviseli a rendszer mindenkori, aktuális, konzisztens állapotát. Viszont, ha ez mind igaz, akkor mit jelent az, hogy "betöltök" egy objektumot az adatbázisból, műveleteket végzek vele, aztán "visszamentem"? Amikor betöltöttem, akkor ideiglenesen nem az adatbázisban van az objektum? Nos, a helyzet az, hogy ebben a kérdésben nincs egyetértés. Vannak, akik ezt úgy fogják fel, hogy betöltöm az adatbázisból az objektumot, dolgozok vele, aztán visszamentem. Tehát igen, ideiglenesen az objektum átköltözik a memóriába, aztán visszamentem az adatbázisba. Személy szerint én ezt a felfogást hibásnak tartom. Ez ugyanis azt jelenti, hogy az objektum ide-oda ugrál az adatbázis és az alkalmazás között. Hol itt, hol ott van. Márpedig az igazi objektum csak egy helyen lehet. Ez így tisztességes, így konzisztens. Ezért én inkább úgy gondolom, hogy az az objektum ami a memóriában létrejön, amivel dolgozok az adott programozási nyelvben, valójában csak egy amolyan "interfész", egy felület az igazi objektumhoz, ami továbbra is az adatbázisban van. Ebben a felfogásban valójában nem betöltöm az objektumot az adatbázisból, hanem kérek hozzá egy felületet amin keresztül tudom manipulálni, mintha egy távirányítóm lenne hozzá. Ezért a memóriában lévő objektumnak igazából nincs is állapota. Az állapot végig az adatbázisban van. Ez azt is jelentheti, hogy a nyelvi objektumnak nincsenek is attribútumai. Lekérdezhetem őket, írhatom őket, de az közvetlenül az adatbázisból jön, és oda is mentődik. Tehát mindezt figyelembe véve láthatjuk, hogy tényleg hatalmas különbség van hibernálás és perzisztens tárolás közt. Ugyan mind a kettő ugyanazokkal a kihívásokkal kell hogy megküzdjön, és megvalósítás szintjén is szinte ugyanazokat a dolgokat kell tudniuk (objektumok azonosításának
a
menedzselése,
attribútumok,
kapcsolatok,
identitás,
objektumok
példányosítása, stb), mégis óriási koncepcionális különbség van köztük, és egész más módon is használjuk a két megoldást.
A továbbiakban részletesen megnézzük a megvalósításokat mindkét esetben.
Megvalósítás formái: interfész-objektum vs betöltés/mentés Mint láttuk korábban, mindenre kiterjedő perzisztencia megvalósításához szükség van valamilyen segéd rendszerre, aminek tudnia kell az objektumok azonosítását, példányosítását, visszaállítását intézni. Ebből kiindulva minden perzisztenciával kapcsolatos feladat elvégzésére érdemes bevezetni egy keretrendszert. Szeretném előre bocsátani, hogy konkrét megvalósítási technikákból több is szóba jöhet, ne várja senki azt, hogy kaphat egy általánosan igaz megoldást, amit minden helyzetben lehet másolbeilleszt módon lehet felhasználni. Más környezetben más jellegű megvalósításra lehet szükség. Az itt ismertetett megoldás is csak egy a több lehetőség közül. Az első dolog, amit érdemes tisztázni, hogy az adott programozási nyelvnek van-e beépített támogatása objektumok, objektum struktúrák automatikus mentésére. Ha van, akkor ez általában az úgynevezett szerializáció. Ezt jelenti, hogy az objektumokat átalakítja valamilyen egyszerű karakterlánc szerű (szeriális) reprezentációvá, amit aztán kedvünkre felhasználhatunk, majd egy ilyen szeriális formából vissza tudja állítani a valódi objektumokat. Ebben az esetben nem kell semmivel se törődnünk (vagy csak kevés dologgal), mindent a nyelv old meg nekünk. Az automatizált szerializáció egyszerű, kényelmes megoldás a munkamenet kimentésére, főleg webes alkalmazásoknál, mivel ott nem fut folyamatosan a programunk, viszont a munkamenet (session) állapotának meg kell maradnia. A saját magunknak fejlesztett keretrendszert többféle módon képzelhetjük el. Egyrészt, valamilyen segéd objektumok formájában, amik végezhetik más objektumok példányosítását, lekérdezését, mentését, betöltését, stb. A másik lehetőség, amit gyakran látunk, hogy van valamilyen ősosztály amiből a perzisztens objektumok származnak, és ez az ősosztály implementálja a perzisztencia kezelés bizonyos funkcióit. Olyan is szóba jöhet, hogy van valamilyen perzisztencia interfész, amit a perzisztens objektumok implementálnak, és az ilyen objektumokat kezelik a keretrendszer segédobjektumai.
Hibernálás Először nézzük meg az egyértelműbb esetet, a hibernálást. Itt az alkalmazás vagy a munkamenet állapotát mentjük ki, hogy felfüggeszthessük későbbi folytatás céljából. Vegyünk most egy olyan megoldást, ahol egy közös ősosztályból származnak a modellünk objektumai. Az első legfontosabb dolog, hogy minden perzisztens objektumunknak legyen egy globálisan egyedi azonosítója. Ez szükséges lesz, hogy mentés és visszaállítás között is egyértelműen hivatkozhassunk az objektumainkra, illetve azok egymásra. Maradjunk az autós példánál: abstract class Perzisztens { int id; } class Auto extends Perzisztens{ } A kérdés az, hogy ki és mikor állítja be az id-t? Nyilván azonosítót akkor osztunk egy objektumnak amikor létrejön. De mit értünk az alatt, hogy létrejön? Ugyanis az objektumot nem csak akkor példányosítjuk, amikor először megszületik, hanem minden egyes alkalommal amikor betöltjük! Ez azt jelenti, hogy lényeges különbség van létrehozás és visszállítás között. Éppen ezért az objektum példányosítása, a visszaállítás eszköze lesz. Azok a dolgok pedig, amiket az objektumok születésükkor szoktak végezni a konstruktorukban, át kell hogy kerüljenek valahova máshová. Emiatt így módosul a kódunk: abstract class Perzisztens { int id; Perzisztens (int id){ this.id=id; } } class Auto extends Perzisztens{ void inicializal(Vektor hely, Motor motor, Rádió radio){ this.hely=hely; this.motor=motor; this.radio=radio; } }
Így tehát, egy autó létrehozásakor először csak egy nyers, üres példány jön létre, mindaddig, amíg meg nem hívjuk az inicializálását, ezután jön majd létre ténylegesen. Ahhoz pedig, hogy az azonosító (id) beállítódjon, szükségünk lesz valamilyen objektum menedzserre (nevezzük perzisztencia menedzsernek), ami az objektumaink példányosítását végzi. Éppen ezért nem mi fogjuk az objektumainkat példányosítani közvetlenül, hanem megadott osztálynév alapján "kérünk" egy példányt a perzisztencia menedzserünktől. Ha azt szeretnénk, hogy perzisztencia menedzserünk közvetlenül osztálynév alapján tudjon objektumokat példányosítani, akkor ezt az adott nyelvnek is támogatnia kell. Továbbá ki kell kötnünk azt is, hogy a konstruktorok egységesek legyenek, így a példányosítást egy általános kóddal meg lehet oldani (bár vannak rá módszerek arra is hogy a konstruktor is tetszőleges adatokat kaphasson). Ez a megoldás valahogy így nézhet ki: class PerzisztenciaMenedzser{ int kovetkezo_azonosito=0; Perzisztens peldanyok[]; Perzisztens ujPeldany(string osztalynev){ kovetkezo_azonosito++; //az osztálynév alapján példányosítás természetesen nyelvfüggő, ez egy fiktív példa peldany= new osztalynev (kovetkezo_azonosito); //megjegyzünk egy referenciát a példányokra. peldanyok[kovetkezo_azonosito]=peldany; return peldany; } } Ha a perzisztencia menedzser nem tudja egy általános módon példányosítani az objektumokat, vagy eleve nyelvfüggetlen megoldást szeretnénk, akkor neki is más segédobjektumoktól kell kérnie a példányokat. Ezeket hívják object factory-nak, vagyis objektum "gyárnak". Az ötlet az, hogy minden példányosítható perzisztens osztályhoz tartozni fog egy factory. A factorynak semmi más dolga nincs, mint példányosítani egyet abból az objektumból aminek a gyártásáért felelős. Így minden objektum akár tetszőleges konstruktorral is rendelkezhet. A perzisztencia menedzsernek valamilyen módon ismernie kell az összes factoryt, és tudnia kell azt is, hogy melyik osztálynak melyik a factoryja.
abstract class Factory{ abstract string getOsztalynev(); abstract Perzisztens ujPeldany(int id); } class AutoFactory extends Factory{ string getOsztalynev(){ return "Auto"; } Perzisztens ujPeldany(int id){ return new Auto(id); } } class PerzisztenciaMenedzser{ int kovetkezo_azonosito=0; Perzisztens peldanyok[]; Factory factoryk[]; Factory getFactoryOsztalyhoz(string osztalynev){ //kikeresi az a factoryt ami a megadott osztályhoz tartozik és visszatér vele } Perzisztens ujPeldany(string osztalynev){ kovetkezo_azonosito++; peldany= getFactoryOsztalyhoz(osztalynev).ujPeldany(kovetkezo_azonosito); peldanyok[kovetkezo_azonosito]=peldany; return peldany; } } Egy ehhez hasonló perzisztencia menedzsernek szüksége lesz arra is, hogy tudjon minden perzisztens objektumról, ezért van egy tömbje, amiben megjegyzi őket. Ez később lényeges lesz, és azért is kell, hogy végig tudjunk menni rajtuk, amikor ki kell menteni a teljes objektum modell állapotát.
Tehát egy autó létrehozása valahogy így néz ki: { PerzisztenciaMenedzser perzisztenciamenedzser; . . Auto auto=perzisztenciamenedzser.ujPeldany("Auto"); auto.inicializal(hely,motor,radio); } Természetesen az itt leírt megvalósítás csak egy példa. Nemcsak helyzetfüggő, de nyelvfüggő is lehet. A lényeg amit általánosságban érteni kell, hogy az objektumok létrehozása és példányosítása elválik, és hogy szükség van valamilyen segédobjektumra, vagy segédrendszerre ami a példányosítást végzi, irányítja. Létrehozáshoz hasonlóan objektumok törlése is igényel egy kis pluszt. Amikor egy objektumot törlünk, vagy törlődik, el kell távolítanunk a perzisztencia menedzser objektumlistájából is. A szemétgyűjtögetős nyelvekben eleve nem is tudnánk addig törölni egy objektumot, amég a perzisztencia menedzser is el nem dobja a referenciáját. Ezért érdemes bevezetni egy explicit törlés metódust is az ősosztályba. abstract class Perzisztens { int id; . . void torol(){ perzisztenciamenedzser.objektumTorol(this.id); } } Nézzük most az attribútumok kimentését. Attribútumok alatt olyan dolgokat értünk most, amik vagy primitív típusok, vagy olyan objektumok amik csak valami adatstruktúrát valósítanak meg, pl lista, rekord, stb. A lényeg, hogy semmiképp nem önálló perzisztens objektumok, mivel ebben az esetben már kapcsolatról beszélünk, az pedig egy másik perzisztencia probléma.
Itt két választásunk lehet. Az attribútumok szerializációját végezheti maga az objektum, vagy esetleg, ha a nyelv is lehetőséget ad rá, akkor a perzisztencia menedzser valamilyen automatikus, általános formában lekérdezheti, enumáralhatja az objektum attribútumait. Az utóbbi elég speciális megoldás, ezért inkább nézzük az elsőt, vagyis hogy az objektum csomagolja be valami szeriális adatszerkezetbe az attribútumait, és visszatér velük, betöltésnél pedig egy ilyen adatcsomagból kiolvassa és visszaállítja az attribútumait: abstract class Perzisztens { . . abstract Adatcsomag attributomokKiment(); abstract void attributumokBetolt(Adatcsomag csomag); . . } class Auto extends Perzisztens { Adatcsomag attributumokKiment(){ Adatcsomag csomag=new Adatcsomag; //itt az autó az attribútumat belerakja a csomagba. csomag.hozzaad(....); return csomag; } void attributumokBetolt(Adatcsomag csomag){ . . //kiolvassa az attribútumait, és visszaállítja attributum=csomag.get(); . . } }
Ezeknek a metódusoknak a meghívása a perzisztencia menedzserünk feladata lesz. Az összes objektum kimentésének a folyamata így nézne ki: class PerzisztenciaMenedzser{ . . Perzisztens peldanyok[]; . . void osszesKiment(){ Adatcsomag csomag=new Adatcsomag(); for(int n=0;n
A legnehezebb rész a kapcsolatok kezelése. Sajnos a mai fő nyelvek a kapcsolatok fogalmát nem ismerik. Helyette az objektumok közti kapcsolatok is csak sima referenciákként jelennek meg. Ez pedig zavaró, mert nem minden referencia kapcsolat. Például: class Vektor{ int x,y,z; Vektor (int x, int y, int z){ this.x=x; this.y=y; this.z=z; } } class Szakasz{ Vektor a,b; . . } A szakasz két végpontja egy-egy vektor, és ugyan az "a" és "b" attribútumok objektumreferenciák, ez mégsem kapcsolat. Egyszerűen a szakasz adatai. Ezeket kimentésnél ugyanúgy attribútumként kellene kezelni, mint ahogy az előbb láttuk. Egyszerűen kimentem az értékeiket, aztán beolvasom és beállítom. A következő helyzet viszont már egész más: class Auto { Személy tulajdonos; . . } A személy egy független, önálló része a világnak, és nem csak az autó egy adata. Arról nem is beszélve, hogy egy személy több autónak is lehet a tulajdonosa, tehát többen hivatkozhatnak rá. Pontosan ez az a szituáció, ami miatt teljesen újra kell gondolnunk a kapcsolatok kezelését, és ezért is lesz szükség arra, hogy egyedi azonosítóval legyenek ellátva az objektumaink, és ezért szükséges az is, hogy a perzisztencia menedzserünk tudjon minden objektumról.
A kapcsolatok kimentéséhez a hivatkozás túloldalán lévő objektumnak az azonosítóját kell mentenünk, visszaállításnál pedig a hivatkozott objektumot szintén az azonosító alapján kell majd lekérdezni a perzisztencia menedzsertől. Ennek egy lehetséges módja így nézhet ki: class PerzisztenciaMenedzser{ . . Perzisztens peldanyok[]; . . Perzisztens getObjektum(int id){ return peldanyok[id]; } } class Perzisztens { abstract Adatcsomag kapcsolatKiment(); abstract void kapcsolatBetolt(Adatcsomag csomag); } class Auto extends Perzisztens { Személy tulajdonos; Adatcsomag kapcsolatKiment(){ . . csomag.hozzad("tulajdonos_id",tulajdonos.id); return csomag; } void kapcsolatBetolt(Adatcsomag csomag){ . . int tulajdonos_id=csomag.get("tulajdonos_id"); tulajdonos=perzisztenciamenedzser.getObjektum(tulajdonos_id); } }
Ezzel megoldódik az gondunk is, hogy egyszerre több autó is hivatkozik ugyanarra a tulajdonosra. Gondban akkor lehetünk, ha úgy akarjuk visszaállítani a kapcsolatot, hogy még a tulajdonos objektum nincs visszaállítva, hiszen az objektumok visszaállításának sorrendje nem egyértelmű. Erre több megoldás is lehetséges. Az első, hogy a perzisztencia menedzserünk getObjektum() metódusát úgy írjuk meg, hogy ha olyan objektumot kérnek tőle, ami még nincs visszaállítva, akkor megpróbálja visszaállítani azt is. A másik, hogy az objektumok közti kapcsolatok visszaállítását csak azután kezdjük el, miután az összes objektumot visszaállítottuk. A harmadik lehetőség, hogy az objektumaink nem valódi referenciát tárolnak egymásra, hanem csak az azonosítókat, és minden alkalommal amikor szükség van a kapcsolat túloldalán lévő objektumra, akkor lekérdezik azt: class Auto extends Perzisztens{ int tulajdonos; Személy getTulajdonos(){ return perzisztenciamenedzser.getObjektum(tulajdonos); } } Akinek nem tetszik, hogy a kapcsolat csak egy egyszerű int formájában van tárolva, az megvalósíthatja a kapcsolatokat is objektumként, valahogy így: class Kapcsolat { int id; Perzisztens get(){ return perzisztenciamenedzser.getObjektum(id); } void set(Perzisztens objektum){ id=objektum.id; } } class Auto extends Perzisztens{ Kapcsolat tulajdonos; . . } Ebben az esetben a kapcsolat objektumot úgy is meg lehet valósítani gyorsabb működés érdekében, hogy letárolja a közvetlen referenciát is a kapcsolat túloldalán alló objektumra, miután megtörtént az első sikeres lekérdezés, és később csak azzal tér vissza.
Természetesen kapcsolatok kezelésénél is több jó megoldás létezhet, főleg a nyelvi lehetőségek miatt, de a lényeg amit meg kell jegyezni, hogy a kapcsolatokat objektum azonosítókkal kell kezelni, és a perzisztencia menedzsertől kell lekérdezni ezek alapján az objektumokat. Végül figyelnünk kell arra, hogy ne próbáljunk meg hivatkozást helyreállítani úgy, hogy a túloldalon lévő objektum még nem létezik.
Perzisztens tárolás Most térjünk át a perzisztenciának a másik fajtájára, ami az adatbázisokhoz kötődik. Mint ahogy azt már említettük, itt az a különbség a hibernációhoz képest, hogy nem az egész objektum modell mentjük és állítjuk vissza, hiszen az fizikailag is lehetetlen lenne. Annak a filozófiának is gyakorlati jelentősége van, hogy az "igazi" objektum mindvégig az adatbázisban él. Eleve a kiindulási helyzet is az, hogy az adatbázisban vannak az objektumok, és csak akkor kérek le egyet, amikor dolgozni szeretnék vele. A perzisztencia legtöbb technikai megoldása itt is szükséges. Ugyanúgy kell egy közös ős, amiből az objektumok származnak, kell egyedi azonosító nekik, kell perzisztencia menedzser, és factoryk is. Itt is elválik a példányosítás és a létrehozás fogalma, és itt talán a legegyértelműbb, hogy miért. Amikor létrehozok egy új objektumot, akkor létre kell jönnie az adatbázisban is az őt reprezentáló bejegyzéseknek, viszont amikor egy létező objektumot kérdezek le, akkor tényleg csak a példány jön létre, adatbázisban nem történik semmi. Ezért is van az, hogy ebben a helyzetben én szívesebben gondolok úgy a nyelvi objektumra, mint egyfajta interfészre, aminek nincs saját állapota, hanem csak amolyan távirányítóként működik a valódi objektum manipulálására, amit az adatbázis bejegyzés testesít meg. Elválik a nyelvi objektum megsemmisítése, és az objektum tényleges megsemmisítése is. A nyelvi objektum megsemmisítése csak annyit jelent, hogy nem kívánok vele tovább dolgozni, ezért eldobom. Ezen is jól látszik, hogy az interfész-objektum filozófia a helytálló.
Mivel itt adatbázisban tárolódnak az objektumaink, ezért a reprezentációjuknak is ehhez kell igazodni. Relációs adatbázisban az objektumok tárolása viszonylag egyértelmű. Minden osztályt egy tábla reprezentál, aminek minden sora egy objektum, a tábla oszlopai pedig az objektum attribútumai. Abban meg kell egyeznie minden táblának, hogy az elsődleges kulcsuk az objektum azonosítója kell hogy legyen. Használnunk kell egy külön táblát arra is, hogy az összes létező objektum azonosítóját tárolja, így tudunk minden objektumot enumerálni, és itt tudjuk léptetni az azonosítót is amikor új objektumot kell létrehozni. Arra is ki kell találni valamilyen szabályt, vagy rendszert, ami meghatározza hogy mely objektumhoz mely tábla tartozik. A legegyszerűbb esetben a tábla nevét érdemes az osztály nevével megegyezőre választani, vagy belőle valamilyen módon származtatni, hogy általánosan kezelhető legyen. Kellhet külön táblaszerkezet a kapcsolatok reprezentálására is. Relációs adatbázisban a különböző számosságú kapcsolat típusok másképp valósulnak meg. Egy az egyhez kapcsolatokat elég egy mezőként reprezentálni ami tartalmazza a hivatkozott objektum azonosítóját, abban az osztályban ahonnan a hivatkozás kiindul. Egy a többhöz kapcsolatok esetén a hivatkozott fél tárolja egy mezőjében a hivatkozó fél azonosítóját. Több a többhöz esetén pedig kell egy külön tábla, ami a hivatkozó és a hivatkozott fél azonosítóját is tárolja. Ha kicsit tudatosabban akarjuk a kapcsolatokat kezelni, akkor érdemes az egy az egyhez és az egy a többhöz kapcsolatokat is külön táblákba kiemelni, így könnyebben enumerálhatóvá válnak.
Az objektumok lekérdezése ebben a fajta perzisztencia menedzserben kicsit más lesz. Mivel itt nincsenek az objektumok előzetesen betöltve, ezért az egyes objektumokat akkor példányosítjuk amikor először lekérdezik. Utána már megjegyezhetjük a példányt, és azt adhatjuk vissza legközelebb: class PerzisztenciaMenedzser{ . . Perzisztens peldanyok[]; . . Perzisztens getObjektum(int id){ //ha még nem létezik, betöltjük if (peldanyok[id]==null){ //betöltjük az adott azonosítójú objektum osztálynevét osztalynev=Adatbázis.betolt("fő_objektum_tábla",id,"osztalynev"); peldanyok[id]=getFactoryOsztalyhoz(osztalynev).ujPeldany(id); } return peldanyok[id]; } } Az attribútumok kezelése esetében két utat választhatunk. Az első, hogy az objektum lekérdezésekor betöltjük az attribútumait is. Ezután szabadon manipulálhatom őket, aztán amikor végeztem, visszamentem őket. Viszont ha az objektum úgyis adatbázisban létezik, és ott van az igazi állapota, és a nyelvi objektum csak egyfajta interfészként szolgál, akkor igazából attribútumokra a hagyományos értelemben véve nincs is szükség. Az attribútumok igazából az adatbázisban vannak, én pedig csak írom és olvasom őket a nyelvi objektumon keresztül. Még ha használok is igazi attribútumokat, azok akkor is csak egy ideiglenes tárolóként szolgálnak a beolvasott értékeknek.
Tehát lássuk a két esetet, először attribútumokkal: abstract class Perzisztens { int id; Array getAttributumok(Array attributum_nevek){ tablanev=perzsztenciamenedzser.getTablanev(this); return Adatbázis.betolt(tablanev,id,attributum_nevek); } void setAttributumok(Array nev_ertek_parok){ tablanev=perzsztenciamenedzser.getTablanev(this); Adatbázis.modosit(tablanev,id,nev_ertek_parok); } abstract void elment(); abstract void betolt(); } class Személy extends Perzisztens{ string nev,cim,telefon; void elment(){ Array adatok= new Array(); adatok["nev"]=nev; adatok["cim"]=cim; adatok["telefon"]=telefon; setAttributumok(adatok); } void betolt(){ Array adatok; adatok=getAttributumok(new Array(nev,cim,telefon)); nev=adatok["nev"]; cim=adatok["cim"]; telefon=adatok["telefon"]; } }
Az egyetlen szépséghibája ennek a megvalósításnak az, hogy minden egyes osztálynak implementálnia kell az attribútumainak az elmentését, betöltését. Ez elég kényelmetlen dolog. Persze segíthet, ha a nyelv lehetőséget ad arra, hogy az attribútumokat tudjuk enumerálni, vagy név alapján elérni, és akkor írhatunk rá egy teljesen általános kódot is az ősosztályban, ami betölti és menti a paraméterben megnevezett attribútumokat. Ilyen problémánk viszont nincs ha teljesen elkerüljük az attribútumokat: abstract class Perzisztens { int id; Array getAttributumok(Array attributum_nevek){ tablanev=perzsztenciamenedzser.getTablanev(this); return Adatbázis.betolt(tablanev,id,attributum_nevek); } void setAttributumok(Array nev_ertek_parok){ tablanev=perzsztenciamenedzser.getTablanev(this); Adatbázis.modosit(tablanev,id,nev_ertek_parok); } } class Személy extends Perzisztens{ } Mindezt teljesen legitim módon megtehetjük, mivel a nyelvi objektum igazából csak a viselkedésmodellt testesíti meg, az állapota pedig, ahogy említettük, permanensen az adatbázisban él. Esetleg abba lehetne belekötni, hogy így nem lehet tudni hogy milyen attribútumai vannak igazából az objektumnak, de ezt is orvosolhatjuk egy ehhez hasnonló változtatással: abstract class Perzisztens { . abstract Array getAttributumNevek(); . . } class Személy extends Perzisztens{ Array getAttributumNevek(){ return new Array("nev","cim","telefon"); } }
Így meg lehet tudni, hogy egy objektumnak milyen attribútumai vannak, vagy akár enumerálni is lehet könnyedén, ami nagyon hasznos. Lássuk mi a helyzet a kapcsolatokkal. Mivel nem töltjük be a teljes objektum modellt, ezért az eltérés a hibernációhoz képest itt lesz a legerősebb. Itt semmiképpen nem implementálhatjuk a kapcsolatokat közvetlen referenciaként, mint ahogy azt egy hagyományos, memóriában működő objektum modellben tennénk. Az oka ennek az, hogy ha szeretnék manipulálni egy objektumot, aminek mondjuk csak az attribútumait szeretném átírni, viszont ennek az objektumnak vannak kapcsolatai száz másik objektumra, akkor azokat az objektumokat nem szeretném betölteni. Ha betölteném, akkor be kellene tölteni az általuk hivatkozott objektumokat, aztán meg az azok által hivatkozottakat, és így tovább a végtelenségig. Tehát itt mindenképp azt a megoldást kell választani, hogy csak azonosítókkal dolgozunk. Sőt, még ennél is tovább kell menni, mert még a hivatkozott objektumok azonosítóit sem tölthetem be, mert lehet hogy már az is olyan sok, hogy fizikailag lehetetlen. A legjobb megoldás az, ha követjük az alap filozófiánkat, vagy hogy akkor és csak akkor töltök be egy objektumot amikor dolgozni akarok vele. Ez esetben még a kapcsolat túloldalán lévő objektumoknak az azonosítóit is csak akkor olvasom be, ha tényleg dolgozni akarok velük.
class Kapcsolat { int id; string kapcsolat_nev; Kapcsolat(int id, string kapcsolat_nev){ this.id=id; this.kapcsolat_nev=kapcsolat_nev; } int get(){ return Adatbázis.kapcsolatBetolt(id,kapcsolat_nev); } void set(int id){ Adatbázis.kapcsolatModosit(this.id,kapcsolat_nev,id); } } class Auto extends Perzisztens{ Kapcsolat tulajdonos=new Kapcsolat(this.id,"tulajdonos"); . . string getTulajdonosNev(){ int szemely_id=tulajdonos.get(); Szemely szemely=perzisztenciamenedzser.getObjektum(szemely_id); Array adatok=szemely.getAttributumok(new Array("nev")); return adatok["nev"]; } } A Kapcsolat osztály egyszerű egy az egyhez kapcsolatot valósít meg. Ehhez szüksége van a hivatkozó objektum azonosítójára, és a kapcsolat "nevére", ami alapján tudja, hogy mely adatbázisban mely mező reprezentálja ezt a kapcsolatot (persze ezt is sok más módon is meg lehet még oldani). Filozófiánkhoz híven, ha a kapcsolatot lekérdezem, csak egy azonosítót kapok, aztán, ha akarom, majd lekérdezem az ahhoz tartozó objektumot is. Ez a megoldás persze nem kőbe vésett törvény, de sokszor előfordul hogy lekérdezek hivatkozásokat csak azért, hogy a visszakapott azonosítókat más kapcsolatokba írjam bele, vagy felhasználjam valami más adatbázis lekérdezésekhez. Ekkor pedig szintén nem volt szükségem a tényleges objektumra.
Utószó Remélem ezután érthetőbb, hogy miért is olyan problémás dolog ez a perzisztencia. Még így is, kiemelve az általános jellemzőit, sokféle technikai megvalósítása létezik, arról nem is beszélve, hogy mekkora különbségeket jelentenek a nyelvi lehetőségek, korlátok. Szinte biztos, hogy minden programozó fog találkozni ezzel a problémával, és ez egy olyan dolog, amibe nem szabad felkészületlenül beleugrani. Már az is elég, ha az ember tisztában van azzal, hogy ez a probléma létezik és hogy vannak rá már megírt, jól működő keretrendszerek amit lehet használni. Kész rémálom tud lenni a perzisztencia problémája, ha valaki úgy találkozik szembe vele, hogy nem is ismeri fel. Főként ajánlom az elmélyülést ebben a témában azoknak, akik adatbázis alapú alkalmazásokat kívánnak fejleszteni (tehát gyakorlatilag minden webprogramozó), mivel a legtöbben képtelenek összeegyeztetni az objektum orientált programozást a hagyományos relációs adatbázisokkal, és ösztönösen eljárás-orientáltan programoznak, ami valljuk be, már nem a 21. század.