NHibernate session, transaction, savepoint

Legújabb felfeldezésem:

Egy NHibernate sessionon belül csak egy tranzakció lehet aktív, beágyazott tranzakciók nem hozhatók létre (mondjuk ez nem nagy dolog, mert az adatbázisgép - PostgreSQL - sem szereti, ha egymásba ágyazok több tranzakciót). Viszont! Az NHibernate nem ismer olyan fogalmat, hogy savepoint, tehát egy tranzakció egyáltalán nem bontható fel részmunkaegységekre és ezek nem ágyazhatók egymásba. Direkt SQL paranccsal nyilván életveszélyes, hiszen az NHibernate layer ekkor nem fog tudni róla, hogy ha történik pl. egy rollback vagy commit, tehát a belső állapota inkonzisztens lesz.

Hozzászólások

Ez nem az NHibernate betegsége(savepoint), nem is a Hibernate-é, hanem az összes mainstream perzisztenciamenedzsment rendszeré. Hihetetlen bonyolult a konkurenciamenedzsmentje.
Nagyon rossz szokást alapozna meg amúgy is a megléte, szóval én nem támogatom bármiféle megvalósítását a dolognak. Inkább foglald a fő metatranzakciódat application level transaction-ba és bontsd fel benne elemi perzisztencia tranzakciókra a tényleg tranzakcionális adatbázisműveleteket. Ne tartsd fel feleslegesen a connection pool-t ha nem muszály.

--
- Miért jó a mazochistának?
- Mert ha rossz, akkor jó. Ha meg jó, akkor rossz, tehát jó.

Ja, persze.

Azonban jó, hogy mondod, szembefutottam egy ehhez kapcsolódó problémával.

Van egy (illetve több) entitás, sok property-vel, kapcsolódó entitásokkal, stb., mindez persze szerente lazy lenni, mind a property-k szintjén, mind a kapcsolatok szintjén. Nem véletlenül, mert csomó helyen egyszerűen nincs szükség minden adatára, meg különben is. :-)

Mindez egy WinForms applikáció BLL-ében található.

Az NHibernate doksi szerint (is) ugyebár, nem illik a sessiont nyitvatartani a GUI/user interakciók idejére. Na de akkor az interakciók közben felmerülő lusta adatbetöltéseket (pl. mégiscsak kellene valamelyik kapcsolódó entitás, vagy akár csak egy még be nem töltött property) hogyan oldom meg?

Ha lezárom/eldobom a sessiont még az interakció előtt, akkor kivételt dob (jogosan) amikor egy betöltetlen tagjához akarok hozzáférni, mivel a session lezárásakor még csak a proxy van a memóriában, ami viszont sessionhoz kötött, ami viszont ekkor éppen nem lesz használható.

Ha életben hagyom a sessiont, akkor rossz ember vagyok, mert feleslegesen fogom az erőforrásokat.

Ha úgy csukom be a sessiont, hogy mégiscsak beolvastatok minden érdekes propertyt/kapcsolódó entitást, akkor meg a lazy módnak vész el az értelme, ráadásul előre kellene tudnom, hogy később mire lesz szükség, ez már messziről is hülyeségnek hat.

Ezt hogy szokás kezelni?

Jelenleg azt csinálom, hogy nyitva tartom a sessiont az egész BL folyamat idejére, a megfelelő helyeken tranzakciókat indítva az entitások letárolásához, de ezek mérete minimális. Egy ilyen folyamat akár több user interakciót is magába foglalhat, tehátt huzamosabb időre magához

Bár én nem tudom mi az a WinForms vagy BLL de azt hiszem itt pattern kérdésről van szó. Ha jól értem amit szeretnél és a session itt Hibernate session-t jelent, akkor egyszerűen töltsd be azokat az adatokat, amelyekre szükség van a GUI screen első megjelenítéséhez(mittomén első 10 cikk + cikk counter) aztán szépen zárd be a session-t. => GUI render => user interakció => session open => data load => session close => GUI render =*=> repeat 4ever.
Ne felejtsd el hogy connection pool van! Tehát amikor te lezárod a sessiont(kapcsolatot), akkor nincs hálózati forgalom a DB-vel, sőt nem is sok kód fut le ennek hatására. Tehát kapcsolat(session) nyitása/zárása eszméletlen gyors Hibernate-en keresztül(mert megnyitja egyszer és utána egy nyitás vagy bezárás csak a pool-ból lop vagy visszaad egy már eleve nyitott kapcsolatot).
--
- Miért jó a mazochistának?
- Mert ha rossz, akkor jó. Ha meg jó, akkor rossz, tehát jó.

Hát igen, ez így szép is lenne, de a demand-loading pont arra volna jó, hogy nem kell előre berángatni mindent, csak ami tényleg feltétlenül szükséges. A gond főleg onnan származik, hogy a GUI render és az interakció átfedésben vannak, mivel nem egyszerűen betöltöm az entitás adatait a formba értékadásokkal, hanem DataBinding-el hozzákapcsolgatom az egyes property-ket a gui elemeihez. Ennek akkor van jelentősége, amikor pl. egy dialógusdobozban több oldalon keresztül (pl. TabControl) van részletezve az entitás sok-sok adata. Ha nem megyek át a második/sokadik tab page-re, akkor nem is nyúl hozzá azokhoz a property-khez, amelyek az ottani kontrollokra van rákötve (tehát gyorsabb az egész, persze elméletileg).

(WinForms = Windows.Forms, M$ .NET framework, BLL = Business Logic Layer, tehát az üzleti logikai réteg az N-rétegű alkalmazásban)

Namármost ha nekem előre be kell rántani mindent, akkor azzal két gondom van: egyrészt külön kóddal kell gondoskodni arról, hogy mit kell behúzni (több meló, nagyobb a hibalehetőség, stb.) (a lazy=false, nagyon belassít néhány esetet, tehát eleve elvetendő), másrészt ez így jóval lassabb, mint a mostani megoldás. Bár a teljes berántás módját egyszerűsíteni lehet, ha Lock-olom az entity példányt a session.Load-ban, de ez meg kicsit csúnya, szerintem, és nem is vagyok biztos benne, hogy a kapcsolódó entitásokat behozza-e.

Most úgy vagyok vele, hogy nem baj, ha nyitva van a session, az ütközéseket meg a versioning úgy is kimutatja. Egyébként is szükség van arra, hogy detektálja a program, ha többen próbálják ugyanazt a szerencsétlen adatot megrágni. :-) Ha meg tényleg elfogy a DB connection, akkor örülünk, mert az azt jelenti, hogy már nagyon sokan használják a programot. :-)

Amúgy régebbi, többszázezer ügyfeles, Cobolban/Oracle-ben elkövetett ügyfélszolgálati rendszerben láttam azt, hogy ott is nyitva voltak adatbáziskonnekciók UI interakciók alatt. És mégis szépen ment. Pedig 2-300 online user nyaggatta egész nap... :-)

Hát igen, ez így szép is lenne, de a demand-loading pont arra volna jó, hogy nem kell előre berángatni mindent, csak ami tényleg feltétlenül szükséges.
Így van. Nem is kell berántanod mindent.

A gond főleg onnan származik, hogy a GUI render és az interakció átfedésben vannak, mivel nem egyszerűen betöltöm az entitás adatait a formba értékadásokkal, hanem DataBinding-el hozzákapcsolgatom az egyes property-ket a gui elemeihez. Ennek akkor van jelentősége, amikor pl. egy dialógusdobozban több oldalon keresztül (pl. TabControl) van részletezve az entitás sok-sok adata. Ha nem megyek át a második/sokadik tab page-re, akkor nem is nyúl hozzá azokhoz a property-khez, amelyek az ottani kontrollokra van rákötve (tehát gyorsabb az egész, persze elméletileg).
Gyorsabb, még gyakorlatilag is. De hogy lehetnem egyébként megoldani, hogy a render+interakció ne legyen átfedésben? Teljesen használhatatlan lenne a program. Konkrétan akkor Hello World lenne :D Egyértelmű hogy on-demand laoding kell ide, ez nem is vitatott. Ez alapján a kifejtésed alapján le is esett valami arról hogy mit akarsz, alább írok róla. :)

(WinForms = Windows.Forms, M$ .NET framework, BLL = Business Logic Layer, tehát az üzleti logikai réteg az N-rétegű alkalmazásban)
Nem ismerem a .NET-et, ezért írtam hogy nemtom mi az. A BLL rövidítést még sosem láttam :) a BL-ről akár beugorhatott volna...

Namármost ha nekem előre be kell rántani mindent, akkor azzal két gondom van: egyrészt külön kóddal kell gondoskodni arról, hogy mit kell behúzni (több meló, nagyobb a hibalehetőség, stb.) (a lazy=false, nagyon belassít néhány esetet, tehát eleve elvetendő), másrészt ez így jóval lassabb, mint a mostani megoldás. Bár a teljes berántás módját egyszerűsíteni lehet, ha Lock-olom az entity példányt a session.Load-ban, de ez meg kicsit csúnya, szerintem, és nem is vagyok biztos benne, hogy a kapcsolódó entitásokat behozza-e.
A lock nem hozza be, nem erre való.

Most úgy vagyok vele, hogy nem baj, ha nyitva van a session, az ütközéseket meg a versioning úgy is kimutatja. Egyébként is szükség van arra, hogy detektálja a program, ha többen próbálják ugyanazt a szerencsétlen adatot megrágni. :-) Ha meg tényleg elfogy a DB connection, akkor örülünk, mert az azt jelenti, hogy már nagyon sokan használják a programot. :-)
De baj. Az a baj, hogy te is abban a projektben gondolkodsz csak, amivel most foglalkozol. Ez gányolás. Gondolati gányolás. Refactoring meg jó habitusok kialakítása maximálisan fontos! Szóval maradj csak annál, hogy a session-t lezárod. Hogy mégis hogyan lehet ezt a paradoxont feloldani? Na erre esett le ez a pattern és hogy Hibernate doksiban valahol olvastam én már ilyesmit:
Session-per-conversation Itt alaposan olvasd át a Unit-of-work és a scope-ját. Session detach a megoldás és így nem is foglalsz db connection-t, mivel ilyenkor nincs is tranzakció folyamatban, ergó jó fej vagy. :)

Amúgy régebbi, többszázezer ügyfeles, Cobolban/Oracle-ben elkövetett ügyfélszolgálati rendszerben láttam azt, hogy ott is nyitva voltak adatbáziskonnekciók UI interakciók alatt. És mégis szépen ment. Pedig 2-300 online user nyaggatta egész nap... :-)
De valszeg vettek is alá 600 connection licencet! Vagy kicsengették az enterprise licencdíjat. Manapság ezt igyekeznek elkerülni a fejlesztők, hogy olcsóbban tudjanak szállítani. Másrészt nem minden oprendszer skálázódik túl jól a sok open connection-re. Például Windows nem. És ez komoly performance büntetéssel is járhat...
--
- Miért jó a mazochistának?
- Mert ha rossz, akkor jó. Ha meg jó, akkor rossz, tehát jó.

A lock nem hozza be, nem erre való.
Valóban nem erre való, de ettől még behozza! :-) Kipróbáltam. Ha a Load-nak LockMode.Read-et adok meg, akkor berántja az ojjektumot, lazy ide vagy oda. :-) (Gányfesztivál 2007 nagydíj)

Elolvastam a cikket, értem is (asszem), de a bajom még nem múlt el. :-/
Pont az a probléma, hogy a WinForms-ban ha egy kontrolhoz hozzábindelsz egy property-t, akkor egészen addig nem nyúl a property értékéhez, amíg a hozzá bindolt kontroll nem válik láthatóvá a képernyőn! Viszont amikor láthatóvá válik, akkor már késő, hiszen akkorra a session vagy le van zárva, vagy el van dobva, viszont az entitás még nem töltődött be (csak mint proxy). :-%

Szemléltetésül egy kis kód:



// BL, első session (tehát ez a session-per-request minta, ha jól értelmezem a dolgot)
Cucc cucc; // ez a kerdeses entitas
CuccDialog cuccDialog = new CuccDialog();
using (ISession s = SessionFactory.CreateSession())
{
   s.Load<Cucc>(cuccID); // entity betolt
   cuccDialog .Cucc = c; // A dialogus megkapja a megjelenitendo cuccot es rabindeli a maga 
                         // kis kontrolljait a cucc instance-re, _de_ ettől még nem töltődik 
                         // be a proxy, mivel a konkrét property értékeket nem olvassa ki a 
                         // bindolási eljárás
}

// itt a session már nincs, viszont a cucc még csak mint proxy van jelen,
// tehat az alabbi sor tutira hibat dob:
// c.ValamiJellemzo = 1;

// Es itt menne at a prezentacios layerbe a dolog
PL.ExecuteDialogValidated(cuccDialog);

// Es mivel ez konkretan keri a c mezoit, elszall

// ....

Érted?

Az NHibernate doksi külön írja, hogy a lazy-vel mappelt collection-öket nem is lehet használni lezárt session mellett. Szóval most tényleg nem értem.... :-/ Akkor tehát mindenképpen szakítani kell a DataBindingel és mégis be kell rántani mindent (legalább egy kierőszakolt értékadásssal), ami megjelenhet a dialógusdobozban?

Azt hiszem találtam valamit, bár nem nagyon tecc... Az NHibernateUtil.Initialize() hívással fel lehet tölteni a kollekciókat még a session lezárása előtt, de ez azt jelenti, hogy _minden_ olyan kollekciót inicializálni kell amely szóba jöhet, még mielőtt átkerül az entity a prezentációs rétegbe... Hát... így már nem is olyan szép... :-)

Most már csak a property-k betöltésére kellene valahogy rávenni...

Lassan összerakom hogy mi is a helyzet. :) A lock berántja/nem rántja be - mostmár rájöttem - igaz. Tényleg berántja, mert csak akkor tudja persistenciaszinten lock-olni.
A többinél pedig azt hiszem, hogy értem a problémád, s arra ez nem jó megoldás, kivéve ha a kontrollokat valahogy filterezed vagy újraírod, hogy transparensen attach-olják a detacholt session-t. Ez tényleg gány. Sajnos itt véget ér az én tapasztalatom is, ez konkrétan a WinForms databinding módszerének speciális hülyesége. J2EE-nél kicsit másképp megy ez a huzavona dolog. Ott neked kell alapból intézkedned, hogyha egy magas szintű komponens behúz valamit akkor előtte legyen session attach. Vagy az ilyen komponensek támogatják vagy ezt a típusú attach/detach játékot, vagy lehetőséget adnak rá, hogy mielőtt entity load történik, cselekedj. És ekkoron te attach-eled a session-t és a végén mielőtt befejeződne, detach. Ezt szerintem nem gondolta végig valamelyik fél...
--
- Miért jó a mazochistának?
- Mert ha rossz, akkor jó. Ha meg jó, akkor rossz, tehát jó.

Lassan összerakom hogy mi is a helyzet. :)
Lehet, hogy én nem vagyok elég érthető... :-)

Ez nem lenne hülyeség, mert minek olvasni egy property-t ha nem is jelenik meg, a baj az, hogy amikor már olvasná a szerencsétlen, akkor már nem lehet. Tehát vagy magát a perszisztert kérem meg, hogy mindent olvasson be, vagy valahogy a bindingnek mondom meg, hogy mindent töltsön be. Azt hiszem ezt valahogy meg lehet csinálni. :-)

Na, szenvedtem egy jót ezzel a témával. Igazábó elvi problémám van az egésszel kapcsolatban, főként architektúra szempontjából.

Van egy entitás (legyen mondjuk egy üzenet), pár oszlop, pár kapcsolat, semmi extra, kivéve, hogy van egy hozzá kapcsolódó entitás (legyen mondjuk csatolás), egy-többes kapcsolatban az üzenettel, amely entitás 1 db. BLOB-ot tartalmaz, ráadásul adott esetben nem is kicsit (akár több MiB is lehet egy tétel) és ebből akárhány (jellemzően 0-5 db) kapcsolódik egy üzenethez.

Namármost a problémám az, hogy a kényelem érdekében az üzenet adatait felhozó GUI formban nem csak az alapadatok szerepelnek, hanem a csatolások listája is, mert annak ott a helye (pl. mint egy szokásos levelezőkliensben). A BLOB-okat és listákat is az Hibernate képes on-demand betölteni, és a WinFormos databinding is csak akkor nyúl hozzájuk ha végképp szükséges, ami ideális lenne abban az esetben, ha megengednénk, hogy a session nyitva legyen a GUI interakció ideje alatt. Ekkor mindig csak az éppen szükséges lekérdezések futnának le, és nem nyomulna át több tíz megabájtnyi adat a hálón csak azért, hogy mondjuk egy subjectet megváltoztasson a júzer. :-) Ha viszont tényleg meg akar nyinti egy csatolást, akkor azt is megtehetné, mert a nyitott session-ön keresztül a proxy gyönyörűen berántaná neki amit kell.

Mivel azonban a sessiont nem illik nyitva tartani, ezért ez az út nem járható.

Akkor viszont előre be kellene tölteni az összes BLOB-ot, még mielőtt feldobnám a PL-be az egész adatszerkezetet, hogy minden kéznél legyen a GUI interakció során, tehát rengeteg erőforrást foglalnék le. Ez szerintem szörnyű.

Lehetne persze azt is, hogy a PL-ből vissza nyúlna a kód a BL-be a megfelelő pillanatban, hogy ide nekem a BLOB-okat, de akkor meg az egész koncepciót rúgnám fel, hiszen akkor el kell kezdeni kotorászni a GUI elemek ételében, hogy tulajdonképpen mikor is jut majd el oda a databinding, hogy megpróbálja beolvasni valamelyik még be nem töltött BLOB property-t. Ami egyúttal azt is jelentené, hogy a Hibernate proxy felelősségi körével kapcsolatos feladatok (mikor mi van vagy nincs betöltve) részben átkerülne a GUI-ba... Szóval ez is gány megoldás lenne, nem is kicsit, ráadásul nem kevés pluszmunka.

Ezt az egész kérdést a Java vajon hogyan oldja meg?

Nem tudom, mi a jó megoldás erre. Egyenlőre, minden tervezési ajánlás ellenére nyitva marad a session az interakció idejére, mivel kvázi ingyér van a DB connection (PostgreSQL/Linux szerverrel kell dolgozni) és egyenlőre nem fogják túl sokan használni (max kb. 50).

Persze ki kell találni erre valamit, de még nem látom, hogy mit. Lehet, hogy nem látom a fától az erdőt... :-) Esetleg visszakanyarodok a DataSet-ekhez, ami viszont sosem tetszett nekem...

Azt sem értem, hogy ha a Hibernate doksi session-per-request (ha jól emlékszem így hívja) mintát javasolja, akkor miért nincs olyan üzemmódja a session-nak (vagy olyan ISession implementáció) ami az on-demand jellegű konnekciókat támogatja. Egyáltalán, miért a BL feladata a session-ök menendzselése?
Az én szempontomból csak egyetlen egy érdekes dolog van, és ez a tranzakció. Az összes többinek szerintem nem kellene rám tartoznia, csak ha én tényleg akarnám (két különböző adatbázissal szeretnék beszélgetni, vagy saját magamon belül szeretnék konkurrens hozzzáféréseket).

Persze lehet, hogy nincs igazam ebben a kérdéseben. :-) Majd valaki rámcáfol... :-)