Proseguiamo nel nostro percorso relativo a Grails: dopo averne visto gli aspetti fondamentali, approfondiamo il discorso delle relazioni in GORM: vedremo il modo in cui Grails e GORM gestiscono le relazioni tra classi.
Nel precedente articolo della serie [1] abbiamo iniziato ad esplorare le potenzialità di GORM, in particolare per quanto riguarda il mapping delle classi persistenti. Proseguiamo ora nel nostro per corso concentrandoci sulle relazioni tra le entità caratteristiche del nostro dominio
Grails integration test
Il nostro obiettivo è quello di esplorare come Grails e GORM gestiscono le relazioni tra le nostre domain classes. Lo strumento più naturale per verificare il comportamento di Grails è il test, in particolare, per poter verificare il comportamento di GORM, dovremo appoggiarci ai test di integrazione offerti da Grails. Recuperiamo la classe Contatto dal precedente articolo:
class Contatto { String nome String cognome String telefono static constraints = { nome (blank: false) cognome (blank:false) indirizzo (nullable:true) } }
Possiamo andare a creare in test di integrazione con il comando:
>grails create-integration-test ContattoIntegrationTests
Questo comando crea una classe di test, chiamata ContattoIntegrationTests nella cartella /test/integration del nostro progetto, all’interno della quale potremo verificare che il comportamento della nostra classe sia quello atteso:
class ContattoIntegrationTests extends GrailsUnitTestCase { ... void testSimpleContattoCreation() { def contatto = new Contatto(nome: "Tony", cognome: "Farseschi", telefono: "+39 328 9990101" ) assertNotNull contatto.save() // verifica che il metodo abbia successo assertNotNull contatto.id // verifica che GORM assegni l'id def trovato = Contatto.get(contatto.id) assertEquals 'Tony', trovato.nome // verifica che il contenuto sia ok } }
Per lanciare i nostri test di integrazione è possibile invocare, da riga di comando:
>grails test-app -integration
Questo comando lancia l’intera suite di test presente sotto la cartella /test/integration. È importante notare che Grails crea già una classe di test nella cartella /test/unit del nostro progetto come side-effect della creazione delle nostre domain class. Un velato messaggio subliminale. Tali classi servono per testare il comportamento delle nostre classi di dominio da un punto di vista logico. Tuttavia, per testare il comportamento di GORM abbiamo bisogno dell’attivazione dell’intero framework, che permette l’attivazione dei metodi “magici” sulle nostre classi di dominio. I test di unità agiscono solo sulle nostre classi, mentre i test di integrazione attivano “tutto l’ambaradan”, al costo di un’esecuzione più lenta. Torneremo sull’argomento nei prossimi articoli, per ora ci interessa disporre di uno strumento di verifica che ci possa seguire passo dopo passo.
Monogamia
Il nostro obiettivo è di esplorare le relazioni tra la classi del nostro domain model. Cominciamo con una banale relazione uno a uno: associamo un Indirizzo per ogni Contatto della nostra applicazione.
Figura 1 – La prima relazione del nostro domain model.
La nostra classe Indirizzo sarà strutturata come segue:
class Indirizzo { String via String cap String citta String provincia String nazione static constraints = { via (blank:false) cap (matches:"[0-9]+", length:5) citta (blank:false) provincia (length:2) } }
Mentre la nostra classe Contatto dovrà contenere un riferimento a un’istanza di Indirizzo, che indicheremo come “nullable” nella sezione “constraints” per renderla opzionale.
class Contatto { String nome String cognome Indirizzo indirizzo String telefono static constraints = { nome (blank: false) cognome (blank:false) telefono (blank:true) indirizzo (nullable:true) } }
Aggiungiamo quindi un nuovo test alla nostra classe ContattoIntegrationTestsù
void testContattoConIndirizzo() { def ufficio = new Indirizzo(via: "Via Capone 12", cap: "00100", citta: "Roma", provincia: "RM", nazione: "Italia") def contatto = new Contatto(nome: "Tony", cognome: "Farseschi", indirizzo: ufficio, telefono: "+39 328 9990101" ) assertNotNull ufficio.save() assertNotNull ufficio.id assertNotNull contatto.save() assertNotNull contatto.id def trovato = Contatto.get(contatto.id) assertEquals "Roma", trovato.indirizzo.citta }
Tutto ok? C’è ancora qualcosa che ci fa storcere il naso: abbiamo invocato save() sia sull’istanza di Indirizzo, che su Contatto. Ci sarebbe piaciuto qualcosa di più immediato. È possibile, ma dobbiamo dare a Grails qualche informazione ulteriore.
Cascading
Stabilire la molteplicità di una relazione non è tutto. Un elemento fondamentale è dato anche dalla tipologia di relazione che intercorre tra le nostre entità, in particolare per quanto riguarda la cancellazione. La domanda da porci in questo caso è: “Il nostro indirizzo può avere senso anche senza un contatto?”, nel mondo reale la risposta è sì, se un nostro amico cambia casa, normalmente non demolisce la propria abitazione precedente (tranne alcuni casi ai tempi dell’università…), ma nel contesto della nostra applicazione, un indirizzo “orfano” non ha molto senso.
GORM sfrutta i meccanismi di cascading di Hibernate pilotandoli con la parola chiave belongsTo. Nel nostro caso, per informare GORM della necessità di propagare la cancellazione del Contatto anche all’indirizzo da questo referenziato dobbiamo aggiungere l’informazione nella classe Indirizzo.
class Indirizzo { ... static belongsTo = Contatto ... }
e questo ci permette di semplificare il nostro codice di test:
void testContattoConIndirizzoCascade() { def ufficio = new Indirizzo(via: "Via Capone 12", cap: "00100", citta: "Roma", provincia: "RM", nazione: "Italia") def contatto = new Contatto(nome: "Tony", cognome: "Farseschi", indirizzo: ufficio, telefono: "+39 328 9990101" ) assertNotNull contatto.save() assertNotNull ufficio.id def trovato = Contatto.get(contatto.id) assertEquals "Roma", trovato.indirizzo.citta }
Il cascading propaga l’invocazione del metodo save() a tutte le classi che dichiarano di dover essere gestite da un’altra classe. Oltre alla creazione-inserimento sul DB, l’attivazione del meccanismo risulta particolarmente utile in caso di cancellazione, come possiamo verificare con un ulteriore test.
void testCancellazioneContatto() { def ufficio = new Indirizzo(via: "Via Capone 12", cap: "00100", citta: "Roma", provincia: "RM", nazione: "Italia") def contatto = new Contatto(nome: "Tony", cognome: "Farseschi", indirizzo: ufficio, telefono: "+39 328 9990101" ) assertNotNull contatto.save() assertNotNull ufficio.id contatto.delete() assertNull Indirizzo.get(ufficio.id) // verifica cancellazione a cascata }
La nostra classe contatto “comanda” la relazione, che è a tutti gli effetti una composizione (l’indirizzo non ha senso se non è associato ad un contatto). I lettori più affezionati avranno inoltre già fatto il collegamento con il concetto di Aggregato affrontato negli articoli su Domain Driven Design.[2]
Figura 2 – Con belongsTo posso specificare nelle classi “foglia” la classe radice della relazione.
Prime considerazioni
Non ci sono ID in giro. Abbiamo già visto nel precedente articolo che la gestione degli ID in Grails è completamente trasparente: GORM definisce i sensible default per Hibernate, che in questo caso significa “occupati tu degli ID”.
Ora però dobbiamo referenziare una domain-class da un’altra domain-class. Sotto, nelle profondità del database, troveremo due tabelle distinte, strutturate come in figura 3.
Figura 3 – La relazione fra Contatto e Indirizzo a livello di tabelle.
Ma questo avviene solo a livello della base dati e non a livello del domain model, che rimane quindi “incontaminato”. I riferimenti tra domain-class avvengono a livello di riferimenti tra istanze, in modo puramente OOP. Nei nostri test utilizziamo l’ID solamente per recuperare l’istanza creata.
Poligamia
Ok, è tempo di verificare il comportamento di Grails nel caso di relazioni uno a molti. Ai tempi del web 2.0 (o 3.0) tutti i nostri amici e colleghi hanno più di un’e-mail. Quella di lavoro, quella di casa, quella del lavoro vecchio etc. Ci servirà quindi un a coppia e-mail, tag (parola che fa molto più cool di “descrizione”) per ciascuno degli indirizzi e-mail da tracciare.
Figura 4 – La relazione tra il nostro Contatto e la classe EMail
La nostra classe EMail sarà decisamente semplice:
class EMail { String email String tipo static constraints = { email (email:true) tipo (blank:true) } }
Ma la cosa più interessante è la semplicità con cui possiamo definire la relazione del lato della classe Contatto:
class Contatto { String nome String cognome Indirizzo indirizzo String telefono static hasMany = [emails:EMail] static constraints = { nome (blank: false) cognome (blank:false) telefono (blank:true) indirizzo (nullable:true) emails (nullable:true) } }
Notiamo che non è nemmeno necessario definire la variabile emails di tipo Set. Grails è abbastanza sveglio da capire che se abbiamo dichiarato il riferimento in hasMany allora necessariamente avremo una variabile con quel nome.
Ciliegina sulla torta: le relazioni hasMany attivano due nuove famiglie di metodi sulle nostre domain classes: addTo*() e removeFrom*(). Questi metodi gestiscono direttamente la persistenza delle classi con molteplicità n: una volta che la nostra classe principale è stata resa persistente con il metodo save(), tutte le istanze aggiunte di Email saranno a loro volta persistenti.
Il test risultante che verifica il tutto
void testContattoConEMail() { def contatto = new Contatto(nome: "Tony", cognome: "Farseschi", indirizzo: null, telefono: "+39 328 9990101") contatto.save() def casa = new EMail (email:"Tony.Farseschi@gmail.com", tipo: "Casa") contatto.addToEmails(casa) def ufficio = new EMail (email:"Tony.Farseschi@cosanostra.com", tipo: "Casa") contatto.addToEmails(ufficio) def chat = new EMail (email: "MagillaGorilla@hotmail.com", tipo: "Chat") contatto.addToEmails(chat) log.info(contatto.emails.toString()) def trovato = Contatto.get(contatto.id) assertEquals 3, trovato.emails.size() assertTrue chat in trovato.emails // un po' di Groovy sugar :-) }
Cascading nelle relazioni uno a molti
Il meccanismo del cascading visto poc’anzi si applica senza problemi anche nelle relazioni uno-a-molti. Per implementare “Muoia Sansone con tutti i filistei” dovremo scrivere.
Class Sansone { static hasMany = [filistei:Filisteo] } Class Filisteo { static belongsTo = Sansone }
…e lanciare
sansone.delete()
Amore libero
Come gestiamo le relazioni molti a molti? Possiamo stigmatizzare cotanta promiscuità, oppure sfruttare GORM per implementarle rapidamente. Grails permette di dichiarare le relazioni molti a molti semplicemente:
- impostando su entrambi i lati della relazione la keyword hasMany;
- definendo il verso della relazione con belongsTo (solo in una direzione).
Il verso definito da belongsTo defnisce su quale lato della relazione sarà possibile invocare i metodi addTo* e removeFrom*.
Supponiamo di voler taggare i nostri contatti (ovviamente con tag multipli, altrimenti non saremmo 2.0): ci basta definire una semplice classe Tag.
class Tag { String name static belongsTo = Contatto static hasMany = [contatti:Contatto] }
Mentre sul lato di Contatto, sarà sufficiente includere la classe Tag tra quelle gestite come hasMany.
static hasMany = [emails:EMail, tags: Tag]
Ovviamente, per verificare il tutto, lanciamo il nostro test:
void testContattoWithTags() { def tagAmico = new Tag(tag:"amico") def tagCollega = new Tag(tag: "collega") def contatto = new Contatto(nome: "Tony", cognome: "Farseschi", indirizzo: null, telefono: "+39 328 9990101") contatto.save() contatto.addToTags(tagAmico) contatto.addToTags(tagCollega) def altro = new Contatto(nome: "Rudy", cognome: "Mentale", indirizzo: null, telefono: "5627819270").save() altro.addToTags(tagAmico); assertEquals 2, contatto.tags.size() assertEquals 1, altro.tags.size() assertEquals 2, Tag.findAll().size() def trovato = Contatto.get(contatto.id) assertEquals 2, trovato.tags.size() }
Conclusioni
Abbiamo visto come GORM permetta la gestione anche dei casi di mapping con semplicità ed eleganza. Abbiamo inoltre visto come sia possibile testare il comportamento delle nostre classi persistenti con i test di integrazione. Nei prossimi numeri vedremo come costruire un’applicazione vera e propria, uscendo dal solco tracciato dallo scaffolding di Grails, ma mantenendo intatte le caratteristiche di elevata produttività caratteristiche del framework.
Riferimenti
[1] Alberto Brandolini, “Sviluppo rapido di applicazioni con Groovy e Grails. IV parte: GORM e le classi di dominio”, Mokabyte 141, Giugno 2009
[2] Glen Smith, Peter Ledbrook, “Grails in Action”, Manning
[3] Alberto Brandolini, “Domain Driven Design: Aggregates e Repositories”, MokaByte 137, Febbraio 2009