17 kwietnia 2009

Z rozdziału 10. o Grails ORM (GORM) z DGG2

Początek rozdziału 10. GORM w "The Definitive Guide to Grails, Second Edition" (DGG2) to ponowne przyjrzenie się mechanizmom odwzorowywania świata obiektowego na relacyjny przez Grails za pomocą Grails ORM (ang. Grails Object-Relational Mapping), zwanego również jako GORM. Graeme i Jeff dają popalić już na pierwszych stronach, w których znalazłem kilka nigdy wcześniej nie słyszanych przeze mnie słówek i zwrotów angielskich, chociażby to garner (z interesującym trybem przypuszczającym - "As you may have garnered from the table of contents for this book") oraz to plunge headfirst into (w "you're going to plunge headfirst into understanding the inner workings of GORM"). Już to nie pozostawia złudzeń, że nie tylko Grails będzie tematem do poznawania ;-)

W Grails każda klasa dziedzinowa (ang. domain class) jest automatycznie rozszerzana (dzięki mechanizmowi metaprogramowania z Groovy) o metody, które pozwalają na wykonywanie zapytań zwracanych jako konkretne egzemplarze (instancje). Aby się o tym przekonać możemy skorzystać z podpowiadania w dowolnym IDE wspierającym Grails, np. NetBeans czy IntelliJ IDEA, bądź uruchomić konsolę Grails - poleceniem grails console - i skrypt (zakłada się dostępność klasy dziedzinowej Ksiazka):
 import pl.jaceklaskowski.grails.*

// utworzenie egzemplarza klasy dziedzinowej, np. Ksiazka
def args = [tytul: "O Grails", autor: "Jacek Laskowski"]
def ksiazka = new Ksiazka(args)

// wypisanie wszystkich dostępnych metod
def i = 0
ksiazka.metaClass.methods.each { metoda -> println "${++i}. $metoda" }

i
W wyniku otrzymamy aż 45 metod (nie wiem, jak wyświetlić dodatkowe metody, np. findBy, ale one mogą być obsługiwane przez metody methodMissing)

Typowym użyciem generowanych metod jest bodaj najpopularniejsza z nich, statyczna get() (jej również nie ma powyżej?!), która przyjmuje identyfikator klasy dziedzinowej i zwraca jej egzemplarz lub null.
 def ksiazka = Ksiazka.get(params.id)
Można również skorzystać z wersji rozszerzonej getAll(), która akceptuje serię identyfikatorów (lista lub varargs) i zwraca listę egzemplarzy.
 def ksiazki = Ksiazka.getAll(1,2,3)
Wczytanie egzemplarza klasy dziedzinowej w trybie do-odczytu, tj. zmiany nie zostaną utrwalone w bazie danych, to metoda read().
 def ksiazka = Ksiazka.read(params.id)
GORM udostępnia metodę list(), której parametry wejściowe określają liczbę zwracanych egzemplarzy (argument max), miejsce początkowe (offset) oraz sposób ich uporządkowania (sort oraz order z wartościami desc oraz asc).
 def ksiazki = Ksiazka.list(max:10, sort: 'tytul', order: 'desc')
Metoda count() zlicza wszystkie dostępne egzemplarze w bazie danych (w zasadzie powinienem napisać rekordów w bazie danych).

GORM udostępnia odmianę metody list - listOrderBy* dla każdego atrybutu klasy dziedzinowej, np. listOrderByTytul() czy listOrderByAutor().

Metoda save() zapisuje egzemplarz w bazie danych (zakładając, że kontrola poprawności danych zakończy się pomyślnie). W zależności od zmiany Hibernate (podstawa technologiczna GORM) wykona INSERT albo UPDATE. Dla niektórych baz danych może być konieczne jawne wskazanie INSERT - parametr insert z true/false, np.
 ksiazka.save(insert:true)
Skasowanie egzemplarza to delete().

W typowym zastosowaniu GORM związek 1-* oparty jest o java.util.Set. Jeśli chcielibyśmy zagwarantować porządek możemy wskazać Grails, aby związek oparty był na SortedSet (nie pozwala na duplikaty i utrzymuje porządek), przez jawne wskazanie typu dla atrybutu odpowiadającego związkowi, np.
 class Ksiazka {
...
SortedSet autorzy
}
SortedSet wymaga, aby umieszczane egzemplarze pochodziły od klasy implementującej Comparable (z metodą int compareTo(o)).

Możemy również wskazać porządek w zmiennej statycznej mapping z sort, np.
 class Ksiazka {
...
static mapping = {
sort "nazwisko"
}
}
Jeśli jedynie wybrane zapytanie czy związek ma podlegać specjalnemu porządkowi wskazujemy interesujący nas związek przez jawne podanie go w mapping z sort, np.
 class Ksiazka {
...
static mapping = {
autorzy sort "nazwisko"
}
}
Można również podeprzeć się deklaracją związku opartej na java.util.List (pozwala na duplikaty i zachowuje porządek wstawiania). Możliwe jest również zdefiniowanie związku na mapie, z java.util.Map.

GORM udostępnia metody addTo* oraz removeFrom* do, odpowiednio, dodawania i usuwania egzemplarzy ze wskazanego związku zwracając egzemplarz klasy dziedzinowej, na której zostały wywołane (kłania się koncepcja "fluent API"). Możemy, więc stworzyć dosyć interesujące konstrukcje programistyczne, gdzie zamiast podawać/powoływać egzemplarz i przypisywać/usuwać ze związku GORM zrobi to za nas, np. (Listing 10-8):
 new Album(title:"Odelay",
artist:beck,
year:1996)
.addToSongs(title:"Devil's Haircut", artist:beck, duration: 342343)
...
.save()
Zmiany w egzemplarzach mogą być propagowane "w dół" (kaskadowane) do egzemplarzy w związkach. Określenie propagowania zmian jest za pomocą właściwości belongsTo (ustala stronę wiodącą w związku na wskazaną klasę dziedzinową), np.
 class Zona {
// mąż
Maz maz
}

class Maz {
static belongsTo = [zona:Zona]
}
Od razu wiadomo, kto rządzi w tym związku ;-) W takiej konfiguracji, skasowanie Zona kasuje egzemplarze Maz. Bez belongsTo, jedynie zapisy i aktualizacje są kaskadowane, ale nie kasowanie. Za pomocą metody cascade w bloku mapping określamy zachowanie kaskadowania, np.
 class Zona {
Maz maz
static mapping = {
maz cascade:'save-update'
}
}
Istnieje specjalny rodzaj kaskady delete-orphan dla przypadku, kiedy chcemy skasować obiekt zależny po usunięciu go ze związku bez jawnego skasowania.

W GORM mamy do dyspozycji budowane dynamicznie zapytania typu find (ang. dynamic finders). Ich działanie polega na tworzeniu zapytań odpowiadających nazwie metody findBy z dołączonymi atrybutami klasy dziedzinowej z kwalifikatorami - Between, Equals, GreaterThan, GreaterThanOrEqual, InList, IsNull, IsNotNull, LessThan, LessThanOrEqual, Like oraz NotEqual, które z kolei łączymy w związki logiczne (z And lub Or) z parametrami wejściowymi będącymi koniecznymi do działania zapytania wartościami, np.:
 Ksiazka.findByTytulLike('%Grails%')

def dzisiaj = new Date()
Ksiazka.findByDataWydaniaBetween(dzisiaj-10, dzisiaj)

Ksiazka.findByTytulLikeAndDataWydaniaBetween('%Grails%', dzisiaj-10, dzisiaj)
Podobnie działają dynamicznie tworzone metody findAllBy* oraz countBy*. grails console naszym sprzymierzeńcem, aby rozeznać się w możliwościach dynamicznych zapytań w GORM. W ten sposób, pozbywamy się warstwy DAO na dobre, którą staje się...GORM.

Na dzisiaj wystarczy tego GORMowego szaleństwa. Dokończenie w kolejnym wpisie.