A következő címkéjű bejegyzések mutatása: Grails. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: Grails. Összes bejegyzés megjelenítése

2015. február 16., hétfő

Grails és a LazyInitializationException

Amikor valamilyen ORM keretrendszert használunk az alkalmazásunkban, akkor örökösen felmerülő probléma, hogy mely kapcsolatokat töltsük be azonnal, amikor a domain objektumot felszedjük az adatbázisból, és melyeket csak akkor, amikor szükség van rájuk. Grails keretrendszerben a GORM dübörög (vagy nem), ami pedig a népszerű, vagy mondhatnám sokak által használt, Hibernatere épül. Vegyünk egy egyszerű példát:
class Bar {
    static hasMany = [foos: Foo]
    static mapping = {
        foos cascade: 'all-delete-orphan'
    }
}
class Foo {
    static belongsTo = [bar: Bar]
}
Bar bar = Bar.createCriteria().get {
    isNotNull 'id'
    maxResults 1
}
bar.discard()
println bar.foos
A művelet eredménye egy org.hibernate.LazyInitializationException, hiszen nem töltöttük be a fookat a domain osztállyal együtt, a discard metódus hatására pedig elveszett a kapcsolat a Hibernate Session és a domain osztályban lévő proxy között. Javísuk a problémát.
Bar bar = Bar.createCriteria().get {
    isNotNull 'id'
    fetchMode 'foos', org.hibernate.FetchMode.JOIN
    maxResults 1
}
Ez a bejegyzés nem jött volna létre, ha csak ennyivel megúsznánk a dolgot. Bár a Hibernate dokumentációja szerint a FetchMode.JOIN "Fetch using an outer join", mégis ismét egy LazyInitializationException hibára hivatkozva száll el a kérés mint a győzelmi zászló. A problémát a cascade okozza!? Ugyanis ha eltérünk az alap értelmezett viselkedéstől (OTM kapcsolatoknál az alap értelmezett a save-update, tehát a domain osztályt törölve az adatbázisban maradnak a hozzá kapcsolodó entitások), akkor a JOIN hatására nem a domain osztályok kerülnek a domainbe, hanem csak proxyk, tehát akkor szedi fel őket ténylegesen a rendszer, amikor hivatkozunk rájuk. A cascade dokumentációja semmit nem említ erről az igen kellemetlen melléhatásról. Tudom Grailsbe "nem szokás" discard()olni, én mégis azt javaslom, hogy mielőtt átadjuk a vezérlést a nézetnek, csak a próba kedvéért válasszuk le a domaint a sessionről, és győződjünk meg róla, hogy nem fog n+1 lekérdezést indítani a háttérben, miközben mi meg vagyunk róla győződve, hogy minden rendben.

Ahogy említettem FetchMode.JOINnal és nélküle is proxyk jelennek meg a domainbe, egy apró különbség azonban van a két eljárás között. FetchMode.JOIN nélkül az alábbi kódsor hibát eredményez (LazyInitializationException).
Bar bar = Bar.createCriteria().get {
  isNotNull 'id'
  maxResults 1
}
bar.discard()
println bar.foos.size()
FetchMode.JOIN megadásával pedig nem, amiből arra lehet következtetni, hogy van eltérés a két viselkedés között. Előbbi esetben az egész kapcsolatot egy proxy helyettesíti, utóbbinál pedig a domaineket helyettesíti egy-egy proxy.

Lefuttattam a tesztet a Grails legfrissebb verziójával (2.4.4), és minden jel arra utal, hogy már megoldották ezt a problémát. Nem tudom ténylegesen melyik verzióval lett javítva, ezért javaslom mindenkinek, hogy ellenőrizze megfelelően működik-e az alkalmazása, vagy frissítsen a legfrissebb verzióra.

2014. november 20., csütörtök

Változékony Grails alkalmazás konfiguráció facepalm

Az egyik Grails-es alkalmazásunk furán kezdett viselkedni, ezért nyomozás indult a cégen belül, és hamarosan meg is lett a furcsaság forrása. Az egyik gsp-ben szükség volt egy lista típusú konfiguráció kiíratására, de más sorrendben mint ahogy az rögzítve lett. A fejlesztő kolléga fogta és átrendezte az eredeti konfigurációt, amit a Grails készségesen meg is tesz. Felteszem a kérdést, hogy vajon ez a helyes működés? Egyfelől méltán tükrözi a Groovy és a Grails nyílt filozófiáját, és nem mellesleg hatékonyabbá teszi a fejlesztést is, hiszen futás-időben az egész konfiguráció felépíthető az alkalmazás újraindítása nélkül. Másfelől pedig rés a pajzson, hiszen bármelyik futó szál kénye kedve szerint módosíthatja az alkalmazás működését, ami akár az alkalmazás biztonságát is veszélyeztetheti. Számtalan Grails plugin (pl. Spring Security) tárolja ugyanitt a beállításait, és ezen beállítások kulcsai mind nyilvánosak, tehát bárki számára elérhetőek és módosíthatóak. Ha ez nem volna elegendő, akkor a ConfigObject még csak nem is szál-biztos. Amellett, hogy a ConfigObject belső működése nem szál-biztos, a tényleges konfiguráció tárolására LinkedHashMap-et használ, aminek a dokumentációjából idézve:
Note that this implementation is not synchronized. If multiple threads access a linked hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally. This is typically accomplished by synchronizing on some object that naturally encapsulates the map. If no such object exists, the map should be "wrapped" using the Collections.synchronizedMap method.
Lehet én vagyok maradi, de valahogy ez az egész több sebből vérzik. És, hogy egy kis kód is legyen, az úgy még tudományosabb:
class TestController {
        def index() {
                def c1 = grailsApplication.config
                def c2 = grailsApplication.config
                c1.asd = "newconfig"
                render c2.asd
        }
}