JAVA exPress > Archiwum > Numer 6 (2009-12-09) > GORM – Grails Object Relational Mapping

GORM – Grails Object Relational Mapping

W tym artykule chciałbym przedstawić podstawowe informacje na temat GORM (Grails Object Relational Mapping) oraz kilka bardziej zaawansowanych zagadnień z nim związanych. Po przeczytaniu artykułu dowiesz się jak działa warstwa persystencji w Grails.

Rozpoczynamy

GORM, warstwa persystencji w Grails zrealizowana jest jako mapowanie obiektowo-relacyjne. Pod maską GORM znajdziemy Hibernate. Możemy z niego również skorzystać bezpośrednio.

Mapowanie OR jest zrealizowane w Grails na tzw. klasach domenowych (Domain class). Klasa domenowa zwana jest w innych kręgach encją. Klasa domenowa utworzona (z linii poleceń lub wyklikana w NetBeans) w najprostszym przypadku jest po prostu pustą klasą, wewnątrz której definiujemy atrybuty, które chcemy przechowywać, np.

    class Person {
        String firstname
        String lastname
    }

Gdy utworzymy taką klasę domenową i wystartujemy nasz projekt (z domyślnymi ustawieniami) w NetBeans, zostanie uruchomiona baza Hypersonic i zostanie utworzona nowa tabelka Person. Będzie posiadała 4 kolumny: firstname, lastname, id i version. Grails domyślnie zajmuje się za nas generowaniem identyfikatorów (w najbardziej odpowiedni dla danej bazy sposób) oraz wersjonowaniem.

Klasa domenowa zwana jest w innych kręgach encją.

Podstawowe operacje (CRUD) wykonujemy w następujący sposób:

    // Utworzenie nowego rekordu CREATE
    def p1 = new Person(firstname:"Mateusz", lastname:"Mrozewski");
    p1.save()
     
    // Odczytanie rekordu READ
    // tu korzystamy z automatycznie wygenerowanego id
    def p2 = Person.get(1) 
     
    // Aktualizacja UPDATE
    // modyfikujemy rekor	d odczytany wcześniej
    p2.firstname = "Krzysztof" 
    p2.save(); 
     
    // Usuwanie DELETE
    // usuwamy odczytany wcześniej rekord
    p2.delete() 

   

Jak widać wykonanie podstawowych operacji jest niezwykle łatwe.

Asocjacje

Z GORM robi się zawsze ciekawiej gdy dochodzi do asocjacji. Jednak jest to również rozwiązane w sposób czysty i prosty. Przykłady różnych relacji poniżej:

    // One-to-one unidirectional - jeden do jednego, jednokierunkowa
    // mając referencję do samochodu, możemy pobrać jego silnik, ale nie odwrotnie
    Class Car {
        Engine engine
    } 
     
    Class Engine {}
     
    // One-to-one bidirectional - jeden do  jednego, dwukierunkowa
    // Mając referencję do samochodu, możemy pobrać jego silnik, odwrotnie również
    Class Car {
        Engine engine
    }
     
    Class Engine {
        Car car
    } 

   

W przypadku obu relacji powyżej operacje nie są kaskadowane. To oznacza, że jeśli skasujemy samochód, to jego silnik pozostanie (usuniemy z bazy, nie mówię o wypadkach). Podobnie ma się zapis zmian, utworzenie nowego rekordu, aktualizacja. Musimy to zrobić ręcznie dla obu klas. Wygodniej było by mieć kaskadowanie. Przykład z kaskadowaniem operacji wygląda tak:

    // One-to-one unidirectional - jeden do jednego, jednokierunkowa
    // mając referencję do samochodu, możemy pobrać jego silnik,
    // ale nie odwrotnie
    Class Car {
        Engine engine
    } 
     
    Class Engine {
        static belongsTo = Car
    }
     
    // One-to-one bidirectional - jeden do  jednego, dwukierunkowa
    // Mając referencję do samochodu, możemy pobrać jego silnik,
    // odwrotnie również
    Class Car {
        Engine engine
    }
     
    Class Engine {
        static belongsTo = [car:Car]
    } 

   

Jak widać kaskadowanie realizujemy poprzez właściwość belongsTo. Warto zwrócić uwagę na różnicę w deklaracji tej właściwości przy relacji jedno i dwukierunkowej.

Od wersji Grails 1.2-M4 mamy jeszcze jedną możliwość deklarowania relacji jeden do jednego:

    class Car {
        static hasOne = [engine:Engine]
    }

    class Engine {
        Car car
    }

Różnica względem poprzednich przykładów jest taka, że klucz obcy zostanie umieszczony w tabeli engine, a nie w tabeli car.

Relację o krotności większej od jeden tworzymy poprzez wykorzystanie właściwości hasMany. Stosując ją po obu stronach relacji otrzymamy relację wiele do wielu, a tylko po jednej stronie relację jeden do wielu. Tak jak w przypadku relacji jeden do jednego, aby wymusić kaskadowanie należy zastosować właściwość belongsTo.  Na przykładzie:

    // One-to-many unidirectional - jeden do wielu, jednokierunkowa
    class Car {
        static hasMany = [wheels:Wheel]
    }
     
    class Wheel {
    }

   

W związku z relacją jeden do wielu należy pamiętać o kilku ważnych sprawach: 

  • Grails do zmapowania tej relacji w bazie wykorzysta dodatkową tabelę złączeniową. Można to zachowanie nadpisać i wykorzystać klucze obce.

  • Grails pozwala na wykorzystanie dwóch strategii pobierania: lazy i eager. Domyślnie stosowane jest lazy, co w przypadku relacji o krotności większej niż jeden może prowadzić do problemów wydajnościowych (każdy kolejny rekord należąc do relacji jest pobierany oddzielnym zapytaniem w miarę jak iterujemy po naszej kolekcji).

  • Domyślnie kaskadowane są operacje INSERT i UPDATE, ale do kaskadowania operacji DELETE musimy zastosować właściwość belongsTo.

W przypadku relacji wiele do wielu stosujemy właściwość hasMany po obu stronach relacji:

    class Book {
        static hasMany = [authors:Author]
    }
     
    class Author {
        static hasMany = [books:Book]
    }

   

Również w tym przypadku możemy zastosować właściwość belongsTo. Dokumentacja wspomina, że scaffolding nie wspiera jeszcze tego typu relacji.

Grails domyślnie stosuje kolekcję Set
w przypadku relacji o krotności większej niż jeden.

Grails domyślnie stosuje kolekcję Set w przypadku relacji o krotności większej niż jeden. Domyślne zachowanie możemy nadpisać stosując taką kolekcję, jaka nam się podoba, z więc dowolną implementację Set, List lub Map.

Strategie dziedziczenia

Grails wspiera dwie strategie dziedziczenia: table-per-hierarchy oraz table-per-subclass. Taka jest przynajmniej informacja w oficjalnej dokumentacji. Jak wiadomo pod maską Grails znajdziemy Hibernate'a, a ten wspiera również strategię table-per-concrete-class, więc pewnie i w Grails dałoby się to jakoś wykorzystać.

Czym tak naprawdę są strategie dziedziczenia? Nazwa brzmi dumnie, ale sprawa nie jest zbyt złożona. Wyobraźmy sobie sytuację, w której mamy klasę domenową dziedziczącą po innej klasie domenowej, np.:

    class Osoba {
        String imie
        String nazwisko
    }
     
    class Pracownik extends Osoba {
        String nip
    }

   

Strategia dziedziczenia określa nam, jak taka struktura zostanie przedstawiona w bazie danych. Dopóki nie wprowadziliśmy dziedziczenia, każda klasa domenowa miała swoją tabelkę. W tym przypadku, jeśli zastosujemy strategię table-per-hierarchy (domyślne) wszystkie klasy z hierarchii będą umieszczone w tej samej tabeli. Tabelka będzie wyglądała następująco:

    CREATE TABLE `osoba` (
        `id` bigint(20) NOT NULL auto_increment,
        `version` bigint(20) NOT NULL,
        `imie` varchar(255) NOT NULL,
        `nazwisko` varchar(255) NOT NULL,
        `class` varchar(255) NOT NULL,
        `nip` varchar(255) NULL,   PRIMARY KEY  (`id`)
    );

Jak widać tabela posiada kolumny imie i nazwisko zadeklarowane w klasie Osoba oraz pole nip z klasy Pracownik. Dodatkowo zawiera automatycznie utworzony przez Grails identyfikator (w moim przypadku auto_increment bo korzystam z MySQLa), kolumnę version służącą do wersjonowania rekordów oraz kolumną class, która pozwala nam określić czy dany wiersz reprezentuje osobę czy pracownika. Plusem tego rozwiązania jest posiadanie w jednej tabeli wszystkiego co potrzeba (kwestia optymalizacyjna - nie potrzeba żadnych operacji złączenia). Minusem jest to, że nasza aplikacja pozwoli teraz zatrudnić pracownika "na czarno" - kolumna nip musi być null (gdyż możemy chcieć dodać osobę, która nie jest pracownikiem i nie ma NIPu).

Drugą dostępną strategią jest table-per-subclass. W tym wypadku każda podklasa będzie posiadała dodatkową tabelkę, która będzie przechowywać tylko dodatkowe pola. Pola odziedziczone będą przechowywane w tabelce klasy nadrzędnej. Jak to zrobić? Wystarczy dodać mały kawałek kodu w klasie nadrzędnej:

    class Osoba {
        String imie
        String nazwisko
     
        static mapping = {
            tablePerHierarchy false
        }
    }
     
    class Pracownik extends Osoba {
        String nip
    }

   

W tym przypadku zostanie utworzona tabela osoba, zawierająca pola id, version, imie i nazwisko. Druga tabelka to pracownik, która będzie zawierała pola id (pokrywające się z tabelą osoba) oraz pole nip. Minusem tego rozwiązania jest potrzeba wykonania złączenia zawsze, gdy będziemy chcieli pobrać rekord pracownika. W zamian za to możemy wymusić nip na poziomie bazy danych.

Zagnieżdżone klasy domenowe

Dokumentacja na ten temat nie jest zbytnio rozpisana, ale w sumie nie wiem, czy potrzeba tu dużo tłumaczenia. Chodzi o to, że możemy zagnieździć klasę domenową w innej klasie domenowej. Z punktu widzenia kodu nadal deklarujemy dwie klasy, oznaczamy jedynie, że pewna właściwość jest klasą zagnieżdżoną, np.:

    class Car {
        Engine engine
        static embedded = ['engine']
    }
     
    class Engine {
        String type
    }

   

Z takiej deklaracji otrzymamy jedną tabelę car, która będzie posiadała pola id, version oraz engine_type. Pozwala nam to na ładne rozdzielenie modelu obiektowego przy jednoczesnym uproszczeniu zapytań. Zamiast dwóch zapytań lub jednego ze złączeniem mamy po prostu jedno. Należy pamiętać, że obie klasy należy zadeklarować w pliku Car.groovy. Jeśli przeniesiemy klasę Engine do oddzielnego pliku do zostanie wygenerowana oddzielna tabelka dla tej klasy.

Opimistic i Pessimistic Locking

Blokowanie to sposób na zabezpieczenie się przez współbieżnymi zmianami. Zasadniczo są dwa podejścia: optymistyczne, które zakłada, że współbieżne zmiany raczej nie nastąpią (sprawdzenie wykonane jest na koniec operacji/transakcji) oraz pesymistyczne, które zakłada, że współbieżne zmiany są na tyle częste, że lepiej wywłaszczyć rekord przed wykonaniem jakichkolwiek zmian. Są to powszechnie stosowane podejścia, nie tylko w Grails (i nie tylko we framework'ach web'owych). 

Każda klasa domenowa
ma automatycznie wygenerowany atrybut version,
który przy każdej operacji UPDATE jest inkrementowany.

Blokowanie optymistyczne wykorzystuje wersję rekordu (robi to za nas Grails, a właściwie to Hibernate). Każda klasa domenowa ma automatycznie wygenerowany atrybut version, który przy każdej operacji UPDATE jest inkrementowany. Jeśli przy zapisie rekordu GORM wykryje, że wersja została w między czasie zmieniona przez inną transakcję, nasza transakcja zostanie wycofana i zostanie wyrzucony wyjątek StaleObjectException. Jak to obsłużymy zależy od nas: możemy napisać zmiany lub zrezygnować z zapisu i poprosić użytkownika o ponowne wykonanie zmian na aktualnych danych.

Blokownie pesymistyczne działa natomiast wykorzystując operację "SELECT ... FOR UPDATE". Wykonanie takiej operacji na bazie danych powoduje zablokowanie rekordu dla innych transakcji (na poziomie bazy danych). Inne transakcje, które spróbują pobrać ten sam rekord zatrzymają się i będą czekać na jego odblokowanie. Należy pamiętać, że jest to mechanizm zależny od danej bazy danych i tak jak wspomina dokumentacja, dostarczony z Grails HSQLDB tego nie obsługuje. Jak wygląda to w kodzie:

    def car = Car.get(1)
    // w tym miejscu inny wątek może pobrać ten sam rekord i blokowanie nic nam nie da
    car.lock() // tu blokujemy rekord
    car.model = "Audi"
    car.save()
     
    // inna wersja rozwiązująca powyższy problem
    def car = Car.lock(1)
    car.model = "Audi"
    car.save()

 

Blokownie pesymistyczne działa natomiast
wykorzystując operację SELECT ... FOR UPDATE.

 

Blokować możemy też od razu w wykonaniu zapytań. Blokady zwalniane są w momencie zatwierdzenia lub cofnięcia transakcji.

Pobieranie danych

GORM pozwala nam na wykonywanie zapytań w kilka różnych sposobów. Możemy do nich zaliczyć podstawowe metody operujące na klasach domenowych, dynamic finders (metody typu find*), kryteria (które pozwalają budować zapytania podobnie jak robi to JaQu) albo wykorzystać stary (dobry) HQL.

Metody klas domenowych

Kawałek kodu chyba najlepiej pokaże, jakie metody możemy wywołać:

    // Wylistuj wszystkie osoby
    def persons = Person.list()
    // Pobierz osobę o id=3
    def person = Person.get(3)
    // Pobierz osoby o id 3,4,5
    def persons = Person.getAll(3, 4, 5)
    // Pobierz 10 osób z offsetem 100, sortując po nazwisku malejąco
    def persons = Person.list(max:10, offset:100, sort:"lastname", order:"desc") 

   

Dla metody list() możemy dodatkowo określić ilość zwróconych rekordów, od którego rekordu rozpocząć, sortowanie łączenie z kierunkiem, a także w jaki sposób pobrać asocjacje (eager/lazy). Metoda get() po prostu pobiera rekord o zadanym id, a getAll() rekordy o zadanych id. Jeśli któryś z nich nie będzie istniał, lista wyników będzie zawierała null.

Dynamic finders

Dynamic finders to metody tworzone dynamicznie w środowisku uruchomieniowym na podstawie właściwości klasy:

    class Person {
        String firstname
        String lastname
        Date dateOfBirth
    }
     
    def p1 = Person.findByFirstname("Mateusz")
    def p2 = Person.findByLastname("Mrozewski")
    def p3 = Person.findByFirstnameAndLastnameLike("Mateusz", "M%")
    def p4 = Person.findByDateOfBirthIsNull()

   

Wypisane metody to tylko kilka możliwych przykładów. Możemy takie metody wywoływać dla każdej właściwości. Tak jak w przykładzie wyszukania osoby p3 możemy połączyć warunki dla dwóch dowolnych właściwości z operatorem AND lub OR  (maksimum dwóch - dla większej ilości powinniśmy wykorzystać kryteria lub HQL). Dwa typy metod to findBy*, który zwraca pierwszy rekord wyniku oraz findAllBy*, który zwraca wszystkie pasujące wyniki. A dozwolone operacje to:

  • InList - czy wartość znajduje się na podanej liście,

  • LessThan - czy wartość jest mniejsza niż podana,

  • LessThanEquals - czy wartość jest mniejsza lub równa niż podana,

  • GreaterThan - czy wartość jest większa niż podana,

  • GreaterThanEquals - czy wartość jest większa lub równa niż podana,

  • Like - podobne do SQLowego LIKE,

  • ILike - jak wyżej, tylko case insensitive (to nie produkt apple'a),

  • NotEqual - nie równe (nie ma samego equal, bo to równoważne z np. findByName),

  • Between - czy wartość jest pomiędzy zadanymi,

  • IsNotNull - czy wartość nie jest null'em,

  • IsNull - czy wartość jest null'em.

Ale chwileczkę, skąd te wszystkie metody się wzięły?

Ten typ zapytań możemy również wykorzystać dla asocjacji. Do tych zapytań możemy też dodać parametry takie jak w metodzie list(). Prawda, że fajne? Osobiście uważam, że przy zastosowaniu już dwóch parametrów przestaje być to czytelne i dużo łatwiej pisze się i analizuje kryteria, ale do prostych zapytań jest to jak najbardziej ciekawe rozwiązanie.

Ale chwileczkę, skąd te wszystkie metody się wzięły? Przecież nic nie dziedziczyliśmy, nic nie implementowaliśmy. Tutaj z pomocą przychodzi nam dynamiczność Groovy oraz to, jak zostało to wykorzystane w Grails. Konkretnie chodzi tu o wykorzystanie metaklas i ich właściwości methodMissing. W książce The Definitive Guide to Grails w dodatku poświęconym Groovy możemy znaleźć nawet prosty przykład implementacji dynamic finder.

Kryteria

Kryteria to sposób na budowanie złożonych zapytań poprzez wykorzystanie składni Groovy. A konkretnie wykorzystana jest tu koncepcja builders.

Nas interesuje jednak jak to wygląda w kodzie. Oto mały przykład z dokumentacji:

    def c = Account.createCriteria()
    def results = c {
            like("holderFirstName", "Fred%")
            and {
                    between("balance", 500, 1000)
                    eq("branch", "London")
            }
            maxResults(10)
            order("holderLastName", "desc")
    }

W tym przykładzie zostały utworzone kryteria dla klasy domenowej Account. Następnie rozbudowaliśmy je o kolejne warunki - ten prosty przykład chyba dość dobrze pokazuje jakie są możliwości kryteriów. 

Co można w takich kryteriach zrobić:

  • wykorzystać wszystkie metody z klasy Hibernate Restrictions,

  • wykorzystać operatory logiczne AND, OR i NOT,

  • pytać o inne klasy domenowe będące w asocjacji z główną klasą,

  • wykorzystać wszystkie metody z klasy Hibernate Projections,

  • wykorzystać wszystkie metody z klasy Hibernate ScrollableResults,

  • określić ilość pobranych rekordów oraz od którego rekordu zacząć,

  • określić czy asocjacje będą pobierane w trybie EAGER czy LAZY.

Nie wypisuję wszystkich możliwości, gdyż jest tego mnóstwo, a wszystko jest w Javadoc'ach. Jednak ta krótka lista przynajmniej pokazuje jak szerokie mamy możliwości. GORM to nie tylko CRUD.

HQL

W GORM możemy też wykorzystać HQLa (Hibernate Query Language). Jest to język zapytań bardzo podobny do SQLa, ale nie operujemy w nim na tabelkach, tylko na encjach (klasach domenowych). Mamy do dyspozycji trzy metody, które możemy wywołać na naszej klasie domenowej:

  • find - zwraca pierwszy wynik pasujący do zapytania,

  • findAll - zwraca wszystkie wyniki pasujące do zapytania,

  • executeQuery - wykonuje zapytanie, niekoniecznie zwracające wyniki (np. UPDATE).

W GORM możemy też wykorzystać HQL

Kilka przykładów:

    // Znajdź pierwszą osobę o imieniu Mateusz
    def person = Person.find("from Person as p where p.firstname = ?",
            ['Mateusz'])
    // Znajdź wszystkie osoby o imieniu Mateusz i nazwisku na M
    def results = Person.findAll(
            "from Person as p where p.firstname = :firstname
            and p.lastname like :lastname",
            [firstname:'Mateusz', lastname:'M%'])

   

Niezbędnikiem jest zapoznanie się z rozdziałem dokumentacji Hibernate poświęconej HQL. 

Zdarzenia i znaczniki czasowe

Bardzo ciekawą funkcją w GORM są zdarzenia. Zdarzenia to domknięcia wywoływane w odpowiednim momencie cyklu życia instancji klasy domenowej. Możemy je użyć np. do zapisania daty ostatniej aktualizacji, utworzenia encji, zapisania do logu śladu po usuwanej encji, itp. Wyróżniamy następujące typy zdarzeń:

  • beforeInsert - wywoływane zanim obiekt zostanie zapisany do bazy po raz pierwszy,

  • beforeUpdate - wywoływane zanim obiekt zostanie zaktualizowany w bazie,

  • beforeDelete - wywoływane przed usunięciem rekordu,

  • afterInsert - wywoływane po zapisaniu obiektu do bazy po raz pierwszy,

  • afterUpdate - wywoływane po zaktualizowaniu obiektu w bazie,

  • afterDelete - wywoływane po usunięciu obiektu z bazy,

  • onLoad - wywoływane po wczytaniu obiektu z bazy.

I w przykładzie:

    class Person {
       Date dateCreated
       Date lastUpdated
       Date loadTime
     
       def beforeInsert = {
          dateCreated = Date()
       }
       def beforeUpdate = {
           lastUpdated =new Date()
       }
       def onLoad = {
           loadTime = new Date()
       }
    }

 

W tym przykładzie naszą klasę domenową rozbudowaliśmy o zapisywanie do bazy daty aktualizacji, daty utworzenia oraz ustawianie czasu wczytania rekordu. Jeśli chodzi o datę utworzenia i aktualizacji, to GORM pozwala nam na małe uproszczenie: wystarczy zadeklarować w klasie domenowej właściwość lastUpdated i dateCreated a będą one automatycznie ustawiane na odpowiednie wartości (konwencja nie tylko ponad konfigurację, ale również ponad kodowanie).

Własne mapowanie

Kolejną cechą GORMa jest własne mapowanie tabel. Własne mapowanie pozwala nam samodzielnie określić jakie będą nazwy tabel i kolumn. Jest to niezwykle przydatne, gdy musimy dostosować się do konwencji przyjętej w firmie/projekcie lub gdy piszemy kod do istniejącej bazy danych. Więcej na ten temat można poczytać oczywiście w dokumentacji, wydaje mi się że ten dział wymaga najmniej dodatkowych opisów i dyskusji.

Własne mapowanie pozwala nam
samodzielnie określić jakie będą nazwy tabel i kolumn.

Wspomnę tylko jeszcze, że możemy również wpłynąć na to, jak generowane będą identyfikatory (domyślnie GORM wykorzystuje ten najodpowiedniejszy dla danej bazy danych). Możemy też wpłynąć na indeksy, jakie zostaną wygenerowane.

Dla zobrazowania możliwości mały przykład:

    class Person {
      String firstName
      static mapping = {
          table 'people'
          firstName column:'First_Name'
      }
    }

Cache

Pod maską GORM siedzi Hibernate, nie mogło więc zabraknąć mechanizmu second-level cache z tej biblioteki. Cache możemy włączyć w pliku konfiguracyjnym DataSource.groovy (domyślnie przy wygenerowaniu projektu jest on włączony). Możemy podobnie jak w Hibernate, w pamięci podręcznej (mało zręczne tłumaczenie słowa cache) zapisać encje lub wyniki zapytań. Czy dana klasa domenowa ma być zapisywana do pamięci podręcznej określa się w samej klasie domenowej:

    class Person {
        static mapping = {
            cache true
        }
    }

   

"Keszować" możemy też asocjacje. Możemy ustawić jeden z kilku dostępnych rodzajów pamięci podręcznej. Wpływa to na wydajność naszej pamięci, a to wynika ze strategii obsługi danych oraz dopuszczalnego ryzyka współbieżnych modyfikacji:

  • read-only - nasza aplikacja tylko odczytuje dane i nigdy ich nie modyfikuje. Szybkie w działaniu, gdyż nie potrzeba nam żadnych synchornizacji;

  • read-write - nasza aplikacja zapisuje i odczytuje dane. Dostęp jest synchronizowany;

  • nonstrict-read-write - nasza aplikacja i czyta i pisze, ale prawdopodobieństwo wystąpienia jednoczesnego zapisu jest znikome; 

  • transactional - przy tej strategi możemy wykorzystać w pełni transakcyjną pamięć podręczną, taką jak JBoss TreeCache.

Ograniczenia (contraints)

Grails posiada wbudowany mechanizm walidacji danych. Pozwala on nam na bardzo łatwe i deklaratywne określanie, jakie właściwości mogą przyjąć jakie wartości. Dlaczego jest to ważne z punktu widzenia GORM? Ponieważ może to wpłynąć na to, jak zostanie wygenerowany schemat bazy danych. Bardzo mi się to spodobało, że dostajemy z automatu zabezpieczenie na poziomie bazy danych również, a nie tylko na poziomie aplikacji.

Warstwa persystencji jest niezwykle rozbudowana.
Jest dość elastyczna i bogata w funkcje.

Do deklaracji ograniczeń w klasie domenowej wykorzystywana jest sekcja constraints:

    class Task {
        String name
        String description

        static constraints = {
            description(maxSize:1000)
        }
    }

Podsumowanie

Jak widać warstwa persystencji jest niezwykle rozbudowana w Grails. Jest dość elastyczna i bogata w funkcje. Jeśli komuś GORM nie do końca odpowiada, to zawsze można skorzystać z Hibernate bezpośrednio, wykorzystać plugin do JPA lub bezpośrednio łączyć się z bazą danych (JDBC albo pakiet GroovySQL). Nie mniej jednak myślę, że GORM zadowoli większość użytkowników.

Nie ma jeszcze komentarzy.

Tylko zalogowani użytkowincy mogą pisać komentarze

Developers World