A következő címkéjű bejegyzések mutatása: Lazy. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: Lazy. Ö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. július 30., szerda

Groovy funkcionális eszközök

A Groovy sorozatot folytatva (1, 2) ebben a bejegyzésben a nyelv funkcionális aspektusát szeretném bemutatni a teljesség igénye nélkül. Két fontos tulajdonság képezi az alapját a funkcionális programozásnak Groovyban, az egyik, hogy van lehetőség anonim funkciók (Closure) írására, a másik pedig, hogy az utolsó kifejezés értéke automatikusan visszatérési érték lesz, ha nincs explicit visszatérési érték meghatározva (üres Closure visszatérési értéke null). Ez a két tulajdonság elengedhetetlen ahhoz, hogy funkcionális szemléletben tudjunk programozni, de pusztán e két dolog használata még nem eredményezi automatikusan a szemlélet-váltást. Vegyük sorra milyen egyéb eszközökkel támogatja a Groovy a munkánkat.
  • Először nézzük a Closure összefűzést:
    def m = { "${it}-" }
    def p = { "${it}+" }
    def pm = m << p
    
    println pm('') == m(p('')) // true
    
    Természetesen a másik irányba is működik a dolog:
    def mp = m >> p
    println p(m('')) == mp('') // true
    
  • A Closure.curry() metódus becsomagolja a Closure példányt, és az eredeti Closure paraméterei helyére lehet fix értékeket beállítani. A példa magáért beszél:
    def plus = { int f, int s, int t ->
        println "$f $s $t"
        return f + s + t
    }
    def fix = plus.curry(0, 0) // további opciók: ncurry() és rcurry()
    println fix(5) // 0 0 5
    
  • Felmerülhet az igény, hogy már meglévő metódusokból csináljuk Closuret. Nem a legelegánsabb megoldás, de mindenféleképpen hasznos ha meglévő eszközeinket szeretnénk "modernizálni":
    class o {
        void f() {
            println 'called'
        }
    }
    
    def c = o.&f // vagy new o().&f
    
    println c.class // class org.codehaus.groovy.runtime.MethodClosure
    
  • A funkcionális programozásra igen jellemző a rekurzív végrehajtás, és ezen programozási nyelvek részét képezik a különböző optimalizációs eszközök. Természetesen a Groovyban is van lehetőségünk finomhangolni rekurzióinkat. Az első ilyen eszköz, amit bemutatok a Closure.memoize(), ami nemes egyszerűséggel annyit tesz, hogy a visszaadott csomagoló Closure gyorsítótárazza a végrehajtás eredményeit. Különös tekintettel kell lennünk használatakor a memória-szivárgásokra, mert alapértelmezetten nincs méret határ szabva a gyorsítótárnak:
    def a = { print "called" }.memoize() // vagy memoizeBetween(int protectedCacheSize, int maxCacheSize)
    a();a() // called
    
    def a = { i -> print "called $i " }.memoize() // vagy memoizeAtLeast(int protectedCacheSize) és memoizeAtMost(int maxCacheSize)
    a(1);a(2);a(2) // called 1 called 2
    
    Meglévő metódusainkat pedig a @Memorized annotációval tudjuk hasonló működésre bírni, mely két opcionális paramétert vár a maxCacheSize és a protectedCacheSize.
  • A rekurzív hívásoknak van egy igen kártékony mellékhatása a JVMben. Minden egyes hívás rákerül a stackre, ami könnyen StackOverflowErrorhoz vezet. Ezt elkerülendő a Closure.trampoline() segítségével referenciát szerezhetünk egy olyan TrampolineClosurera, mely szekvenciálisan hívja az eredeti Closuret egészen addig, míg az TrampolineClosuret ad vissza. Ezzel a technikával mentesíti a stacket, lássuk ezt a gyakorlatban:
    def s
    s = { l, c = 0 ->
        l.size() == 0 ? c : s(l.tail(), ++c)
    }.trampoline()
    
    println s(1..10) // 10
    
    A Closure.trampoline() metódus mintájára az @TailRecursive annotációt használhatjuk, a dokumentációban szereplő megkötésekkel.
  • A funkcionális nyelvek általában az un. lazy evaluation szemléletet követik, ami röviden annyit jelent, hogy csak akkor értékel ki a rendszer valamit, ha arra feltétlenül szükség van. A Groovy is biztosít megoldásokat a paradigma követéséhez.
    def l = [].withDefault { 45 }
    println l[3] // 45
    println l // [null, null, null, 45]
    
    Vagy a @Lazy annotációval tetszőleges adattagot varázsolhatunk lusta kiértékelésűre. Egy opcionális paraméterével akár puha referenciában is tárolhatjuk az értéket, természetesen az alapértelmezett működés szerint erős referenciát alkalmaz:
    class Person {
        @Lazy(soft = true) pets = ['Cat', 'Dog', 'Bird']
    }
    
    def p = new Person()
    println p.dump() // <Person@7b073071 $pets=null>
    p.pets
    println p.dump() // <Person@18e30556 $pets=java.lang.ref.SoftReference@3f0e6ac>
    
    Annyit mindenféleképpen meg kell még jegyeznem, hogy ha a mező statikus, és nem puha referenciában történik a tárolás, akkor a Initialization on demand holder mintát követi a fordító.
A beépített funkciók után térjünk át a haladó technikákra. Bár a Groovynak szoros értelemben nem része a GPars keretrendszer, mégis érdemes kicsit közelebbről megismerkedni vele. A dokumentációból idézve:

"GPars is a multi-paradigm concurrency framework, offering several mutually cooperating high-level concurrency abstractions, such as Dataflow operators, Promises, CSP, Actors, Asynchronous Functions, Agents and Parallel Collections."

  • Meglévő Closurejainkat könnyedén aszinkron hívásokká alakíthatjuk a GParsExecutorsPool segítségével, ahogy a példa is mutatja.
  • Collectionök párhuzamos feldolgozására a GParsPoolt tudjuk segítségül hívni. A GParsPool osztály ParallelArray alapon végzi a műveleteket, míg párja a GParsExecutorsPool hagyományos thread poolokat használja.
  • A GPars része egy a Java Fork/Join könyvtárára épülő magasabb absztrakciós réteg. Ez a köztes réteg -mely a mindennapi fejlesztést hivatott megkönnyíteni- használata során nem kell szálakkal, poolokkal, és szinkronizációval bajlódnunk. Részletek a dokumentációban találhatók.
  • A Dataflow egy alternatív párhuzamos feldolgozási szemlélet. Szépsége az egyszerűségében rejlik, apró párhuzamos feladatokra bonthatjuk az alkalmazásunkat, és amikor az egyik darabka még egy ki nem értékelt adathoz szeretne hozzáférni, akkor blokkolt állapotba kerül amíg egy másik ki nem értékeli azt. Működéséből adódóan nincs verseny helyzet, nincs Dead és Live Lock sem többek között. Megkötés, hogy csak egyszer adhatunk értéket a DataflowVariable életciklusa során.
  • Az Agentek szál-biztos, nem blokkoló párhuzamosítást tesznek lehetővé, és ehhez annyit kell tennünk, hogy az osztályunkat a groovyx.gpars.agent.Agentből származtatjuk. Fontos különbség a Dataflow modellhez képest, hogy az Agent tartalma tetszőlegesen változtatható.
  • Természetesen elmaradhatatlan kellék a méltán népszerű Actor modell. Leegyszerűsítve az Actorok üzeneteket fogadnak, továbbítanak, és válaszolnak azokra. Minden üzenet bekerül egy sorba, majd onnan a feldolgozásra. A megoldás előnye, hogy implicit szál-biztos, nem blokkolt, és nincs szükség szinkronizációra sem a soros feldolgozás miatt. Lényeges tulajdonsága az Actor rendszernek, hogy nem hagyományos szál-kezelést használ, hanem saját maga menedzseli a feladatokat. Létezik állapot-tartó, és állapot-mentes Actor egyaránt.
Ahogy a Barátkozás a Groovyval bejegyzésben is leírtam, a Groovy nem kezdő programozók kezébe való eszköz. Szép és jó ez a sok beépített okosság, de az alapok ismerete nélkül csak még jobban összezavarnak mindent, megnehezítik a hiba feltárást és az elhárítást is. Remélem sikerült kedvet csinálnom a téma mélyebb megismeréséhez, ráadásként pedig egy igazi "ínyencséget" hagytam:
def deliver(String n) {
 [from: { String f ->
     [to: { String t ->
         [via: { String v ->
             println "Delivering $n from $f to $t via $v."
         }]
     }]
 
 }]
}
deliver "Béla" from "Mezőberény" to "Kisfái" via "Traktor" // Delivering Béla from Mezőberény to Kisfái via Traktor.