Jak do tej pory w książce Definitive guide to grails temat gorm pojawiał się chyba przez wszystkie rozdziały i teraz najwyższy czas o szczegółowe przyglądnięcie się temu zagadnieniu.
Każda klasa domenowa automatycznie jest rozszerzona o pewne metody, wspierające zapytania. Na przykład metoda get(id), zwraca daną encję lub null jeśli nie zostanie znaleziona.
getAll(id1, id2) – zwraca listę obiektów, może jako parametr przyjąć listę.
Te dwie metody sprawiają, że zwracany obiekty można modyfikować (persisted). Jeśli jest potrzebny obiekt tylko do odczytu (read-only) z pomocą przychodzi metoda read(id).
Zazwyczaj z bazy pobiera się jakąś listę elementów i służy do tego metoda list(), która zwraca wszystkie encje. Można wprowadzić dodatkowe parametry
- max – ilość zwracanych rekordów
- offset – wykorzystywane przy stronicowaniu
- sort – właściwość po której jest sortowanie
- order – kolejność sortowania
Często z metodą list() jest używana metoda count(), która zwraca ilość elementów.
Przykład użycia tych metod i parametrów:
// get all the albums; careful, there might be many!
def allAlbums = Album.list()
// get the ten most recently created albums
def topTen = Album.list(max:10, sort:'dateCreated', order:'desc')
// get the total number of albums
def totalAlbums = Album.count()
Dodatkowo są metody listOrderBy*, gdzie w miejsce * wstawia się nazwę pola, np
def allByDate = Album.listOrderByDateCreated()
Zapisywanie nowego i aktualizacja istniejącego rekordu odbywa się poprzez metodę save(). Hibernate, który znajduje się w GORM automatycznie rozpoznaje czy ma wykonać aktualizację czy zapis. Przy starszych silnikach bazodanowych, mogą się pojawić błędy ale można to obejść poprzez wymuszenie:
object.save(insert:true)
Usuwanie to metoda delete()
Zazwyczaj relacja jeden do wielu (one-to-many) to jest java.util.Set. Jeśli jest ważna kolejność to można użyć SortedSet. Trzeba podać wtedy regułę sortowania i może to być poprzez implementację Comparable.
Ewentualnie można dodać w właściwości mapping sortowanie (i wtedy będzie użyte dla każdego zapytania)
static mapping = {
sort "trackNumber"
}
Takie sortowanie może nie być potrzebne dla każdego zapytania a potrzebne dla zapytań jeśli jest powiązanie z innymi elementami.
static mapping = {
songs sort: "trackNumber"
}
Klasa jest w relacji 1-n z klasą Song, którą reprezentuje zbiór songs.
Zamiast setów, można używać listy i odwoływać się przez indeksy
List songs
println album.songs[0]
Także mapy są dozwolone, gdzie kluczem(indeksem) jest jakiś string.
Jako, że GORM zapewnia asocjacje to także posiada metody do zarządzania nimi addTo* i removeFrom*, które zwracają obiekt na którym zostały wywołane i dzięki temu wywołania można łączyć w łańcuchy
new Album(title:"Odelay",
artist:beck
year:1996)
.addToSongs(title:"Devil's Haircut", artist:beck, duration:342343)
.addToSongs(mySong)
...
.save()
Domyślna kaskadowość w GORMie zależy od właściwości belongsTo. Jeśli ta właściwość istnieje to operacje zapisu, updatu i usunięcia są kaskadowe. Jeśli jej nie ma to tylko zapis i aktualizacja są a usuwanie nie jest kaskadowe. Można nad tym mieć większa kontrolę poprzez
static mapping = {
songs cascade:'save-udpate'
}
Możliwe opcje takie jak w hibernate.
Dynamiczne findery są jednym z najpoteżniejszych konceptów GORM, trochę zbliżone do wcześniejszych listOrderBy*. Mogą zawierać w sobie logiczne zapytania jak And, Or, Not i wyglądają findBy*, gdzie w miejsce gwiazdki wchodzi pole z klasy z odpowiednim łączeniem np
findByTitleAndTime("title", 30)
W tych zapytaniach są dozwolone także inne operatory(operator, ilość parametrów, przykład):
- Between 2 Album.findByDateCreatedBetween(today-10,today)
- Equals 1 Album.findByTitleEquals(‘Aha Shake Heartbreak’)
- GreaterThan 1 Album.findByDateCreatedGreaterThan(lastMonth)
- GreaterThanOrEqual 1 Album.findByDateCreatedGreaterThanOrEqual(lastMonth)
- InList 1 Album.findByTitleInList(['Aha Shake Heartbreak', 'Odelay'])
- IsNull 0 Album.findByGenreIsNull()
- IsNotNull 0 Album.findByGenreIsNotNull()
- LessThan 1 Album.findByDateCreatedLessThan(lastMonth)
- LessThanOrEqual 1 Album.findByDateCreatedLessThanOrEqual(lastMonth)
- Like 1 Album.findByTitleLike(‘Shake’)
- NotEqual 1 Album.findByTitleNotEqual(‘Odelay”)
Podobnymi metodami dla findBy* są findByAll* (zwraca listę) i countBy*(ilość).
Dzięki dynamicznym finderom, można w aplikacji zrezygnować z warstwy DAO.
Kryteria są również potężnym narzędziem wykorzystującym buildery w groovy. Kryteria w GORM są bardziej rozbudowane niż w hibernate.
Przed użyciem kryteriów należy stworzyć instancję kryteriów dla danej klasy poprzez statyczną metodę createCriteria(). Po utworzeniu instancji można wywołać jedną z czterech metod z domknięciami:
- get - odnajduje unikalny element
- list - zwraca listę
- scroll - zwraca ScrollableResults dla zapytania
- count - ilość elementów
Najpowszechniejsze jest list
def c = Album.createCriteria()
def results = c.list {
eq('genre', 'Alternative')
between('dateCreated', new Date()-30, new Date())
}
Dostępne kryteria oparte są na klasie
Restricions.
Jeśli jest asocjacja w klasie to również można użyć jej w kryteriach. Tworzy się to poprzez zbudowanie zagnieżdżonego kryterium gdzie nazwą jest właściwość w klasie
def criteria = Album.withCriteria {
songs {
ilike('title', '%Shake%')
}
}
Istnieją zapytania przez przykład (query by example) gdzie jako parametr do finderów jest przekazywany obiekt:
def album = Album.find( new Album(title:'Odelay') )
GORM wspiera także zapytania w hql. Występują tutaj metody find, findAll i executeQuery. Dozwolone są kolejne parametry (positional parameters), paramery nazwane (named parameters)
// query for all albums
def allAlbums = Album.findAll('from com.g2one.gtunes.Album')
// query for an Album by title
def album = Album.find(
'from Album as a where a.title = ?',
['Odelay'])
// query for an Album by title
def album = Album.find(
'from Album as a where a.title = :theTitle',
[theTitle:'Odelay'])
// get all the songs
def songs = Album.executeQuery('select elements(b.songs) from Album as a')
Do stronicowania wykorzystuje się parametry offset i max. Można je przekazać w ostatnim argumencie dynamicznych finderów jako mapę:
def results = Album.findAllByGenre("Alternative", [max:10, offset:20])
W widoku służy do tego tag
g:paginate, którego obowiązkowym atrybutem jest total, określający liczbę wszystkich encji. Domyślnie jest wywoływana aktualna akcja danego kontrolera, można to zmienić poprzez atrybuty contoller i action (tak jak w g:link). Również można określić nazwy przycisków dalej i wstecz poprzez atrybuty prev i next.
Wszystkie opcje konfiguracyjne w hibernate są dostępne w GORM. Wykorzystywane pliki to grails-app/conf/Config.groovy dla loggerów i grails-app/conf/DataSource.groovy. W tym pliku można zadeklarować blok hibernate i konfiguracja wygląda wtedy tak jak w plikach hibernate
hibernate {
cache.use_second_level_cache=true
cache.use_query_cache=true
cache.provider_class='com.opensymphony.oscache.hibernate.OSCacheProvider'
}
Są dostępne zaawansowane mechanizmy zarządzania sesjami. Warto pamiętać o czyszczeniu sesji (mniejsze zużycie pamięci)
def index = {
Album.withSession { session ->
def allAlbums = Album.list()
for(album in allAlbums) {
def songs = Song.findAllByAlbum(album)
// do something with the songs
...
session.clear()
}
}
}
Występuje automatyczny zmiany (automatic flush) (Można zmienić na manual, commit i domyślny auto):
- kiedy zapytanie jest uruchomione
-
bezpośrednio po skończeniu akcji kontrolera, jeśli nie został rzucony wyjątek
- przed skomitowaniem transakcji
Dlatego warto używać metody save(), ponieważ zapewnia ona walidacje i sprawia że obiekt jest tylko do odczytu jeśli pojawi się jakiś błąd. Jeśli nie jest naszym zamiarem aby zmieniać obiekt lepiej korzystać z metody read.
Można tworzyć bloki z transakcjami, rollbackami i zapisami po części operacji
def save = {
Album.withTransaction { status ->
def album = Album.get(params.id)
album.title = "Changed Title"
album.save(flush:true)
def savepoint = status.createSavepoint()
...
// something goes wrong
if(hasSomethingGoneWrong()) {
status.rollbackToSavepoint(savepoint)
// do something else
...
}
}
Domyślnie asocjacje są lazy. Można to zmienić poprzez
static mapping = {
artist fetch:'join'
}[/ourcecode]
lub w zapytaniu
[sourcecode language="java"]
def albums = Album.list(fetch:[artist:'join'])
for(album in albums) {
println album.artist.name
}
Także w kryteriach, hql i dynamicznych finderach można tego użyć.
Dostępne są 4 strategie cacheowania:
- read-only – tylko do odczytu
- nonstrict-read-write – malo i rzadkie operacje zapisu
- read-write – dużo i często, istnieje szansa zmian w tym samym czasie
- transactional - pełne zachowanie transakcyjne
class Album {
...
static mapping {
cache true
songs cache:'read-only'
}
}
Również można dać cacheowanie do zapytań poprzez parametr cache:true
Możliwa jest tabela dla hierarchii i tabela dla podklasy
Automatyczne wspierane są dateCreated i lastUpdate, wyłączenie tego to
class Album {
...
static mapping = {
autoTimestamp false
}
}