Parę słów o GORM

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

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
  }
}

Xml oraz bazy danych w groovy

Pewne przyśpieszenie w nauce groovy na podstawie książki Programming Groovy: Dynamic Productivity for the Java Developer Venkat’a Subramaniam’a, a mianowicie dwa rozdziały o przetwarzaniu xml oraz bazach danych w groovy. Przy okazji link do mojego tutorialu o parsowaniu xml przy użyciu Digestera.

Parsowanie xml w grooy
Groovy upraszcza DOM api przez dodanie wielu przydatnych metod.

Przykładowy xml

<languages>
  <language name="C++">
    <author>Stroustrup</author>
  </language>
  <language name="Java">
    <author>Gosling</author>
  </language>
  <language name="Lisp">
    <author>McCarthy</author>
  </language>
  <language name="Modula-2">
    <author>Wirth</author>
  </language>
  <language name="Oberon-2">
    <author>Wirth</author>
  </language>
  <language name="Pascal">
    <author>Wirth</author>
  </language>
</languages>

Dostęp do potomków odbywa się przez wskazanie nazwy właściwości np

rootElement.language

równoznaczne z getElementsByTagName(’name’)

Aby wyciągnąć wartość atrybutu należy poprzedzić jego nazwę @

language.@name

GPath obsługuje POJO, POGO oraz xml podobny trochę do XPath, porównanie ich prędkości można znaleźć tutaj.

Przykład parsowania przy użyciu DomCategory:

document = groovy.xml.DOMBuilder.parse(new FileReader('languages.xml' ))
rootElement = document.documentElement
use(groovy.xml.dom.DOMCategory) {
  println "Languages and authors"
  languages = rootElement.language
  languages.each { language ->
    println "${language.'@name'} authored by ${language.author[0].text()}"
  }
  def languagesByAuthor = { authorName ->
    languages.findAll { it.author[0].text() == authorName }.collect {
      it.'@name' }.join(', ' )
  }
  println "Languages by Wirth:" + languagesByAuthor('Wirth' )
}

wynikiem jest:
Languages and authors
C++ authored by Stroustrup
Java authored by Gosling
Lisp authored by McCarthy
Modula-2 authored by Wirth
Oberon-2 authored by Wirth
Pascal authored by Wirth
Languages by Wirth:Modula-2, Oberon-2, Pascal

Aby używać DOMCategory należy osadzić kod wewnątrz bloku use.

Trochę prościej przy użyciu XmlParser:

languages = new XmlParser().parse('languages.xml' )
println "Languages and authors"
languages.each {
  println "${it.@name} authored by ${it.author[0].text()}"
}
def languagesByAuthor = { authorName ->
  languages.findAll { it.author[0].text() == authorName }.collect {
  it.@name }.join(', ' )
}
println "Languages by Wirth:" + languagesByAuthor('Wirth' )

Nie trzeba używać bloku use ale jest kilka minusów:
-nie zachowuje XML InfoSet
-ignoruje komentarze i instrukcje przetwarzania
-brak namespaców
Mimo to zapewnia wygodę przy większości parsowań.

Dla dużych dokumentów XmlParser, może mieć problemy z pamięcią, lub jeśli istnieją namespacy należy użyć wtedy XMLSlurper (takie samo użycie jak XmlParser).

Obsługiwane namespacy, w metodzie declareNamespace() tworzy się mapę namespaców np:

<languages xmlns:computer="Computer" xmlns:natural="Natural">
<computer:language name="Java"/>
<computer:language name="Groovy"/>
<computer:language name="Erlang"/>
<natural:language name="English"/>
<natural:language name="German"/>
<natural:language name="French"/>
</languages>

i skrypt:

languages = new XmlSlurper().parse(
'computerAndNaturalLanguages.xml' ).declareNamespace(human: 'Natural' )
print "Languages: "
println languages.language.collect { it.@name }.join(', ' )
print "Natural languages: "
println languages.'human:language'.collect { it.@name }.join(', ' )

odwołanie się do elementu z namespacem to

element.'ns:name'

Tworzenie xml:
Do prostych xml można użyć właściwości GString i tworzenia stringów wielonijkowych np:

langs = ['C++' : 'Stroustrup' , 'Java' : 'Gosling' , 'Lisp' : 'McCarthy' ]
content = ''
langs.each {language, author ->
 fragment = """
 <language name="${language}" >
 <author>${author}</author>
 </language>
"""
content += fragment
}
xml = "<languages>${content}</languages>"

Bardziej zaawansowane wykorzystuje StreamingMarkupBuilder:

langs = ['C++' : 'Stroustrup' , 'Java' : 'Gosling' , 'Lisp' : 'McCarthy' ]
xmlDocument = new groovy.xml.StreamingMarkupBuilder().bind {
  mkp.xmlDeclaration()
  mkp.declareNamespace(computer: "Computer" )
  languages {
    comment << "Created using StreamingMarkupBuilder"
    langs.each { key, value ->
      computer.language(name: key) {
        author (value)
      }
    }
  }
}
println xmlDocument

Bazy danych w groovy.
GSQL – wraper na JDBC zapewniający wiele użytecznych metod. Przykład operacji na bazach danych w groovy.

Połączenie z baza danych

def sql = groovy.sql.Sql.newInstance(baza , user, password, driver )

Jeśli istnieje instancja klas java.sql.Connection lub java.sql.DataSource można użyć odpowiedniego konstruktora Sql zamiast newInstance.
Zamknięcie połączenia poprzez close.

Zapytania (Select)

sql.eachRow('SELECT * from weather' ) {
  println , it.city, it[1]
}

Dostęp do znalezionych pól poprzez it.name i it[index]
Istnieje inna wersja metody eachRow, która ma 2 domknięcia jako parametry. Pierwsze to metadane(wywoływane tylko raz) i np można pobrać nazwy kolumn np:

processMeta = { metaData ->
  metaData.columnCount.times { i ->
    printf "%-21s" , metaData.getColumnLabel(i+1)
  }
  println ""
}
sql.eachRow('SELECT * from weather' , processMeta) {
  printf "%-20s %s\n" , it.city, it[1]
}

Metoda rows(), pobiera dane ale nie iteruje po nich, zwraca ArrayList.
firstRow() – zwraca pierwszy element.
call() – proceury składowane().
withStatement() – tworzy domknięcie, które będzie wywoływane przed wykonaniem zapytania.

Bardzo łatwo można tworzyć xml na podstawie pobranych danych:

bldr = new groovy.xml.MarkupBuilder()
bldr.weather {
  sql.eachRow('SELECT * from weather' ) {
    city(name: it.city, temperature: it.temperature)
  }
}

i przykładowy wynik to

<weather>
<city name='Austin' temperature='48' />
<city name='Baton Rouge' temperature='57' />
<city name='Jackson' temperature='50' />
<city name='Montgomery' temperature='53' />
<city name='Phoenix' temperature='67' />
<city name='Sacramento' temperature='66' />
<city name='Santa Fe' temperature='27' />
<city name='Tallahassee' temperature='59' />
</weather>

Wyszukiwanie za pomocą kryteriów przez metode dataSet(tableName):

dataSet = sql.dataSet(‚weather’ )
citiesBelowFreezing = dataSet.findAll { it.temperature < 32 } println "Cities below freezing:" citiesBelowFreezing.each { println it.city }[/sourcecode] Inserty do bazy także przez dataSet i metodę add(), która przyjmuje mapę: [sourcecode language='java'] dataSet.add(city: 'Denver' , temperature: 19)[/sourcecode] Można użyć tradycyjnego podejścia [sourcecode language='java'] temperature = 50 sql.executeInsert("" "INSERT INTO weather (city, temperature) VALUES ('Oklahoma City' , ${temperature})"" ")[/sourcecode] update i delety wykonuje się podobnie Pobranie danych z excela [sourcecode language='java'] def sql = groovy.sql.Sql.newInstance( "" "jdbc:odbc:Driver= {Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}; DBQ=C:/temp/weather.xlsx;READONLY=false"" ", '' , '' ) println "City\t\tTemperature" sql.eachRow('SELECT * FROM [temperatures$]' ) { println "${it.city}\t\t${it.temperature}" } [/sourcecode]

Logowanie sql w javie – log4jdbc

Zdecydowana większość pewnie słyszała o log4j, wiele osób też go używało i uważa za dobre narzędzie. Jednak co zrobić jak chcemy sprawdzić jakie operacje na bazie danych są wykonywane w naszej aplikacji a jeszcze lepiej ich czasy. Tutaj z pomocą przychodzi nam log4jdbc. Proste narzędzie, które pozwala nam logować informacje o zapytaniach, kwerendach i to dosyć szczegółowo.

W log4jdbc dostajemy 4 loggery:

  1. jdbc.sqlonly - najprostszy, pokazuje tylko zapytania SQL.
  2. jdbc.sqltiming - ukazane czasy wykonywania operacji, w większości przypadków ten jest wystarczający.
  3. jdbc.audit - wszystkie wywołania SQLowe oprócz ResultSet.
  4. jdbc.resultset - wszystko łącznie z ResulSet.

Całą konfigurację log4jdb możemy zdefiniować w pliku log4j.xml a tutaj znajduje się przykładowy plik log4j.xml z włączonym logowaniem zapytań do bazy danych.
Osobiście polecam takiego appendera

<appender name=”sql-timing-appender” class=”org.apache.log4j.RollingFileAppender”>
<param name=”File” value=”ścieżka_gdzie_zapisać_log/sql.log”/>
<param name=”Append” value=”true”/>
<param name=”Encoding” value=”UTF-8″/>
<param name=”MaxFileSize” value=”10000KB”/>
<layout class=”org.apache.log4j.PatternLayout”>
<param name=”ConversionPattern” value=”—–> %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n%n”/>
</layout>
</appender>

ponieważ możemy zdefiniować tutaj rozmiar logów a także plik za każdym razem będzie dopisywany do wcześniejszych logów a nie tworzony nowy.

Jak już mamy konfigurację zapisu to jeszcze potrzebujemy biblioteki. W pracy korzystałem z następujących (nie mogę tutaj wrzucić archiwów a na jakiś hostingu plików to zginie szybko, więc po nazwach będzie można je znaleźć) .

  • log4jdbc3-1.1beta
  • slf4j-api-1.4.3
  • slf4j-log4j12-1.4.3

Ostatnia rzeczą jaką wykonujemy jest zdefiniowanie połączenia z bazą danych na nowych ustawieniach. Zmieniamy sterownik na net.sf.log4jdbc.DriverSpy , który obsługuje najbardziej popularne bazy danych. Jeśli, którejś nam brak to zawsze możemy ustawić we właściwościach log4jdbc aby wskazywał na dany sterownik. A także zamieniamy stary adres lokalizacji bazy:
jdbc:derby://localhost:1527//db-derby-10.2.2.0-bin/databases/MyDatabase na
jdbc:log4jdbc:derby://localhost:1527//db-derby-10.2.2.0-bin/databases/MyDatabase


Po zakończeniu zbierania logów należy przywrócić poprzednie ustawienia połączenia z bazą, ponieważ na tym sterowniku baza danych chodzi dużo wolniej niż na natywnym, zaobserwowane na przykładzie sterownika posgresql

%d blogerów lubi to: