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:

err
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.