13 lutego 2009

Relacja z rozdziału 3. w "The Definitive Guide to Grails, Second Edition"

Kiedy poznawałem możliwości Grails z perspektywy książki "Beginning Groovy and Grails: From Novice to Professional" sądziłem, że przedstawia ona wszystko, czego mógłbym sobie życzyć. Podchodząc do kolejnej książki o Grails "The Definitive Guide to Grails, Second Edition" jedyne, co gwarantowało sensowność kolejnej lektury, to jej autorzy - liderzy projektu Grails - Graeme Rocher i Jeff Brown oraz wersja, której dotyczyła - Grails 1.1. Miałem pewne obawy, czy dobry programista może być równie dobrym autorem książki informatycznej, ale po kolejnych rozdziałach (już 4 mam za sobą) poznałem tyle nowych cech Grails, które nie znalazły się w pierwszej książce, że mam pewność, że miałem dużo szczęścia rozpoczynając poznawanie Grails w tej kolejności - najpierw "Beginning Groovy and Grails", a teraz "The Definitive Guide to Grails, 2nd Edition". Pewnie wystarczyłaby jedynie ta druga, ale polecam obie i to właśnie w tej kolejności. W ten sposób udaje mi się rozszerzać wiedzę, a nie ją deaktualizować, czy zamazywać. Ta kolejność gwarantuje płynne wejście (przynajmniej teoretyczne) w temat Grails.

Rozdział 3. "Understanding Domain Classes" dotyczy warstwy modelu, który w Grails jest oparty na klasach dziedzinowych (odpowiadają one encjom w JPA). Każda z klas dziedzinowych składa się z właściwości (atrybutów), które domyślnie są mapowane na tabele w bazie danych, aby możliwy był trwały zapis stanu ich egzemplarzy (instancji). Poza kolumnami, które odpowiadają właściwościom klasy dziedzinowej, Grails dodaje kolumny id (unikatowy identyfikator) oraz version (służy do obsługi optymistycznego blokowania).

Warunki jakie muszą spełniać atrybuty klasy dziedzinowej wyrażane są przez statyczną właściwość constraints, której wartością jest domknięcie. Domknięcie może zawierać ograniczenia dla podzbioru atrybutów. Wykonanie save() klasy dziedzinowej to automatyczne wykonanie domknięcia constraints, którego niespełnienie skutkuje błędem kontroli i wstrzymaniem zapisu do bazy danych.

Z klasą dziedzinową związany jest automatycznie generowany atrybut errors - egzemplarz klasy org.springframework.validation.Errors. Jedną z metod interfejsu Errors jest getAllErrors(), więc dostęp do wszystkich błędów kontroli poprawności danej klasy dziedzinowej sprowadza się do:
 <egzemplarz-klasy-dziedzinowej>.errors.allErrors.each { 
// wykonaj operację na pojedynczym błędzie reprezentowanym przez it, np.
println it.defaultMessage
}
Grails dostarcza metodę validate() do każdej klasy dziedzinowej, która zwraca wartość logiczną true, przy poprawnie zakończonej kontroli poprawności. Mamy do dyspozycji również metodę clearErrors(), która czyści zgłoszone do tej pory błędy (błędy są kumulowane).

Poza prostymi warunkami (ograniczeniami) określonymi w domknięciu constraints (w książce wymieniono 19 reguł), możemy zdefiniować własne przez domknięcie validator, która zwraca wartość logiczną true przy poprawnie zakończonej kontroli. Domknięcie validator akceptuje dwa argumenty - pierwszy jest wartością atrybutu do kontroli, a drugi egzemplarz klasy dziedzinowej. Możemy również zwrócić wartość typu String, która będzie identyfikatorem komunikatu błędu (domyślnie <klasa-dziedzinowa>.<atrybut>.validator.error).

Wyłączenie pola z mechanizmu utrwalania (zapisu do bazy) - oznaczenie pola jako ulotne (ang. transient) - to umieszczenie jego nazwy w liście w statycznym atrybucie transients, np.:
 class Wyrazenie {
Integer poleUlotne

static transients = ['poleUlotne']
}
To jest pierwsza z konstrukcji, które nie podobają mi się, bo przede wszystkim wymagają wsparcia IDE, aby nie popełnić błędu - literówki - w nazwie pola ulotnego. Aż się prosi o mechanizm adnotacji, albo stworzenie nowego podobnego do constraints, gdzie podaje się nazwy atrybutów jako metody. Skoro tam można, tu również. Muszę zapytać o to na grupie użytkowników Grails. ...po chwili...Już!

Nie wszystkie atrybuty klasy dziedzinowej muszą odpowiadać polu (instancji) klasy. Wystarczy metoda odczytu (ang. getter) albo zapisu (ang. setter), aby Grails potraktował to jako atrybut klasy. Zasada ulotności działa również i w tym przypadku. Wyłączenie z zapisu to transients z nazwą atrybutu.

Grails, za pomocą GORM, mapuje klasy dziedzinowe bez konieczności plików XML - "objaw" zasady konwencji-ponad-konfigurację. Możliwe jest zdefiniowanie własnego mapowania za pomocą dedykowanego DSL zwanego Custom Database Mapping DSL lub ORM DSL. Warto z niego skorzystać przy wdrażaniu aplikacji z istniejącym schematem bazodanowym, który nie przystaje do domyślnego mapowania w Grails. Użycie ORM DSL jest możliwe przez deklarację publicznego atrybutu statycznego mapping jako domknięcia z column, np. (Listing 3-13):
 class Person {
String firstName

static mapping = {
id column: 'person_id'
firstName column: 'person_first_name'
version false
}
}
Dodatkowo w przykładzie wyłączono wersjonowanie - version false, które służy do optymistycznego blokowania.

Domyślna nazwa tabeli odpowiada nazwie klasy dziedzinowej - zmiana przez table 'nazwa-tabeli' w mapping.

Relacje w Grails - zadeklarowanie atrybutu w klasie dziedzinowej typu innej klasy dziedzinowej to relacja jeden-do-jednego, w której obie strony są równoważne (nie ma właściciela relacji). Określenie strony wiodącej - właściciela relacji - to static belongsTo = [car:Car], gdzie klasa zawierająca tą deklarację będzie automatycznie posiadała atrybut car typu Car. Nazwa atrybutu może być dowolna, ale naturalnie nazwać ją zgodnie z jej typem (bo ta nazwa będzie nazwą bytu w dziedzinie/modelu, więc będzie intuicyjna). Jeśli nie chcemy modelować zależności zwrotnej (zależnej), np. w klasie Car z powrotem do klasy wiodącej, wystarczy w klasie wiodącej zadeklarować belongsTo z typem Class zamiast mapą, np. static belongsTo = Car. Oczywiście w obu przypadkach operacje są przekazywane kaskadowo w dół grafu (powiązań) i np. skasowanie klasy wiodącej będzie kasowało klasę zależną.

Relacja jeden-do-wielu modelowana jest atrybutem hasMany, która jest mapą, która z kolei składa się z klucza będącego automatycznie dodawanym do klasy dziedzinowej atrybutem-kolekcją oraz wartością mapy będącej typem obiektów w kolekcji. Domyślną kolekcją jest java.util.Set. Jeśli potrzebujemy bardziej wyrafinowanej kolekcji musimy zadeklarować jej typ explicite, np. (Listing 3-19):
 class Album {
String title

static hasMany = [songs:Song]

SortedSet songs
}
Klasa dziedzinowa w Grails może rozszerzać inną za pomocą typowej konstrukcji Groovy/Java - słowo kluczowe extends. W/g autorów dobry model charakteryzuje się co najwyżej 2-poziomową hierarchią dziedziczenia. Domyślnym mapowaniem dla hierarchii dziedziczenia jest tablica-per-hierarchia. Włączenie mapowania tablica-per-klasa to deklaracja tablePerHierarchy false w static mapping w klasie macierzystej (najbardziej ogólnej). tablica-per-hierarchia nie pozwala na włączanie warunków typu "niepuste", ale jest szybka w działaniu, podczas gdy tablica-per-klasa to powolne zapytanie złączeniowe (JOIN) na kilku tabelach. Strategia mapowania jest deklarowana na poziomie hierarchii/klasy i może być różna dla różnych klas dziedzinowych.

Włączenie zanurzenia klasy jakby była integralną częścią składową innej, tak aby pojedyncza tabela zawierała kolumny obu to deklaracja static embedded = ['nazwa-atrybutu'], gdzie typ nazwa-atrybutu to klasa zanurzana.

Grails wspiera dwa typy testowania - jednostkowe i integracyjne. Pierwsze znajdują się w klasach w katalogu test/unit, a drugie w test/integration. Rozbudowywanie klas dziedzinowych o metody dynamiczne, np. validate(), save() jest wyłącznie w testach integracyjnych. Stąd testy jednostkowe są bardzo szybkie (bez całego bagażu "integracyjnego"). Każdorazowe wykonanie polecenia grails create-* tworzy poza właściwym bytem, również klasę z testem integracyjnym.