26 stycznia 2009

Model i usługi w Grails - rozdział 6. z "Beginning Groovy and Grails"

Po bardzo intensywnym rozdziale 5 o tworzeniu interfejsu użytkownika w Grails (patrz: Tworzenie interfejsu użytkownika w Grails - rozdział 5 z "Beginning Groovy and Grails") przyszła pora na niemniej intensywny rozdział 6. "Building Domains and Services" w książce Beginning Groovy and Grails: From Novice to Professional. Tym razem (jedynie) 50 stron, z niewielką dawką zrzutów ekranów, co daje bardzo merytoryczny rozdział (aż do bólu). Kiedy kilkakrotnie czytałem wczorajszą moją relację miałem dosyć. Zasada, że relacja podnosi ilość zapamiętanej informacji działa znakomicie, ale mam wrażenie, że jak na relację była zbyt obszerna. Może gdybym opuścił moje wtrącenia?! Hmmm, może. Ta nie będzie jednak inna. Ostrzegałem.

Rozdział 6. rozpoczyna się nawiązaniem do filmu..."Monty Python and the Holy Grail", w którym my występujemy jako rycerze króla Artura i czytając tę książkę o Grails mamy nadzieję znaleźć go również. W tym sensie nasza wyprawa zmierza ku krainie szczęśliwości grailsowej - jego fundamentom do budowania modelu aplikacyjnego (nic nie przychodzi mi do głowy poszukując tłumaczenia dla angielskiego domain poza "dziedziną", która nie wydaje mi się bardziej twórcza od "modelu"). Ponownie pojawia się odwołanie do Ruby on Rails, zaś inne szkielety webowe nazywając "other web frameworks" (jeszcze trochę, a rzucę Grails i rozpocznę rozpoznanie tak promowanego, nawet w książce o Groovy & Grails, Ruby on Rails ;-)). Później pojawia się nawiązanie do kolejnego tematu rozdziału - usługach, które w przeciwieństwie do kontrolerów nie mają świadomości webowej (zero gwarantowanego dostępu do sesji czy parametrów żądania i nie mogą być bezpośrednio wywoływane z GSP).

GORM to dla Grailsów mechanizm utrwalania danych jak ActiveRecord dla Ruby on Rails (znowu o nim!), czy JPA dla Java EE. Na długo mnie zmroziło, kiedy kontynuując te porównania przeczytałem "WebSphere uses iBATIS". Że co?! Mariusz i inni ewangeliści iBATIS byliby zachwyceni, ale w którym produkcie z rodziny IBM WebSphere znajduje się iBATIS? Na razie wciąż nie mogę wyjść z podziwu dla potrzeby umieszczenia tego zdania (które w/g mnie jest nieprawdziwe).

GORM opiera swoje działanie na Hibernate i tak na prawdę opakowuje go grailsowym DSLem. W/g autorów GORM to połączenie najlepszych cech ActiveRecord z Hibernate, więc znasz jedno z nich i masz 95% wiedzy o GORM.

Pojęcie relacji między klasami domenowymi wyraża się w GORM za pomocą DSLa - klasa A należy do (belongsTo) B lub klasa A ma wiele (hasMany) B. Wszystko.

Wcześniej już relacjonowałem, że GORM automatycznie dodaje pola id oraz version. Pole id to automatycznie generowany identyfikator, za pomocą którego GORM potrafi związać odłączone (ang. detached) encje^H^H^Hklasy dziedzinowe z tymi utrwalonymi. Za pomocą pola version udostępniony jest mechanizm optymistycznego blokowania.

Dowiedziałem się przy okazji o nowym nazewnictwie dla stylu nazwa_pola_w_bazie, który nazywa się snake_case (podobnie jak CamelCase dla sposobu nazywania klas w Javie - hmmm, a jak mogłoby to być po polsku?!).

Mimo, że GORM to w dużej mierze JPA nie ma co liczyć na wiele podobieństw. Można powiedzieć, że jest tyle różnic, a w zasadzie uproszczeń, że wiedza, że GORM opiera się na Hibernate i JPA na niewiele się zdaje. Najlepiej o tym zapomnieć i nie sugerować się niedoskonałościami JPA/Hibernate. Pewnie niejednego zdumiało moje ostre nastawienie do JPA, ale po zapoznaniu się z uproszczeniami w GORM zastanawiam się, dlaczego trzeba było tak skomplikować JPA. W GORM mamy wyłącznie hasMany oraz belongsTo.

Relacja jeden-do-jednego (1-1) to po prostu użycie jednej klasy dziedzinowej w drugiej (za Listing 6-4 w książce):
 class User {
Address address
}

class Address {
User user
}
Relacja jeden-do-wielu (1-*) modelowana jest przez static hasMany, z listą, w której podaje się nazwę pola i typ oraz belongsTo po stronie wiele (w celu nałożenia ograniczenia w bazie danych przy generowaniu schematu), np.
 class Uzytkownik {
static hasMany = [konta: Konto]
}

class Konto {
static belongsTo = Uzytkownik
}
Relacja wiele-do-jednego (*-1) to nic innego jak odwrócenie relacji jeden-do-wielu i GORM nie wprowadza żadnych specjalnych mechanizmów dla niej (w przeciwieństwie do adnotacji @ManyToOne w JPA!).

Relacja wiele-do-wielu (*-*) to umieszczenie słowa kluczowego hasMany w klasach dziedzinowych po obu stronach relacji, z belongsTo w jednej z nich, np.
 class Blog {
static hasMany = [tagi: Tag]
}

class Tag {
static hasMany = [blogi: Blog]
static belongsTo = Blog
}
Podczas tworzenia schematu bazodanowego lub mapowaniu klas dziedzinowych do istniejącego będziemy potrzebować mechanizmu jego definicji. W GORM mamy do dyspozycji niskopoziomową konfigurację Hibernate (niezalecane) lub preferowany GORM DSL z static mapping:
 class Konto {
static mapping = {
// sekcja odwzorowania modelu obiektowego na relacyjny (mapowanie)
}
}
Zmiana nazwy tabeli - table '<nowa-nazwa-tabeli>'

Zmiana nazw kolumn w tabeli:
 columns {
nazwa column: 'pelna_nazwa'
imie column: 'pierwsza_nazwa'
}
Zmiana sposobu generowania identyfikatorów klucza głównego - id generator: 'hilo', params: [...]. Zmiana na klucz złożony: id composite: ['nazwa', 'imie']

Nie zaleca się zmiany prostego klucza głównego na złożony, gdzie głównym powodem jest...Hibernate, który działa najwydajniej z kluczem prostym.

Wyłączenie wersjonowania - version false.

Pobieranie danych w GORM odbywa się z opóźnieniem. Zmiana na gorliwe ładowanie (wczytywanie):
 columns {
tagi lazy:false
}
Ustanowienie indeksu dla danej kolumny - nazwa index:'Nazwa_Idx'.

Domyślnie GORM hierarchi dziedziczenia przypisuje pojedyncza tabelę (co uniemożliwia nałożenie warunków niepuste czy klucz główny) z kolumną class jako wyróżnikiem do jakiego typu dana krotka należy. Istnieje możliwość zmiany na tabela per klasa (co skutkuje kopiowaniem danych z klasy bazowej oraz łączeniami - joins - do wczytania wszystkich użytkowników) przez tablePerHierarchy true.

Włączenie pamięci podręcznej 2. poziomu (ang. second-level cache) to zmiana w DataSource.groovy (ustawienie parametrów cache.* w sekcji hibernate na true) oraz dodaniu cache true do sekcji static mapping w klasie dziedzinowej (domyślnie read-only oraz non-lazy). Można również określić inne parametry pamięci podręcznej - cache usage:'read-write', include:'lazy'.

Wyłączenie atrybutów klasy dziedzinowej z mechanizmu utrwalania (ustanowienie ich ulotnymi/nietrwałymi - ang. transient properties) - static transients = ["nazwaPolaUlotnego", "kolejne"].

W GORM istnieje mechanizm zdarzeń - zdarzenie przed zapisem, aktualizacją i skasowaniem klasy dziedzinowej w bazie oraz po jej wczytaniu, odpowiednio beforeInsert, beforeUpdate, beforeDelete oraz onLoad. Jeśli stworzymy pola lastUpdated oraz dateCreated będą one automatycznie aktualizowane przez GORM. Wyłączenie ich to autoTimestamp false w static mapping.

Ograniczenia (warunki) jakie musi spełniać klasa dziedzinowa wyrażane są w sekcji static constraints. Poza warunkami określa porządek wyświetlania pól przy wykorzystaniu mechanizmu rusztowania (scaffolding), np.
 class Wpis {
static constraints = {
tytul(nullable:false)
autor(blank:false)
data()
}
}
blank:false = nullable:false + pole nie może być ciągiem o długości 0

Dostępne ograniczenia to blank, creditCard, email, inList, matches, max, min, minSize, maxSize, notEqual, nullable, range, scale, size i url.

Istnieje możliwość stworzenia własnych za pomocą atrybutu validator, po którym następuje treść ograniczenia (jako domknięcie). Wynik true/false określa poprawne/niepoprawne spełnienie warunku. Atrybut it wskazuje na kontrolowane pole (jak to w domknięciu).
 static constraints = {
tytul(nullable:false,
validator: {
println it
return true
})
}
Istnieje możliwość zdefiniowania własnego warunku z parametrami val i obj, które są, odpowiednio, nową wartością pola i samym obiektem klasy dziedzinowej.

Bezpośrednie wywołanie walidacji to wywołanie metody validate() na obiekcie klasy dziedzinowej, np.
 assert true == wpisOGrails.validate()
W Grails istnieje hierarchia komunikatów walidujących, z bardziej ogólnych, niezwiązanych z danym ekranem, do bardzo szczegółowych z nazwą klasy dziedzinowej i pola, np. (za Table 6-3) todo.name.blank.error.Todo.name czy po prostu blank. Komunikaty walidujące zapisane są w zewnętrznym pliku komunikatów (podobnie jak komunikaty dla znacznika GSP - g:message) - domyślnie w messages.properties. Do wyróżnienia własnych walidatorów dodajemy validator, np. wpis.tytul.validator.error.Wpis.tytul byłby najbardziej szczegółowym.

W GORM nie ma potrzeby tworzenia klas DAO z void delete(KlasaDziedzinowa k), void save(KlasaDziedzinowa k) czy podobnie. Każda klasa dziedzinowa ma zapewnione metody save(), get() oraz delete() automatycznie. Po prostu są i tyle. Dzięki temu możemy klasę dziedzinową wciąż nazywać lekką mimo połączonej funkcjonalności klasy dziedzinowej i DAO.

W Grails wyróżnia się trzy typy zapytań - dynamiczne zapytania GORM, zapytania HQL oraz zapytania kryteriowe (ang. Hibernate's criteria queries). GORMowe zapytania, podobnie jak kryteriowe, są ograniczone do zapytań dla pojedycznej klasy.

Możemy zliczać - <klasa-dziedzinowa>.count() oraz zliczać po wybranym polu - <klasa-dziedzinowa>.countBy<NazwaAtrybutu>(<wartość>). W podobnym schemacie konstruowane są zapytania pojedynczego rezultatu - findBy i findWhere - oraz zapytania wielokrotnego rezultatu - findAllBy, findAllWhere, getAll, listOrderBy i list, tj.
 <klasa-dziedzinowa>.<typ-zapytania><Pole1><typ-wiązania><Pole2>...(<lista-wartości>)
, np.
 Wpis.findByTag('grails')
Wpis.findByTagAndTitle('grails','%Groovy and Grails%')
Wpis.findByTagAndTitleOrDate('grails','%Groovy and Grails%','15.01.2008')
i tak dalej.

Można również tak (Listing 6-29):
 Todo.findWhere([priority:"1", status:"2"])
albo (Listing 6-32):
 Todo.getAll(1,3,5)
gdzie pobieramy klasy dziedzinowe odpowiadające identyfikatorom 1, 3 i 5.

Możemy również zawężać interesujące nas rekordy za pomocą filtrów, np. (Listing 6-35)
 Todo.list(max: 10, offset: 20, sort: "priority", order "desc")
Po tej prezentacji zapytań GORMowych pojawia się ciekawe podsumowanie: "using these queries is like eating sushi - an hour later, you're hungry for more". I pojawiają się zapytania HQL z find, findAll oraz executeQuery. Wiedza HSQ przydaje się. Później pojawiają się zapytania kryteriowe ze wstawkami DSLowymi, np. (Listing 6-40)
 List executeCriteriaQuery(def params) {
def todos = Todo.createCriteria().list {
and {
params.each { key, value ->
like (key, value)
}
}
}
}
Wyjaśnienie, co robi powyższe zapytanie pozostawiam dociekliwym. Przykład ma jedynie dać pojęcie możliwości GORM - połączenia cech Hibernate z dynamicznością Groovy.

Na zakończenie sekcji dotyczącej modelu autorzy przedstawiają dwie wtyczki grailsowe upraszczające wprowadzanie zmian między klasami dziedzinowymi a schematem bazodanowym - dbmigrate oraz LiquiBase. Zainteresowani są kierowani do dokumentacji dostępnej chociażby przez grails plugin-info. Podobno w Rails utrzymywanie synchronizacji między klasami a schematem jest bardziej zaawansowane niż w Hibernate, więc nie ma innej możliwości dynamicznej aktualizacji poza stworzeniem bazy danych i aktualizacji (nie wiem, czego jeszcze mógłbym oczekiwać, ale pewnie w dokumentacji RoR jest to wyjaśnione - udam, że mnie to nie interesuje ;-)).

W końcu przechodzimy do sekcji usługowej - omówienia usług w Grails. Klasy usług w Grails nie mają bezpośredniego dostępu do otaczającego ich środowiska webowego ze wszystkimi bajerami typu sesja, żądanie, itp., jak to ma miejsce przy kontrolerach, oraz jedynie kontroler może być wywołany z GSP. Klasy usług umieszczamy w katalogu grails-app/services. Zaletą "wynoszenia" funkcjonalności do klas usługowych jest możliwość kontrolowania obszaru transakcyjnego oraz kontekstu.

Tworzymy serwis poleceniem grails create-service <nazwa-usługi> i otrzymujemy klasę usługi z testem. Domyślnie transakcje są włączone w trybie PROPAGATION_REQUIRED przez
 boolean transactional = true
w klasie usługi <nazwa-usługi>Service.

Dostęp do usługi możliwy jest za pomocą mechanizmu wstrzeliwania zależności (ang. DI - Dependency Injection), który sprowadza się do zadeklarowania zmiennej z nazwą odpowiadającą nazwie klasy usługi, np.
 def todoService
Jeszcze do niedawna Seam nie był dla mnie szkieletem DI, ale po ostatnich rozmowach podczas WarsJava 2008 i lekturze kilku wpisach na blogach zmieniłem zdanie. Dodatkowo, i w tej książce Seam występuje w jednym zdaniu z Spring, HiveMind oraz "any other injection framework out there".

Istnieje możliwość wstrzelenia usługi do innej usługi oraz dowolnego ziarna springowego zdefiniowanego w spring/resources.xml przez jego identyfikator (atrybut id w bean).

Jeśli chcemy wykonać pewne czynności podczas uruchamiania usługi korzystamy ze springowego org.springframework.beans.factory.InitializingBean i jego metody afterPropertiesSet().

Na koniec pojawia się temat dostępnych kontekstów (wykonania) dla usług - prototype, request, flash, flow, conversion, session oraz singleton (domyślny). Zmiana kontekstu (zasięgu) możliwa jest przez
 static scope = "flow"
w klasie usługi.

W ostatnim komentarzu, już w podsumowaniu, zaleca się "odwiedzić" kod źródłowy GORM i w ogóle Grails, aby poznać faktyczną ich moc. Może kiedyś.

4 komentarze:

  1. "Dowiedziałem się przy okazji o nowym nazewnictwie dla stylu nazwa_pola_w_bazie, który nazywa się snake_case (podobnie jak CamelCase dla sposobu nazywania klas w Javie - hmmm, a jak mogłoby to być po polsku?!)"

    * Konwencja Wężowa, Konwencja Wielbłądzia :)
    * Konwencja Pełzająca, Konwencja Pagórkowata :)

    OdpowiedzUsuń
  2. Nie ma to jak dobry humor! Głosuję za pierwszą parą - wężowa/wielbłądzia.

    OdpowiedzUsuń
  3. Cześć,
    Bardzo fajne te Twoje notki o Grails (zainspirowały i mnie do pobawienia się tym frameworkiem)!
    Taka mała uwaga odnośnie nomenklatury: zauważyłem, że często używasz określenia "relacja" gdy piszesz o związkach między tabelami. Chyba nie jest to do końca poprawne, bowiem w relacyjnych bazach danych, relacją jest właśnie tabela (stąd mapowanie obiektowo-relacyjne oznacza mapowanie obiektów na tabele), a związki między tabelami (relacjami), to po prostu związki, albo powiązania.

    http://pl.wikipedia.org/wiki/Model_relacyjny
    http://bobo.fuw.edu.pl/DB/OLD/wyklad5.html#relacyjny

    Choć może niepotrzebnie się czepiam, bo może istnieją różne szkoły w tym temacie (nie zdziwiłoby mnie to) ;)

    Pozdrawiam

    OdpowiedzUsuń
  4. @xis: Zgoda. Od dzisiaj będą powiązania, albo po prostu związki. W ten sposób będzie zdecydowanie poprawniej. Konstruktywna uwaga, która pojawiłaby się od wielu, ale tylko Tobie udało się zebrać i zwrócić mi uwagę na to. Gratulacje!

    OdpowiedzUsuń