Nemrégiben egy projekt kapcsán felmerült egy elég csúnya hiba. Nagyobb megterhelés esetén a rendszer processzor használata extrém magasra pörgött fel, olyannyira, hogy képtelen volt a webes kérések kiszolgálására, így újra kellett indítani. A rendszer fejlesztésével egyre instabilabbá vált a rendszer, aztán egy ponton elérte a teljes használhatatlanság fokát. Sokáig kerestük a hiba okát, mire rájöttünk, hogy mi okozta.
A Hibernate egy rendkívül kényelmes JPA alapú ORM rendszer. Az ember létrehoz néhány felcímkézett osztályt, és ide tárolja az adatokat. A Hibernate pedig szinte láthatatlanul elvégzi az adatbázis leképzést, és a kapcsolódó adatbázis műveleteket. A hibát ott követtük el, hogy kihasználva ezt a kényelmességet úgy használtuk ezeket a perzisztens objektumokat, mintha a memóriában lennének. Ciklusok futkároztak keresztül kasul a listákon, olvasgattuk/írogattuk az elemeket, mindeközben fittyet hányva arra, hogy milyen komoly adatbázis műveletek futnak a háttérben. Mint később kiderült, annyira komolyak, hogy néhány száz felhasználó taccsra tudott vágni egy 8 processzoros gépet. Mikor bekapcsoltuk az SQL-ek loggolását, elborzadva láttuk az eredményt, egy-egy webes kérés kiszolgálásakor százával pörögtek a 3-4x-es join-olt lekérdezések. Nem csoda, hogy az alkalmazás hamar felzabálta a gépet. Végül is az adatbázis használat optimalizálásával, és cache-ek használatával hamar úrrá lettünk a problémán. Ezt a kis affért tekinthetnénk akár egyszerű programozási hibának is, de engem gondolkodóba ejtett.
Egy régi kollégámmal annak idején sokat vitatkoztunk a Python és a Java nyelv kapcsán. Ő Python párti volt, én viszont nem nagyon tudtam megbarátkozni a nyelvvel. Ez azóta persze megváltozott. Script nyelvként nagyon megszerettem a Pythont, de komolyabb projektbe nem mernék belefogni vele. A vitánk alapja az volt, hogy szerintem a Python nagyon megengedő nyelv. Nem típusos, és szinte bármit felül lehet benne definiálni. Ez egyfelől nagyon kényelmes, ugyanakkor véleményem szerint egyben veszélyes is. Sokan egy programnyelv típusosságára vagy az operator overloading hiányára úgy tekintenek, mint koloncra, ami gúzsba köti a fejlesztőt, nem adja meg a kellő szabadságot, hogy kényelmes és elegáns kódot készítsünk. Én ugyanakkor ezeket a megkötéseket segítségnek tekintem, amelyek segítenek abban hogy hibátlan és optimális kódot készítsünk. A barátom érve ezzel szemben az volt, hogy ezeknek az eszközöknek a használata nem kötelező. Aki nem tud programozni, az ne használjon ilyesmit, aki jó fejlesztő, nem követ el szarvas hibákat. A probléma csak az, hogy az ember hamar elcsábul, vagy a felhasznált programkönyvtárak miatt belekényszerül abba, hogy éljen ezekkel a lehetőségekkel. A másik probléma, hogy ezek a nyelvek általában nem is adnak lehetőséget arra, hogy határok közé szorítsuk magunkat. Python esetén például ha akarnánk sem tudnánk típusos függvényeket írni, ami azért néha hasznos tud lenni, hiszen segít abban, hogy ne rontsuk el a paraméterezést (két paraméter véletlen felcserélése komoly galibát tud okozni). Van egy harmadik ok is, amiért a szigorú nyelvek mellett tettem le a voksomat. Egy komoly fejlesztés sokszor igen feszített tempóban történik. Kemények a határidők, sok a feature igény, kevés az ember. Ilyen körülmények között nem ér rá az ember gondolkodni, hogy vajon szép-e ez a kód, nem okoz-e majd később galibát, stb. Egyszerűen csak darálja a funkciókat, hogy elkészüljön a szoftver. Hasonló hibába estünk bele mi is a Hibernate kapcsán. Kihasználtuk az eszköz által nyújtott kényelmet, daráltuk a kódot, és nem gondoltuk végig, hogy ennek milyen következménye lehet. És ez a kis programozási nyelvekkel kapcsolatos kitérő itt kapcsolódik vissza az eredeti témához. Kényelem vs. biztonság.
A Hibernate-es probléma kapcsán tehát elgondolkodtam, hogy milyen elvek szerint kellene vajon felépíteni egy ORM rendszert úgy, hogy ne futhasson bele az ember ilyen hibákba, hasonlóan ahhoz, ahogyan a Java típusossága segít abban, hogy ne cserélhessük fel a paramétereket úgy, ahogy Pythonban. Az első gondolatom az volt, hogy talán vissza kellene térni a tiszta SQL-hez. Így rá vagyunk kényszerítve, hogy explicit módon adjuk meg a lekérdezéseket, és rá vagyunk szorítva arra, hogy ezek a lekérdezések optimálisak legyenek. (Legalábbis a nem optimális lekérdezések kibökik a szemünket.) Egy ilyen rendszerben SQL-el kérdeznénk le az adatokat, leképeznénk őket objektumokba, elvégeznénk a szükséges műveleteket, majd SQL-el pakolnánk őket vissza az adatbázisba. Ez tipikus DAO filozófia, és pont azért alakultak ki az ORM rendszerek, mert ez így azért elég kényelmetlen. E mellett azt se felejtsük el, hogy a manuális leképzés megint csak sok hibalehetőséget rejt magában. Az ORM rendszerek automatikus leképzése, és az objektum szintű típusosság sok gondot levesz a vállunkról, ráadásul az alkalmazás adatbázis rendszer független lesz, ami megint csak nagy előny. Tehát az ORM-től nem olyan könnyű megszabadulni, és valószínűleg nem is kell, de nagyon oda kell rá figyelni, ami nem jó, és mint látható, komoly hibákat szülhet.
Ami mostanában körvonalazódik a szemeim előtt, az valami olyan megoldás, ami ötvözi a JPA és a DAO-k előnyeit. A durva SQL-ek amiatt születtek, mert a rendszer felhúzott sok csatolt entitást is (ez okozta a sok join-t). Éppen ezért én a saját ORM rendszeremből kihagynám a kapcsolatokat, vagy legalábbis nem tenném annyira transzparenssé, ahogyan a Hibernate. Például egy entitáshoz kapcsolódó további entitásokat egy entitás osztályban definiált függvény szippantaná fel. Tehát az entitások tábla szintűek lennének, a kapcsolatok pedig explicit módon jelennének meg lekérdezések formájában. Bár nem okozott akkora galibát, de némi fejtörést igen, hogy bonyolultabb objektum manipulációk esetén az objektum struktúrát több request/response szakaszig a session-ben kell tartani, és csak ezután szabad visszarakni az adatbázisba. Mivel az entityManager csak 1-1 request alatt él, hamar problémát okozott, hogy egy session-ben lévő perzisztens objektumot (ami közben már elveszítette perzisztens jellegét) vissza akartunk rakni az adatbázisra (Lazy...Exception). Ennek kiküszöbölésére valamilyen long transaction mechanizmust tudnék elképzelni, ahol több request-en átívelő kérésekhez lehetne kérni egy long transaction-t. A long transaction session szintű objektum lenne, és ha felolvasunk/módosítunk valamit, ide kerülne. Végül egy commit-al a végén kerülne vissza az összes objektum az adatbázisra. Itt lehetne valamilyen ütközés figyelés (pl. verzionáljuk az objektumokat), ami konkurens írás esetén exception-el jelez. Végül pedig kellene valamilyen transzparens cache az adatbázis és az alkalmazás közé, hogy ha valamit felolvastunk, azt ne kelljen még egyszer felszedni. Valahogy így nézne ki egy ideális ORM rendszer. Ami még kicsit zavaró, az a sok dinamikus proxy, ezek teljesen feleslegesen terhelik a processzort, ezek helyett hatékonyabb lenne statikus (fordítási idejű) kódgenerálást alkalmazni.
Találkoztam még egy érdekes Space4j nevű projekttel, ami az egész dolgot megfordította. Nem az adatbázist képezi memóriára, ehelyett az objektumokat a memóriában tartja, ami nem kell, azt perzisztensen kipakolja onnan, és e mellett mindent megold. Itt tehát nem a perzisztens objektumok cache-elése a feladat, hanem a cache (memória) perzisztens mentése. Talán pont egy ilyen megoldás lenne a leginkább ideális, persze kérdés, hogy mindezt hogy lehetne integrálni a már meglévő rendszerekkel, amennyiben hostolt rendszerben szeretnénk azt használni (pl. Google App Engine). Emellett még nem próbáltam a rendszert, tehát nem tudok nyilatkozni róla, hogy milyen rejtett hátrányai lehetnek egy ilyen megoldásnak.
A fent említett problémába mi is belefutottunk. Sokat tanakodtunk, hogy mi lehet a jó megoldás és hogyan is kéne alkalmazni ezt a nagyon egyszerűnek kinéző eszközt (JPA). Rá kellett döbbenjünk, hogy a "nagyon egyszerű" megfogalmazása a JPA esetén nagyon félrevezető, ugyanis korántsem fedi el az ORM problémákat. Ugyanazokkal a kihívásokkal kell foglalkoznunk mint eddig is, csak kicsit másképp.
VálaszTörlésMi akkor felaszámoltuk az entitás (JPA) rétegben levő kapcsolataink többségét. Egyirányosítottuk a kapcsolatokat, annak elenére, hogy 1-1 helyen a kétirányú indokolt lett volna. Majdnem mindent átmozgattunk lusta kiértékelésre, s bevezettünk egy amolyan DTO szerű réteget, ami ugye itt már indokolatlan lenne. A helyzet sokkal jobb lett, de nyertünk mi a JPA-val bármit is?
Azóta eltelt már több mint egy év, és a probléma nem hagyott nyugodni ezért mindenhol keressgéltem. Most, olvasva a Eric Evans: Domain Driven Design című könyvet, kezdem úgy érezni, hogy a probléma forrása nem a JPA, hanem inkább az ahogyan mi a rendszert felépítettük (design). Persze majd a következő rendszer lesz ennek a megmondója...
Egyébként a probléma egyik fő oka talán az, hogy a fejlesztők hajlamosak végtelen erőforrásokban gondolkodni... akarom mondani: nem gondolkodni... :-)
Nem rossz ötlet amit írtál, felszámolni a kapcsolatokat, és csak tábla szinten használni a JPA-t. Bár nem lesz olyan kényelmes, mint amilyen lehetne, viszont nyerni még így is nyersz néhány dolgot:
VálaszTörlés- adatbázis szerver független lesz a megoldásod
- az SQL-t objektumok alá rejted, így nem kell hosszú insert/update-eket írni, és hibát sem tudsz véteni ezekben
Szóval a JDBC-nél még mindig sokkal jobban jössz ki, de az eredeti “teljesen transzparens adatbázis kezelés” filozófiánál (ami az ORM-et ihlette) rosszabbul. Szerintem az ideális megoldás a perzisztenciára valami olyasmi, amit a későbbi post-okban fejtegettem, memóriában tartott sima POJO-k, amiket valamilyen háttérmechanizmus képez le teljesen transzparensen valamilyen perzisztens tárolóra. Az ORM valahogy az adatbázisból indul ki, arra próbál ráhúzni valamit, míg ez a megoldás a programhoz kötődő adatstruktúrákból, és ezt próbálja leképezni, és szerintem ez a helyes hozzáállás.
Amúgy a DSL-ekben én is iszonyatosan nagy potenciált látok rengeteg területen, úgy érzem, hamarosan ez fogja meghatározni a szoftverfejlesztést.