11 marca 2009

Grails w akcji - nie ma to jak prosty, ale własny widok

Od jakiegoś czasu wspomninam o moich postępach w lekturze książki "The Definitive Guide to Grails, Second Edition" (DGG2) i zdecydowanie za mało w nich doświadczeń praktycznych. Teoretycznie, po 10-ciu rozdziałach DGG2 i wcześniejszej książce, mógłbym sądzić, że znam Grails od podszewki. Zacząłem nawet przeglądać skrypty dostarczane z Grails, ale wciąż to za mało. Wiedza teoretyczna jak najbardziej wskazana, ale praktyczna pozwala uzmysłowić sobie niuanse, o których mógłbym nawet nie pomyśleć podczas lektury książek. Zresztą, doświadczam tego nierzadko, że mimo dużej liczby artykułów temat przemawia do mnie dopiero wtedy, kiedy zacznę się z nim zmagać praktycznie. Wtedy wszystko wydaje się takie oczywiste.

Kręcę się koło tworzenia aplikacji grailsowej, którą mógłbym obsłużyć kilka tematów - najważniejszy z nich, to narzędzie pomocnicze do nauki dla moich dzieciaków (jak mają coś wkuwać, np. chemia czy angielski, to nie są za bardzo zmotywowane do pracy z książką, a przy kompie to jakoś tak wszystko łatwiej im wchodzi), po drugie jako sposób na pogłębienie wiedzy praktycznej z Grails i ostatni, to po prostu stworzenie aplikacji referencyjnej, którą mógłbym wykorzystać do moich kolejnych wystąpień o Grails (jest jeszcze jeden, a właściwie dwa powody, ale o nich na razie sza).

Pierwszy krok podczas tworzenia aplikacji grailsowej to stworzenie projektu poleceniem grails create-app. Określamy nazwę projektu i po chwili mamy właściwą strukturę katalogową (dobry moment, aby umieścić ją od razu w systemie kontroli wersji - właśnie otworzyłem konto na github, aby nie być gołosłownym i stworzyłem projekt nauczyciel).

Warto przyjrzeć się wynikowi polecenia grails create-app, aby zajrzeć za kulisy Grails.
 jlaskowski@work /cygdrive/c/projs/sandbox
$ grails create-app nauczyciel
Welcome to Grails 1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: c:/apps/grails

Base Directory: C:\projs\sandbox
Running script c:\apps\grails\scripts\CreateApp_.groovy
Environment set to development
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\src
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\src\java
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\src\groovy
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\controllers
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\services
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\domain
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\taglib
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\utils
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\views
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\views\layouts
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\i18n
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\conf
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\test
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\test\unit
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\test\integration
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\scripts
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\web-app
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\web-app\js
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\web-app\css
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\web-app\images
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\web-app\META-INF
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\lib
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\conf\spring
[mkdir] Created dir: C:\projs\sandbox\nauczyciel\grails-app\conf\hibernate
[propertyfile] Creating new property file: C:\projs\sandbox\nauczyciel\application.properties
[copy] Copying 1 resource to C:\projs\sandbox\nauczyciel
[unjar] Expanding: C:\projs\sandbox\nauczyciel\grails-shared-files.jar into C:\projs\sandbox\nauczyciel
[delete] Deleting: C:\projs\sandbox\nauczyciel\grails-shared-files.jar
[copy] Copying 1 resource to C:\projs\sandbox\nauczyciel
[unjar] Expanding: C:\projs\sandbox\nauczyciel\grails-app-files.jar into C:\projs\sandbox\nauczyciel
[delete] Deleting: C:\projs\sandbox\nauczyciel\grails-app-files.jar
[move] Moving 1 file to C:\projs\sandbox\nauczyciel
[move] Moving 1 file to C:\projs\sandbox\nauczyciel
[move] Moving 1 file to C:\projs\sandbox\nauczyciel
Installing plug-in hibernate-1.1
[mkdir] Created dir: C:\Documents and Settings\jlaskowski\.grails\1.1\projects\nauczyciel\plugins\hibernate-1.1
[unzip] Expanding: C:\Documents and Settings\jlaskowski\.grails\1.1\plugins\grails-hibernate-1.1.zip
into C:\Documents and Settings\jlaskowski\.grails\1.1\projects\nauczyciel\plugins\hibernate-1.1
Executing hibernate-1.1 plugin post-install script ...
Plugin hibernate-1.1 installed
Created Grails Application at C:\projs\sandbox/nauczyciel
Warto nadmienić, że polecenie grails to tak naprawdę skrypt napisany w Gant, czyli nakładce Groovy dla Ant. Wiedza jaką mamy odnośnie Ant jest wystarczająca, aby zrozumieć działanie Gant. Dodając do tego znajomość Groovy i mamy komplet. Każde polecenie grails ma swój skrypt. Grails przeszukuje właściwe katalogi w poszukiwaniu skryptu i w przypadku create-app będzie to $GRAILS_HOME/scripts/CreateApp_.groovy. Można do niego zajrzeć, aby poznać, jak niewiele trzeba było, aby stworzyć Grails (można powiedzieć, że wszystko było, tylko należało dopasować do siebie części - Spring, Hibernate, Groovy, teraz Ant i kilka innych klocków). Pierwsza linia tego skryptu to
 includeTargets << grailsScript("_GrailsCreateProject")
która (jak łatwo się zorientować) dołącza zawartość kolejnego skryptu _GrailsCreateProject.groovy i ustawia zadanie createApp jako domyślne. Kiedy zaczniemy przeglądać skrypt dalej dojdziemy do zadania createStructure (przez initProject) , który znajduje się w _GrailsInit.groovy i przedstawia się następująco:
 target(createStructure: "Creates the application directory structure") {
ant.sequential {
mkdir(dir: "${basedir}/src")
mkdir(dir: "${basedir}/src/java")
mkdir(dir: "${basedir}/src/groovy")
mkdir(dir: "${basedir}/grails-app")
mkdir(dir: "${basedir}/grails-app/controllers")
mkdir(dir: "${basedir}/grails-app/services")
mkdir(dir: "${basedir}/grails-app/domain")
mkdir(dir: "${basedir}/grails-app/taglib")
mkdir(dir: "${basedir}/grails-app/utils")
mkdir(dir: "${basedir}/grails-app/views")
mkdir(dir: "${basedir}/grails-app/views/layouts")
mkdir(dir: "${basedir}/grails-app/i18n")
mkdir(dir: "${basedir}/grails-app/conf")
mkdir(dir: "${basedir}/test")
mkdir(dir: "${basedir}/test/unit")
mkdir(dir: "${basedir}/test/integration")
mkdir(dir: "${basedir}/scripts")
mkdir(dir: "${basedir}/web-app")
mkdir(dir: "${basedir}/web-app/js")
mkdir(dir: "${basedir}/web-app/css")
mkdir(dir: "${basedir}/web-app/images")
mkdir(dir: "${basedir}/web-app/META-INF")
mkdir(dir: "${basedir}/lib")
mkdir(dir: "${basedir}/grails-app/conf/spring")
mkdir(dir: "${basedir}/grails-app/conf/hibernate")
}
}
Właśnie to zadanie najbardziej uzmysławia, że to co się dzieje pod spodem create-app, to nic innego jak stary, dobry Ant przesłonięty Groovy DSL. Zero magii (która towarzyszyła mi na początku poznawania Grails).

Następnie pora na stworzenie klasy dziedzinowej poleceniem grails create-domain-class pl.jaceklaskowski.nauczyciel.Pierwiastek. Zazwyczaj polecenia do tworzenia klas w Grails nie zawierają opcjonalnego pakietu. Postanowiłem zmienić ten zwyczaj (u siebie przynajmniej). Zalet nadawania pakietów do naszych klas nie ma co przedstawiać - ochrona przez potencjalnym konfliktem z tą samą klasą z innego projektu. Do klasy dodaję trzy atrybuty - symbol, nazwa i opis. Warto w tym momencie skorzystać z jakiegoś IDE - ja korzystam z NetBeans 6.7 (wersja z dzisiaj).
 package pl.jaceklaskowski.nauczyciel

class Pierwiastek {

String symbol
String nazwa
String opis

}
Mamy dziedzinę (model). Pozostaje stworzyć dla niej kontroler. Możemy tak - grails create-controller pl.jaceklaskowski.nauczyciel.Pierwiastek, albo podpieramy się narzędziami w IDE (na węźle Controllers wybieramy New > Grails Controller...).
 package pl.jaceklaskowski.nauczyciel

class PierwiastekController {

def index = { }
}
Teraz mamy kilka możliwości - albo kasujemy domknięcie def index i definiujemy def scaffold = Pierwiastek, co włączy dynamiczne rusztowanie (ang. scaffolding), albo tworzymy widok, który będzie odpowiadał domknięciu index (w przeciwnym przypadku skończymy z 404 jak w Wystąpienia publiczne jako utrwalenie wiedzy z Grails w tle). Najlepiej byłoby połączyć oba rozwiązania i wiem, że się da, ale raz, że nie pamiętam dokładnie jak, a dwa, że nie będę teraz aż tak kombinował. Wybieram stworzenie widoku dla akcji index. I ponownie mamy kilka opcji - albo grails generate-views pl.jaceklaskowski.nauczyciel.Pierwiastek, albo podparcie się IDE do stworzenia domyślnej, pustej funkcjonalnie strony grails-app/views/pierwiastek/index.gsp. Wybieram pierwsze podejście (jeszcze z niego nie korzystałem publicznie). W katalogu grails-app/views/pierwiastek powinny pojawić się 4 pliki, które odpowiadają każdej z akcji CRUD:
 $ ls grails-app/views/pierwiastek/
create.gsp edit.gsp list.gsp show.gsp
Ale moment! Nie ma index.gsp?! Zatem nie pozostaje mi nic innego jak wrócić do stworzenia jej ręcznie. Z pomocą NetBeans otrzymuję następującą stronę grails-app/views/pierwiastek/index.gsp:
 <%@ page contentType="text/html;charset=UTF-8" %>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Lista pierwiastków</title>
</head>
<body>
<h1>Na razie niewiele, ale początek już mamy...</h1>
</body>
</html>
Uruchomienie aplikacji poleceniem grails run-app i chwila na zastanowienie, co dalej.

Aktualna wersja aplikacji znajduje się w repozytorium git jako projekt nauczyciel. Zainteresowanych udziałem w tym mini-projekcie zapraszam do kontaktu. Najgorzej będzie ze stylami (rozmieszczę div'y, ale pewnie nic poza tym), więc jeśli masz zmysł plastyczny i potrafisz stworzyć właściwe CSSy koniecznie musimy obgadać temat współpracy.

6 komentarzy:

  1. Może się przyda się przyda http://www.mor.ph/ ;)

    "You can deploy Rails, Java, Grails, and PHP applications to Morph AppSpace."

    Pozdrawiam,
    Krzysztof Kowalczyk

    OdpowiedzUsuń
  2. Już od jakiegoś czasu zastanawiam się nad umieszczaniem klas domenowych i kontrolerów w pakietach, do tej pory tego nie robiłem: po co przecież mój projekt jest mały, nie dojdzie do konfliktów nazw. Ale chyba za Twoim przykładem zacznę robić porządek w klasach :)
    A tak przy okazji: ciekawe jak zachowają się Grails'y w sytuacji dwóch klas domenowych o tej samej nazwie, ale w różnych pakietach? Czy aby GORM nie zgłupieje? Jak to zostanie odwzorowane w bazie?
    Muszę sprawdzić :)

    OdpowiedzUsuń
  3. Uprzejmie donoszę, że sprawdziłem. GORM zgłupiał, ale tylko troszkę. Utworzył jedną tabelę i wszystko wrzuca do niej. Utworzyłem dwie klasy Test w różnych pakietach, uruchomiłem aplikację - w bazie pojawiła się jedna tabela. Dodałem do kontrolera utworzenie instancji i zapisanie dla obu typów - dwa rekordy w jednej tabeli.

    OdpowiedzUsuń
  4. A tu jeszcze parę słów na ten temat:
    http://tech.mrozewski.pl/2009/03/13/grails-i-klasy-domenowe-o-tych-samych-nazwach/

    OdpowiedzUsuń
  5. Dlaczego warto umieszczać kasy domenowe w pakietach:
    http://marceloverdijk.blogspot.com/2009/03/grails-tip-of-day-always-use-packages.html

    Idealnie wpasowało się w naszą dyskusję :)

    OdpowiedzUsuń