Sviluppo rapido di applicazioni con Groovy & Grails

IV parte: GORM e le classi di dominiodi

Dopo la prima introduzione a Grails e la nostra esplorazione del linguaggio Groovy, è giunto il momento di ritornare mani e piedi su Grails e scoprire come sfrutta Groovy per offrire features che in Java non ci saremmo mai sognati.

Riassunto delle puntate precedenti

Nel primissimo articolo della serie su Grails [8] abbiamo verificato quanto fosse semplice creare una Domain Class, ovvero una classe Groovy con una corrispondente tabella sul database. Abbiamo visto che Grails si fa carico di un sacco di operazioni tediose, generando tramite le funzionalità di scaffolding, uno scheletro funzionante dell'applicazione che ci permette di avere in tempi rapidissimi una CRUD application funzionante, con tanto di navigazione, validazione etc.

Ora dimentichiamo per un attimo il presentation layer, e andiamo ad esplorare in maggiore dettaglio il legame tra le nostre classi di dominio ed il database sottostante.

Creazione di una domain class

Ricapitolando quanto fatto nel primo articolo della serie, creare una domain-class è decisamente immediato. Supponendo di avere creato la nostra applicazione con

>grails create-app Agenda

il passaggio successivo è quello di creare le nostre classi di dominio, a riga di comando, con:

>grails create-domain-class contatto

Quindi andiamo a popolare la nostra domain class con i nostri attributi.

class Contatto {
    String nome
    String cognome
    String email
    Date dataNascita
    String numeroTelefono
}

La nostra domain-class non appartiene ad alcun package. In Grails, la suddivisione delle classi per ruolo è basata sulla struttura delle directory e sul rispetto della naming convention. È possibile comunque usare i package nelle classi utilizzate dalle nostre domain class (come in effetti avviene per le librerie) ma le domain class sono fondamentalmente le classi nella cartella /domain.

GORM

Dietro le quinte, Grails è in grado di derivare dalla nostra domain class tutte le informazioni necessarie per gestirne la persistenza. La componente che si occupa di questo lavoro sporco è GORM, ovvero Grails Object Relational Mapping. Si tratta per certi versi di un di un wrapper su Hibernate, che permette di sfruttare, direttamente dalle nostre domain class in Groovy, le potenzialità del ben collaudato framework ORM in Java.

Il termine wrapper è però largamente riduttivo: GORM è in effetti un vero e proprio strato aggiuntivo che preserva la logica di funzionamento di Hibernate, ma agisce come mediatore "smart" sulla logica di configurazione offrendo una serie di funzionalità che semplificano la vita del programmatore.

Hibernate può essere configurato in vari modi, mediante configurazione XML, JPA annotations, programmaticamente, etc.
Ma fondamentalmente ha bisogno di 2 tipi di informazione:

  1. Informazioni generali (p.e.: database a cui ci dobbiamo connettere, cache, connection pool, etc.)
  2. Informazioni specifiche di ogni classe di dominio (p.e.: informazioni di mapping tra attributi e campi dei nostri record).

 

Figura 1 - La tradizionale suddivisione della configurazione di Hibernate: un file di impostazioni generali e una serie di configurazioni di mapping locali, associate alle singole classi (come files .hbm.xml o come annotations).

 

Il compito di GORM in questo quadro è molto chiaro: semplificare la vita al programmatore, evitando di farci perdere tempo negli aspetti più ovvi (e noiosi) della configurazione o in quelli più macchinosi.
GORM fornisce a Hibernate tutte le informazioni necessarie, prendendole dalla configurazione di Grails (molto più compatta e pre-impostata su valori comodi), dalla struttura delle domain-class, ma soprattutto dal buon senso. In uno scenario di questo genere, Hibernate dispone di tutte le informazioni necessarie senza che sia necessario configurarlo direttamente;quindi il tempo da noi speso nella configurazione si riduce drasticamente, e - grazie alla disponibilità di impostazioni di default che permettono di cominciare a lavorare immediatamente - possono essere differite nel tempo anziche' essere concentrate in una fase di startup del progetto (normalmente piuttosto concitata).

 

 

Figura 2 - GORM fornisce a Hibernate tutte le informazioni necessarie. Hibernate diventa un "dettaglio implementativo".

In Grails, le meta-informazioni di mapping non risiedono in un file esterno, n� richiedono una sintassi particolare come nel caso delle annotations, ma sono localizzate nel codice in un formato estremamente leggibile. In questo scenario possiamo quindi ignorare completamente la presenza di Hibernate e limitarci a definire la struttura delle classi di dominio, aggiungendo di volta in volta solo le meta-informazioni che servono.

Il quadro in realtà è un po' diverso. Per applicazioni create da zero quello appena descritto rappresenta il modo normale di lavorare. Tuttavia, in determinate circostanze (p.e.: nel caso di applicazioni con una forte componente legacy), può risultare necessario conservare le informazioni già mappate su Hibernate. GORM non ci obbliga a fare piazza pulita: la configurazione di Hibernate resta una strada valida, per gestire questo tipo di situazioni o ambienti ibridi Java-Groovy.

 

 

Figura 3 - È comunque possibile utilizzare la configurazione di Hibernate, se necessario (nessuno ce lo vieta...).

 

Il legame con il database sottostante

Chiarito che GORM non ci farà domande stupide, ma anzi cercherà di "fare da solo" per quanto possibile, vediamo ora che cosa succede al momento della creazione della nostra domain class.

Nel nostro caso, Grails istruisce il database a creare una tabella contatto... ma dov'è il database? O meglio, come possiamo specificare quale database utilizzare?

Configurazione generale

la configurazione generale della nostra applicazione la troviamo nella classe DataSource.groovy, nel package /conf del nostro progetto. In un linguaggio dinamico come Groovy, una classe permette di gestire la configurazione in maniera più pratica di un file di properties. Diamo un'occhiata al listato che Grails genera al momento della creazione dell'applicazione:

dataSource {
    pooled = true
    driverClassName = "org.hsqldb.jdbcDriver"
    username = "sa"
    password = ""
}
hibernate {
    cache.use_second_level_cache=true
    cache.use_query_cache=true
    cache.provider_class='com.opensymphony.oscache.hibernate.OSCacheProvider'
}
// environment specific settings
environments {
    development {
        dataSource {
            dbCreate = "create-drop" // one of 'create', 'create-drop','update'
            url = "jdbc:hsqldb:mem:devDB"
        }
    }
    test {
        dataSource {
            dbCreate = "update"
            url = "jdbc:hsqldb:mem:testDb"
        }
    }
    production {
        dataSource {
            dbCreate = "update"
            url = "jdbc:hsqldb:file:prodDb;shutdown=true"
        }
    }
}

Tutto ciò che serve a Hibernate per lo startup è già impostato in questa classe. Il database designato a default è HSQLDB, un in-memory database che risulta estremamente efficiente nelle operazioni di sviluppo. Notiamo che in questo file sono riportate anche le informazioni di configurazione della cache di Hibernate e soprattutto le informazioni di configurazione differenziate per ambiente: avremo URL differenti per le differenti istanze di DB e differenti policy per l'aggiornamento dei dati, ma è decisamente comodo che queste informazioni siano ben strutturate e leggibili. Ovviamente questo è solo un punto di partenza: Hypersonic è ideale nelle fasi iniziali, ma per test e produzione andremo a configurare il nostro database "vero".

Configurazione con MySQL

Ad esempio, per configurare la connessione con MySQL possiamo impostare il datasource come:

dataSource {
    driverClassName = "com.mysql.jdbc.Driver"
    username = "root"
    password = "qualsiasicosamanonpippo"
    dbCreate = "create-drop"
    url = "jdbc:mysql://localhost/MokabyteAgenda"
}

e aggiungere nella cartella /lib della nostra applicazione il file .jar del driver; Grails lo trova e lo aggiunge senza colpo ferire al classpath del progetto.

La nostra tabella

Assodato che "sotto" c'è un database e non tanti piccoli "gnomi delle query", possiamo andare a vedere cosa succede un po' più in dettaglio. Nella figura successiva vediamo la nostra struttura della tabella contatto sul database:

Figura 4 - La struttura della tabella contatto sul nostro database.

Notiamo come siano stati impostati per default i nomi dei campi (mediante ridimensionamento delle maiuscole e anteponendo un underscore a quelle che erano le maiuscole nei nomi degli attributi) e impostati i tipi e il dimensionamento. La gestione del corretto tipo di dato per ogni specifico modello di database è una delle feature standard di Hibernate: spostandoci su un diverso vendor ci limiteremo a configurare il datasource.

Notiamo inoltre la presenza dell'id e del campo version, utilizzato da Hibernate per implementare il controllo di versione nel caso di modifiche concorrenti.

Il legame con Hibernate

Comincia ad esser un po' più chiaro il ruolo di GORM per quanto riguarda la configurazione: in pratica è una sistematica applicazione del buon senso! Hibernate richiede un certo numero di informazioni di configurazione? GORM lo istruisce sfruttando i sensible defaults: mentre in HIbernate devo marcare con un'annotation @Entity o con un file di configurazione .hbm.xml la mia classe persistente, in Grails è ovvio che una domain class sia persistente, per cui ci pensa GORM. Lo stesso ragionamento vale per gli attributi, che vengono trattati per default come persistenti, lasciando a noi il compito di specificare quali sono le eccezioni al comportamento più frequente.

Per quanto intelligente, GORM ancora non legge nel pensiero. Negli articoli precedenti ([9], [10]), abbiamo visto che Groovy ci permette di non specificare il tipo degli attributi di una classe, ricorrendo alla keyword def. GORM però ha bisogno di informazioni più precise per poter generare il database sottostante (ricordate i database? quegli strani oggetti che chiedono insistentemente "di che tipo è?" e "quanto è grande?" di qualsiasi cosa gli parlate...). Le nostre domain-classes devono quindi necessariamente specificare il tipo degli attributi persistenti.

Default mappings

Il mapping degli attributi verso il differente sistema di tipi di ogni piattaforma è delegato a Hibernate. In genere il comportamento è decisamente ragionevole, ma non è infrequente dover ricorrere a mapping personalizzati. Uno dei modi per impostare queste informazioni è dato dalla variabile statica mapping:

class Contatto {
    String nome
    String cognome
    String email
    Date dataNascita
    String numeroTelefono
    static mapping = {
      table 'persone'
        version false
        columns {
            id column:'persona_id
        }
    }   
}

Con questa possiamo uscire dalle impostazioni di default, in tal caso impostando il nome della tabella, e modificando il nome della colonna id.

Figura 5 - La struttura della tabella, con nome della tabella e del campo chiave modificati.

GORM mette a disposizione un vero e proprio DSL [3] per le impostazioni di mapping, che permette di accedere a tutte le impostazioni disponibili in mapping, per impostare custom types o farci del male con chiavi composte legacy.

Operazioni elementari

Ogni domain class dispone delle operazioni elementari per il proprio salvataggio. Per salvare un'istanza della nostra classe è sufficiente scrivere:

Contatto mario = new Contatto(nome:"mario",
                              cognome:"rossi",
                              email: "mario.rossi@zz.org",
                              dataNascita: new Date(), 
                              numeroTelefono: "+39 0241 XXXXX"
)
mario.save()

Mentre per recuperare un oggetto già memorizzato sul database, facciamo riferimento al metodo statico get() che prende come parametro l'ID del record che ci interessa:

def altroContatto = Contatto.get(1)

il metodo get() restituisce un istanza valorizzata di Contatto se l'id che passiamo come parametro corrisponde a una classe persistente, oppure null, se questo non è presente sul database (lo stesso comportamento dell'omonimo metodo get() di Hibernate). Come visto in precedenza, l'id è già presente come campo sul DB. È anche presente nella nostra domain-class, anche se esplicitamente non lo abbiamo dichiarato. Del resto, è abbastanza ovvio che una classe persistente abbia un id, o meglio che tutte le classi persistenti ne abbiano uno. Notiamo inoltre che sparisce il fastidioso casting tipico di Hibernate.

Il metodo save() agisce anche in caso di aggiornamenti della nostra domain-class:

def altroContatto = Contatto.get(1)
altroContatto.email = "mario.rossi@fantastic.org"
altroContatto.save()

Infine, per cancellare dal database il record corrispondente alla nostra domain-class è sufficiente ricorrere al metodo delete():

// cancello altroContatto dal Database
altroContatto.delete()

I metodi save(), delete() e get() fanno parte della dotazione di serie delle nostre domain-class e non è necessario dichiararli: Grails "pimpa" le nostre classi ([4], [5]) con dei metodi extra sfruttando le possibilità di manipolazione a run-time offerte dalla metaprogrammazione in Groovy.

Come il cugino Ruby on Rails, Grails si basa sull'Active Record Pattern [6], in cui le entità caratteristiche dell'applicazione sono in grado di "rendersi persistenti" esponendo i metodi per il salvataggio e il recupero. In Java, una delle principali controindicazioni di Active Record Pattern era legata all'implementazione tipicamente basata sul principio di ereditarietà da una superclasse comune, un po' come accadeva con i vituperati EJB Entity 1.x e 2.x o in tanti framework di persistenza fatti in casa.

Grails invece rende disponibili i metodi CRUD senza che questo finisca per corrompere le classi del nostro dominio, che restano dei perfetti POGO (Plain Old Groovy Objects).

Questo approccio semplifica abbastanza l'architettura rispetto ad una tradizionale applicazione Java+Spring+Hibernate: non interagiamo direttamente con gli oggetti Hibernate, quali Session e SessionFactory (Grails lo fa per noi) e non ricorriamo esplicitamente ai DAO. I metodi che tradizionalmente sarebbero collocati sui DAO sono infatti esposti direttamente dalla classe, come metodi d'istanza se riconducibili ad uno specifico oggetto o come metodi di classe se quest'oggetto non è disponibile (come nel caso dei metodi che si occupano di recuperare informazioni dal database (il metodo get() visto poc'anzi, ma anche tutta la famiglia dei metodi finder).

Finder Methods

Il recupero di una classe, basandosi sull'id, può tornare utile in alcuni casi, ma più frequentemente avremo necessità di recuperare informazioni sulla base di qualche parametro, ottenendo in risposta una collezione di risultati. Il metodo più banale per verificare questa funzionalità è quello di farci restituire tutti i record:

def contatti = Contatto.list()

il metodo list() senza parametri restituisce tutte le istanze memorizzate sul database della nostra domain-class. Le cose cominciano a farsi interessanti quando cominciamo a specificare alcuni parametri per il metodo list()

def primiVentiContatti = Contatto.list(max:20, sort:'cognome')

ma soprattutto, quando applichiamo ordinamenti in maniera molto più parlante rispetto a quanto siamo abituati a fare:

def contattiOrdinati = Contatto.listOrderByCognome(max:20)

Questa è una delle caratteristiche più cool di GORM: definisce metodi dinamici, in questo caso i metodi della famiglia listOrderBy* che vengono interpretati a runtime sulla base della struttura corrente della classe, a patto che rispettino la naming convention standard. La ciliegina sulla torta è data dai metodi di ricerca veri e propri:

def red = Contatto.findByCognome("Rossi")
def allReds = Contatto.findAllByCognome("Rossi")

Il metodo findByCognome restituisce un'istanza che soddisfa la condizione specificata, mentre il metodo findAllByCognome restituisce una List di tutte le istanze che soddisfano la nostra condizione. Ma non è finita; il meccanismo dei dynamic methods ci riserva ancora qualche sorpresa: è possibile infatti combinare più parametri di ricerca nella signature del nostro metodo.

def marioRossi = Contatto.findByNomeAndCognome("Mario", "Rossi")

GORM effettua il parsing della signature del nostro metodo e la sfrutta per creare la corrispondente query dietro le quinte, sfruttando gli argomenti nell'ordine in cui sono passati. Ovviamente potrò combinarli anche in or o utilizzando operatori di confronto più raffinati, quali Between, GreaterThan, GreaterThanOrEqual, IsNull, IsNotNull, LessThan, LessThanOrEqual, Like, NotEqual. Il metodo vero è proprio viene generato a runtime, sulla base della signature subito prima di essere eseguito.

Poco fa abbiamo detto che GORM non legge nel pensiero... se consideriamo che ci permette di invocare metodi che non esistono e che questi metodi si comportano esattamente come vorremmo, direi che ci va comunque abbastanza vicino.

Attributi opzionali o non persistenti

A default, tutti gli attributi delle nostre classi sono considerati persistenti ed obbligatori. Sarà quindi necessario istruire Grails a considerare determinati attributi come non persistenti, e a permettere l'immissione di valori nulli, per gli attributi non obbligatori (non c'è niente di più fastidioso di un'applicazione che ti costringe a inserire un'informazione di cui non disponi).

In Grails, queste informazioni sono gestite ricorrendo a variabili statiche con nomi convenzionali: nel nostro caso, aggiungiamo un boolean onlineNow al nostro Contatto, che ovviamente non vorremo salvare sul database.

boolean onlineNow
static transients = ['onlineNow'] // onlineNow non sarà salvato sul database

La variabile di classe transients è una list, contenente l'elenco dei nomi degli attributi che non devono essere resi persistenti.

Invece, per indicare che il campo dataNascita è opzionale, facciamo ricorso ad un altra variabile static, denominata constraints:

class Contatto {
    String nome
    String cognome
    String email
    Date dataNascita
    String numeroTelefono 
    ...
    static constraints = {
        dataNascita(nullable: true)   // dataNascita non è un campo obbligatorio
        email(email: true)
    }
   
}

A differenza di transients, constraints è una closure, formata da tanti builder nodes (i builder sono un altro dei costrutti caratteristici di Groovy) quanti sono gli attributi che vogliamo vincolare. Grails mette a disposizione una serie di constraints non indifferente, come è possibile vedere nel seguente specchio riassuntivo:

È inoltre possibile definire i nostri criteri di validazione custom, da applicare a specifici campi.
Il rispetto dei constraints viene verificato al momento dell'invocazione del metodo save() sulla nostra domain class, quindi avviene anche all'interno del codice di business (ad esempio nei nostri test di integrazione).

Constraints e scaffolding

Per quanto possibile, Grails utilizza anche le informazioni provenienti dei constraints per pilotare la configurazione del database: ad esempio gli attributi inList, maxSize, size, min, max, range e scale influenzano la dimensione con cui viene creata la corrispondente colonna sul database, così come nullable influenza i constraints che il DB applica sul campo.

Considerazioni finali

GORM è probabilmente il fiore all'occhiello di Grails, permette di gestire la persistenza in maniera estremamente leggera per il programmatore, offrendo tutta la potenza di Hibernate, utilizzabile in modo molto più leggero. A differenza di altri framework di persistenza che permettono di generare il codice dei DAO o dei metodi di data retrieval indipendentemente dal fatto che questo codice serva oppure no, in Grails il codice c'è quando serve: l'applicazione è più leggera e leggibile, e il nostro codice è solo il minimo indispensabile.

Riferimenti

[1] Graeme Rocher, Jeff Brown, "The definitive guide to Grails", Apress

[2] Grails online reference
http://grails.org/doc/1.1.x/

[3] Grails Mapping DSL
http://grails.org/GORM+-+Mapping+DSL

[4] MTV Pimp My Ride
http://www.mtv.com/ontv/dyn/pimp_my_ride/series.jhtml

[5] Caparezza, "Pimpami la storia", in "Le dimensioni del mio caos", 2008

[6] Martin Fowler, "Patterns of Enterprise Application Architecture", Addison Wesley

[7] Christian Bauer, Gavin King, "Java Persistence with Hibernate", Manning

[8] Alberto Brandolini, "Sviluppo rapido di applicazioni con Groovy & Grails - I parte: Cominciamo subito!", MokaByte 130, Giugno 2008
http://www2.mokabyte.it/cms/article.run?articleId=DYG-V69-HKE-Z3O_7f000001_10553237_7391cad5

[9] Alberto Brandolini, "Sviluppo rapido di applicazioni con Groovy & Grails - II parte: Groovy, un linguaggio carico", MokaByte 132, Settembre 2008
http://www2.mokabyte.it/cms/article.run?articleId=XUT-F64-2GE-JMR_7f000001_10911033_521ed4fc

[10] Sviluppo rapido di applicazioni con Groovy & Grails - III parte: Alle radici del dinamismo, MokaByte 135, Dicembre 2008
http://www2.mokabyte.it/cms/article.run?articleId=OXN-C3X-RI9-648_7f000001_10911033_2a8ab5e2

 

 

 

Condividi

Pubblicato nel numero
141 giugno 2009
Articoli nella stessa serie
Ti potrebbe interessare anche