Grails to bardzo ciekawy framework przygotowany dla jÄ™zyka Groovy. Od razu zdobyÅ‚ mojÄ… sympatiÄ™ za to, że – nie tylko nazwÄ… – przypomina architekturÄ™ Ruby on Rails, czy też Symfony dla PHP, realizujÄ…c najważniejsze polityki nowoczesnych frameworków: Convention Over Configuration i Do Not Repeat Yourself. Zawsze ubolewaÅ‚em (i w sumie nadal ubolewam) nad brakiem takiego rozwiÄ…zania dla ‘czystej’ Javy (może Ty znasz jakiÅ› przykÅ‚ad?), dlatego tym chÄ™tniej zainteresowaÅ‚em siÄ™ tajnikami Grails.
Przyznam, że raczej sceptycznie reagujÄ™ na różne ‘liberalne’ jÄ™zyki programowania, np. fakt, że jakÄ…Å› operacjÄ™ można wykonać używajÄ…c różnych konstrukcji jÄ™zyka (bo wg mnie zaciemnia to nieco obraz samego kodu i utrudnia jego pielÄ™gnacjÄ™), albo dynamiczcne typowanie (zawsze przeklinaÅ‚em PHP za tÄ™ ‘przypadÅ‚ość’), jednak daÅ‚em Groovy’emu szansÄ™, za jego głównÄ… zaletÄ™: peÅ‚nÄ… kompatybilność z JavÄ… – Groovy jest de facto wykonywany przez maszynÄ™ wirtualnÄ… Javy, wiÄ™c kompatybilność ta jest oczywista.
Umówmy siÄ™ – należę do tych bardziej konserwatywnych programistów wierzÄ…cych w ‘twardÄ…’ skÅ‚adniÄ™, a w Groovy nie umiem (jeszcze) nic poza “Hello World”, toteż bardzo zależaÅ‚o mi na frameworku, w którym bÄ™dÄ™ mógÅ‚ wykorzystać już istniejÄ…ce ziarenka Javy.  Samego Groovy’ego też chÄ™tnie zgłębiÄ™, ale najpierw chcÄ™ zobaczyć co mogÄ™ zrobić z Grailsami wykorzystujÄ…c istniejÄ…cy już kod Javy.
Grails – jak RoR, czy Symfony – posiada wÅ‚asny mechanizm mapowania obiektowo-relacyjnego (gdzieÅ› tam głęboko napÄ™dzany przez Hibernate), zwany GORM, pozwalajÄ…cy na mapowanie klas Groovy’ego z bazÄ… danych. Ja jednak, przez zaÅ‚ożenie, chcÄ™ mapować ziarna encji Hibernate napisane w Javie. Grails pozwala na to bez problemów, kilka prostych czynnoÅ›ci i już możemy wykorzystać przygotowane wczeÅ›niej encje w Grails.
No, może nie całkiem bez problemów.
Aby zmapować klasy encji wystarczy, zgodnie z opisem na stronie Grails, wyedytować plik DataSource.groovy, dodać mapowanie w hibernate.cfg.xml i wreszcie skopiować swoje klasy javy do katalogu src/java. Po wykonaniu tych czynności możemy już wygenerować na podstawie encji nowy kontroler i widok, wklepując w konsoli:
grails generate-all pelna.nazwa.naszej.Klasy
Grails wygeneruje dla nas wszystkie niezbÄ™dne klasy kontrolerów (w jÄ™zyku Groovy, rzecz jasna), elementy widoku (strony GSP), sÅ‚owem mamy już caÅ‚y CRUD. Problem jednak pojawia siÄ™ w przypadku, gdy nasza encja wyposażona jest w klucz zÅ‚ożony, a schemat naszej bazy danych – jak w moim przypadku –  nie może zostać zmodyfikowany, bo jest wykorzystywany przez inne programy.
Grails teoretycznie pozwoli na mapowanie takich encji, wygeneruje też dla nich kontrolery i pliki widoków, jednak nie bedą one działały. Rozważmy poniższy przypadek.
Mamy encjÄ™ Produkt (mapowanÄ…  na tabelÄ™ “PRODUKTY”), encjÄ™ Cennik (mapowanÄ… na tabelÄ™ “CENNIKI”) i encjÄ™ Cena (tabela “CENY”) . NetBeans, na podstawie istniejÄ…cego schematu bazy danych, wygenerowaÅ‚ mi takie klasy (po drobnej korekcie nazw wÅ‚asnoÅ›ci):
package net.schowek.grailsapp;
// importy
@Entity
@Table(name = "PRODUKTY")
@NamedQueries({@NamedQuery(name = "Produkt.findAll", query = "SELECT p FROM Produkt p")})
public class Produkt implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Basic(optional = false)
@Column(name = "pr_id")
private Integer id;
@Basic(optional = false)
@Column(name = "pr_nazwa")
private String nazwa;
@Column(name = "pr_opis")
private String opis;
public Produkt() {
}
public Produkt(Integer prId) {
this.id = prId;
}
// gettery i settery
@Override
public String toString() {
return getNazwa();
}
}
package net.schowek.grailsapp;
// importy
@Entity
@Table(name = "CENNIKI")
@NamedQueries({@NamedQuery(name = "Cennik.findAll", query = "SELECT c FROM Cennik c")})
public class Cennik implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Basic(optional = false)
@Column(name = "cn_id")
private Integer id;
@Column(name = "cn_nazwa")
private String nazwa;
public Cennik() {
}
public Cennik(Integer cnId) {
this.id = cnId;
}
// gettery i settery
@Override
public String toString() {
return getNazwa();
}
}
package net.schowek.grailsapp;
// importy
@Entity
@Table(name = "CENY")
@NamedQueries({@NamedQuery(name = "Cena.findAll", query = "SELECT c FROM Cena c")})
public class Cena implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
private CenaPK id;
@JoinColumn( name = "ce_pr_id", referencedColumnName = "pr_id", insertable = false, updatable = false )
@ManyToOne
private Produkt produkt;
@JoinColumn( name = "ce_cn_id", referencedColumnName = "cn_id", insertable = false, updatable = false )
@ManyToOne
private Cennik cennik;
@Column(name = "ce_cena")
private BigDecimal cena;
public Cena() {
}
public Cena(CenaPK cenaPK) {
this.id = cenaPK;
}
// gettery i settery
@Override
public String toString() {
return getId().toString();
}
}
I wreszcie klasa określająca nasz klucz złożony:
package net.schowek.grailsapp;
// importy
@Embeddable
public class CenaPK implements Serializable {
@Basic(optional = false)
@Column(name = "ce_pr_id")
private int cePrId;
@Basic(optional = false)
@Column(name = "ce_cn_id")
private int ceCnId;
public CenaPK() {
}
public CenaPK(int cePrId, int ceCnId) {
this.cePrId = cePrId;
this.ceCnId = ceCnId;
}
public int getCePrId() {
return cePrId;
}
public void setCePrId(int cePrId) {
this.cePrId = cePrId;
}
public int getCeCnId() {
return ceCnId;
}
public void setCeCnId(int ceCnId) {
this.ceCnId = ceCnId;
}
@Override
public String toString() {
return "net.schowek.grailsapp.CenaPK[cePrId=" + cePrId + ", ceCnId=" + ceCnId + "]";
}
}
Tak przygotowanesą podstawą wygenerowanej aplikacji.  Po uruchomieniu aplikacji za pomocą komendy grails run-app mogę już eksplorować zasoby cen i produktów. Problem jednak zaczyna się, gdy chcę przejrzeć listę cen. Już na liście (http://localhost:8080/GrailsApp/cena/list) widać, że odnośniki do rekordów cen nie przypominają standardowych odnośników do rekordów tabel o prostych kluczach. Przykładowy link do ceny wyglądał tak:
http://localhost:8080/GrailsAppl/cena/show/net.schowek.grailsapp.CenaPK%5BcePrId%3D63%2C+ceCnId%3D2%5D
a strona, którą można pod nim zobaczyć wygląda tak:

Jak widać, Grails w miejsce id, podstawia wartość obiektu klasy CenaPK, a ten reprezentowany jest przez swojÄ… metodÄ™ toString().  Po odwoÅ‚aniu siÄ™ do serwera i zapytaniu o cenÄ™ o podanym kluczu, serwer – co oczywiste – czÄ™stuje nas wyjÄ…tkiem.
Oto prosty sposób, w jaki można temu zaradzić (zakładając, że klucz złożony składa się z pól typu numerycznego).
Przede wszystkim, zmieniłem kod klasy CenaPK na taki (zmieniłem toString() i dodałem jeden konstruktor):
public static final String KEY_SEPARATOR="_";
@Override
public String toString() {
return cePrId+KEY_SEPARATOR+ceCnId;
}
public CenaPK(String k) {
String[] keys = k.split( KEY_SEPARATOR );
this.cePrId = Integer.parseInt(keys[0]);
this.ceCnId = Integer.parseInt(keys[1]);
}
Jak widzisz, połączyłem w jeden string dwa klucze oddzielone od siebie separatorem. Oznacza to, że ponowne wygenerowanie widoków komendą:
grails generate-views net.schowek.grailsapp.Cena
poskutkuje nową listą cen, zawierającą odnośniki do rekordów podobne do tych:
http://localhost:8080/GrailsAppl/cena/show/63_2
Aby wszystko grało, musiałem jeszcze przerobić kontroler cen (plik CenaController.groovy w katalogu controllers/) i zmienić wszystkie linie ładujące pojedynczy rekord ceny z
def cenaInstance = Cena.get( params.id )
na
def cenaInstance = Cena.get( new CenaPK(params.id) )
Od tej pory mogłem się już cieszyć działającą aplikacją obsługującą klucze złożone.
Aha, separatorem klucza może być w zasadzie dowolny znak, jednak sÄ… tu wyjÄ…tki – np. nie zadziaÅ‚a na pewno slash, ponieważ jest on interpretowany przez UrlMappera grails. Warto zatem sprawdzić co zadziaÅ‚a, a co nie.