Grails-Tutorial
Autorin: Dr. Silvia Rothen, rothen ecotronics, Bern, Schweiz
Letzte Überarbeitung: 13.05.18
Grails ist ein javabasiertes Framework zur Erstellung von Webapplikationen mit Datenbankanbindung. Das vorliegende Tutorial konzentriert sich vor allem auf die Einbindung bestehender ERP-Datenbanken im Enterprise-Bereich. Dabei habe ich vor allem Aufgabenstellungen evaluiert, die mir bis jetzt beim Entwickeln von Webapplikationen mit ADF Business Components und ADF Faces begegnet sind.
Mein anfänglicher Enthusiasmus ist einer gewissen Skepsis gewichen. Wie ich Grails in Bezug auf die Anbindung von existierenden Oracle-Datenbanken einschätze, lesen Sie in "Grails and real databases - a bumpy road".
Achtung: Dieses Tutorial entstand über mehrere Jahre. Begonnen habe ich ungefähr mit der Version 0.5. Nicht alles stimmt noch für die aktuelle Version. Letzte Änderungen stammen vom Februar 2012 und beziehen sich auf die Grails-Version 2.0.0.
Inhaltsverzeichnis
- Was ist Grails
- Schneller Einstieg
- Versionswechsel
- Ausführen einer bestehenden Applikation
- Grails und Datenbankanbindung
- Datenquellen
- Grails an Oracle XE anbinden
- 1:1-, 1:n-, m:n-Beziehungen
- Mapping bestehender Datenbanken mit ORM DSL ab Version 1
- Mapping bestehender Oracle-Datenbanken mit
Annotations
- Registrierung in hibernate.cfg.xml
- Einträge in Datasource.groovy
- Import-Statement
- Mit Oracle-Sequence generierter Primärschlüssel
- Optimistic Locking mit Feld Version
- Read-only-Tabelle
- Read-only Spalte in aktualisierbarer Tabelle
- Where-Clause für Subset einer Tabelle
- Mit Annotation Datentyp CLOB erzwingen
- Mapping bestehender Oracle-Datenbanktabellen mit XML
- Stored Procedure einbinden
- Standardmappings von Grails- auf Oracle-Datentypen
- DB-Views in Grails
- Allgemeine Tipps und Tricks mit Datenbanken
- IDE für Grails einrichten
- Architektur einer Grails-Anwendung
- Verzeichnisstruktur einer Grails-Applikation
- Grails-Konventionen
- Grails-Befehle
- GSP-Syntax
- Testen von Grails-Applikationen
- Grails-Snippets
- Grails Tipps und Tricks
- Passwortgeschützter Website mit Acegi-Plugin
- Internationalisierung (i18n)
- Groovy
- Glossar
- Quellen und Links
Was ist Grails
- Framework zur Erstellung von Web-Anwendungen
- Grails basiert
auf der Java-Plattform und auf robusten
Java-Komponenten sowie
Frameworks, unter anderem:
- Spring: Version 3.1.0 (in Grails-Version 2.0.0)
- Hibernate 3: OR-Mapper, Hibernate-Version 3.6.7 sowie 3.2.0 für die Hibernate Annotations (in Grails-Version 2.0.0)
- Ant: Version 1.8.2 (in Grails-Version 2.0.0)
- Sitemesh: Version 2.4 (in Grails-Version 2.0.0)
- Spezialisiert auf CRUD-Applikationen, d.h. Web-Applikationen mit Create, Read, Update, Delete auf Datenbanktabellen. Die Autoren schwärmen zwar alle davon, dass sich die Anwendungsmöglichkeiten von Grails nicht darauf beschränken, aber alle Tutorials und Artikel, die ich bisher gesehen habe, zeigen leider noch nicht viel anderes.
- Webseiten- und Code-Generierung für CRUD-Operationen
- Programmiersprache: Groovy
- Groovy ist keine Skriptsprache, sondern wird wie Java von der Java Virtual Machine zu Bytecode kompiliert.
- Grails erzeugt WAR-Dateien, die in jedem Java-EE-Applikationsserver laufen
- Konzepte wie bei Rails und Ruby
- Konventionen statt Konfiguration (Convention over Configuration)
- Grails-Release 0.1: März 2006
- Projektleader von Grails: Graeme Rocher
- Projektleader von Groovy: Guillaume Laforge
- Aktuelle Version im Februar 2012: Grails 2.0.0
Schneller Einstieg
Bezieht sich auf Grails-Version 1.0.3 unter Windows.
- Falls nicht bereits vorhanden, ein Java JDK installieren, mindestens
1.4, aber für Annotations und ähnliches möglichst Version 1. 5 oder 1.6.
Achtung: es muss ein JDK vorhanden sein, JRE reicht nicht. Das kein JDK vorhanden ist, merkt man u.a. daran, dass im Verzeichnis \bin\ die Datei native2ascii.exe nicht vorhanden ist - Grails von grails.org
herunterladen und und in Verzeichnis entzippen, z.B
C:\Programme\programming\grails-1.0.3
Neu gibt es für Windows auch einen Installer, der ein Untermenü ins Startmenü einbindet, aber die Umgebungsvariablen muss man nach wie vor manuell setzen. - In Umgebungsvariablen (Arbeitsplatz - Systemeigenschaften -
Erweitert) neue Systemvariable erstellen
set GRAILS_HOME=C:\Programme\programming\grails-1.0.3 - Systemvariable für JAVA_HOME erstellen oder
anpassen, falls bereits vorhanden.
set JAVA_HOME=C:\Programme\programming\javaclasses\jdk1.6.0_05
Unter Windows lassen sich die Systemvariablen auf der Kommandozeile z.B. mit echo %JAVA_HOME% überprüfen - In Umgebungsvariablen (Arbeitsplatz - Systemeigenschaften -
Erweitert) Path ergänzen oder falls bereits vorhanden abändern mit
%JAVA_HOME%\bin;%GRAILS_HOME%\bin - Von jetzt an passiert alles auf der Kommandozeile:
In Verzeichnis gehen, wo neue Grails-Applikation erzeugt werden soll, z.B.
cd C:\dateienmitback\programming\grails\ - Applikation anlegen mit
grails create-app
Namen für Applikation angeben, z.B. mybookmarks - Ins Verzeichnis der Applikation wechseln
cd mybookmarks - Ein neues Objekt erzeugen mit
grails create-domain-class
Objektnamen eingeben, z.B. user
Kann in Kleinschrift eingegeben werden, Klasse erhält automatisch den Namen "User". - Klasse User suchen und öffnen, z.B.
C:\dateienmitback\programming\grails\mybookmarks\grails-app\domain\User.groovy - Klasse mit ein paar Feldern ergänzen
class User {
static hasMany = [bookmarks:Bookmark]
String loginname
String email
} - DB-Mapping erzeugen mit grails
generate-all
Wenn nach der Domain-Klasse gefragt wird, den Namen eingeben, aber mit Grossbuchstaben am Anfang, also User. - Applikation starten mit
grails run-app
Wenn Port 8080 bereits belegt ist, weil man z.B. Oracle XE installiert hat:
grails -Dserver.port=9090 run-app - Wenn Meldung "Server running".. erscheint,
Applikation laden, z.B. mit
http://localhost:9090/mybookmarks/
Versionswechsel
Versionswechsel von Grails führen dazu, dass man auch seine Projekte mit grails upgrade updatet. Je nach Version sind weitere Änderungen nötig.
JQuery und der Wechsel zu Version 2.0.0
Ab Grails-Version 2 ist JQuery automatisch die Standard-JavaScript-Library. Damit erfolgt die Einbindung anders als früher. Anstelle von
<g:javascript library="jquery" />
kommen die folgenden zwei Zeilen in den Header einer Seite, beziehungsweise in die Template-Seite, wenn die Einbindung für alle Seiten gelten soll:
<r:layoutResources/>
<r:require modules="jquery-ui, blueprint"/>
Version 2.0.0 und H2
Neuerdings wird die H2-DB standardmässig eingebunden. Dazu gehört auch eine Konsole, die man während der Ausführung des Projektes mit projekt/dbconsole aufrufen kann, also z.B. mit http://localhost:9090/mybatis/dbconsole/Allerdings gibt es bei der Konsole auch einen Stolperstein: Die vorgeschlagene URL entspricht nicht unbedingt der im Projekt verwendeten URL. In diesem Feld muss unbedingt dasselbe eingetragen werden wie in der Konfiguration der Datasource:
Insbesondere wenn man (so wie ich) übersieht, dass am einen Ort "file" und am andern "mem" steht, dann wird man sich wundern, wo die Daten eigentlich geblieben sind.
Version 2.0.0 und Oracle
Und gleich noch ein Stolperstein: Wer Grails mit Java 6 und Oracle einsetzt, muss auch den entsprechenden Oracle Treiber ersetzen, d.h. statt ojdbc14.jar kommt jetzt ojdbc6.jar ins lib-Verzeichnis. Den Driver kann man unter http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-10201-088211.html herunterladen. Ausserdem schadet es nicht, grails clean abzusetzen, bevor man das Projekt nach dem Update startet.
Wechsel von Version 1.0.3 zu 1.0.4
Für den Upgrade unbedingt zuerst für jedes Projekt die folgenden zwei Befehle auf der Kommandozeile absetzen:
- grails clean
- grails upgrade
Wechsel von Version 0.5.6 zu 0.6
Folgende Anpassungen sind generell und in den Projekten nötig:- Beim Wechsel auf eine neue Version, z.B. von grails 0.5.6 zu grails 0.6, müssen die Umgebungsvariablen PATH und GRAILS_HOME angepasst werden.
- Projekte mit einer Version 0.4 oder älter müssen ausserdem mit den Befehlen grails clean und grails upgrade aktualisiert werden (klappt nicht immer).
- Wer eigene Taglibs oder andere spezielle Elemente in diesen Projekten hat, sollte sich unbedingt vorher die README-Datei durchlesen!
- Wenn externe Datenbanken eingebunden sind, dann müssen die Einträge aus den drei alten Dateien DevelopmentDataSource.groovy, ProductionDataSource.groovy und TestDataSource.groovy in die neue Datei DataSource.groovy übertragen werden.
- Generell müssen alle Werte aus den alten Konfigurationsdateien in die neuen übertragen werden. Anschliessend wird das Verzeichnis config von allen Dateien gesäubert ausser den vier folgenden:
- BootStrap.groovy (vorher ApplicationBootStrap.groovy)
- Config.groovy (vorher 3 log4j...-Dateien)
- DataSource.groovy (vorher DevelopmentDataSource.groovy, ProductionDataSource.groovy und TestDataSource.groovy)
- UrlMappings.groovy (vorher ApplicationNameUrlMappings.groovy)
- Das Hibernate-Verzeichnis und das Spring-Verzeichnis stecken neu in \grails-app\conf\
- Das Plugin-Verzeichnis ist verschwunden
Ausführen einer bestehenden Applikation
- Ins Stammverzeichnis der Applikation wechseln
- grails -Dserver.port=9090 run-app aufrufen
- Wenn Meldung "Server running".. erscheint, Applikation
laden, z.B. mit
http://localhost:9090/mybookmarks/user/list
Grails und Datenbankanbindung
Achtung: Defaultmässig ist Grails für die Verwendung einer RAM-Datenbank konfiguriert. Sobald man die Applikation stoppt, sind alle Daten weg!
Datenquellen
Eine Standardinstallation von Grails ist bereits für die Verwendung einer In-Memory-Datenbank (HSQLDB) eingerichtet. Mit den Datasource-Dateien im Verzeichnis grails-app/conf/ lässt sich Grails für die Verwendung beliebiger Datenbanken konfigurieren. Vorgesehen sind in Version 0.5.6 drei Datasource-Dateien für Entwicklungs-, Test- und produktive Umgebung:
- DevelopmentDataSource.groovy
- ProductionDataSource.groovy
- TestDataSource.groovy
Ab Version 0.6 sind diese 3 Datenquellen in einer einzigen Datei DatSource.groovy in den folgenden 3 Closures abgelegt:
- development
- production
- test
Die Auswahl der jeweiligen Umgebung erfolgt über den Aufruf grails run-app. Ohne weitere Angaben wird die Entwicklungsumgebung gestartet. Soll stattdessen die produktive oder die Testumgebung gestartet werden, dann wird prod oder dev in den Befehl eingefügt, also z.B. grails prod run-app.
Grails an Oracle XE anbinden
Soll statt der In-Memory-Datenbank (HSQLDB) eine richtige Datenbank verwendet werden, dann muss Grails zuerst dafür konfiguriert werden. Hier eine kurze Beschreibung für die Verwendung von Oracle XE. Eine ausführliche Beschreibung findet sich unter:
http://www.oracle.com/technology/pub/articles/grall-grails.html
Oracle Konfiguration auf der angegebenen Webseite funktioniert mit zwei Anpassungen
- Syntax für Klassen: @Property fällt weg
- es gibt ab Version 0.6 ein Konfigurationsfile DataSource.groovy mit 3 Closures für die Datasource, abgeändert habe ich development
Die wichtigen Punkte für die Konfiguration von Grails mit Oracle:
- ODBC-Treiber in der Datei ojdbc14.jar aus dem Oracle-Verzeichnis ORACLE_XE_HOME/app/oracle/product/10.2.0/server/jdbc/lib/ in Verzeichnis \lib\ der Grails-Applikation kopieren
- Datei ..\grails-app\conf\DataSource.groovy anpassen (siehe Codebeispiel)
dataSource {
pooled = false
driverClassName = "org.hsqldb.jdbcDriver"
username = "sa"
password = ""
}
// environment specific settings
environments {
development {
dataSource {
url = "jdbc:oracle:thin:@localhost:1521:XE"
driverClassName = "oracle.jdbc.OracleDriver"
username = "meinname"
password = "xxxxxx"
}
}
....
Hier noch die Konfiguration von Version 0.5.6
class DevelopmentDataSource {
boolean pooling = true
String dbCreate = "update"
String url = "jdbc:oracle:thin:@localhost:1521:XE"
String driverClassName = "oracle.jdbc.OracleDriver"
String username = "meinname"
String password = "xxxxxx"
}
Achtung:
Mit dbCreate lässt sich einstellen, ob die Applikation selbst Tabellen anlegen, aktualisieren und verändern darf, d.h. ob sie das Datenbankschema ändern darf. Die drei Optionen sind
- create-drop: Datenbank wird immer gedropped und neu erstellt. Alle Daten werden gelöscht.
- create: Datenbank wird neu erstellt, falls sie nicht existiert. Falls sie existiert, wird sie nicht aktualisiert. Alle Daten werden gelöscht.
- update: Erzeugt die Datenbank, falls sie existiert und aktualisiert sie, falls sie nicht existiert. Bestehende Daten bleiben erhalten.
Wenn Sie mit bestehenden Tabellen arbeiten (die Grails-Literatur spricht mit einer gewissen Überheblichkeit von Legacy-Databases) oder ihr Datenbankschema nur manuell ändern möchten, dann löschen Sie unbedingt die folgende Zeile:
String dbCreate = "update"
Quelle für diesen Tipp:
Jason Rudolph, InfoQ, Getting Started with Grails
Die Tabellen werden übrigens erst beim Befehl run-app angelegt oder geändert, nicht bereits beim Scaffolding mit generate-all.
1:1-, 1:n-, m:n-Beziehungen
Für die Persistenzschicht sind in Grails die Domain-Klassen zuständig. Verschiedene Arten von Beziehungen werden dort folgendermassen umgesetzt.
Pseudo-1:1-Beziehung in Version 1.0.3
Das vorliegende Beispiel lehnt sich an das Face-Nose-Beispiel aus Kapitel 5 der Reference Documentation an, baut dieses aber in den folgenden Punkten aus, so dass es den Anforderungen einer bestehenden Datenbank entspricht:
- Die Spalte mit dem Fremdschlüssel entspricht nicht den Grails-Konventionen
- Die Beziehung ist unidirektional (nur ein Fremdschlüssel in der Detailtabelle), aber es soll trotzdem beim Löschen und Speichern von der Mastertabelle zur Detailtabelle kaskadiert werden
Die in der Dokumentation vorgeschlagene Lösung kann unter diesen Voraussetzungen nicht verwendet werden: Damit das Löschen und Speichern kaskadiert, muss die Mastertabelle eine Beziehung zur Detailtabelle aufweisen, was auf Datenbankebene als Fremdschlüssel in Master- und Detailtabelle realisiert wird. Da eine Nase zu einem Gesicht gehört, wird Face als die Master- und Nose als die Detailtabelle betrachtet.
Hier der Vorschlag aus der Dokumentation:
class Face {
Nose nose
}
class Nose {
static belongsTo = [face:Face]
}
Statt der vorgeschlagenen Variante benutze ich eine 1:n Beziehung, die über eine Constraint unique auf dem Fremdschlüssel in der Detailtabelle in eine 1:1-Beziehung umgewandelt wird.
class Face {
static hasMany = [noses: Nose]
}
class Nose {
static belongsTo = [face:Face]
static mapping = {
columns {
face column:"FACENR"
}
}
static constraints = {
face unique:true
}
}
Mit static mapping wird zudem auf einen Fremdschlüssel gemapped, der nicht face_id, sondern FACENR heisst.
Einfache 1:n Beziehung
Das Beispiel bezieht sich auf Version 1.0.3. Das folgende Beispiel zeigt, wie eine 1:n-Beziehung zwischen zwei Domain-Klassen (d.h. Tabellen) erstellt werden kann. Dies funktioniert auch in Oracle, solange der User in der Datasource auch die notwendigen Grants hat, um Tabellen zu erstellen und abzuändern. In diesem Beispiel gehe ich von Grails-konformen Tabellen aus, d.h. die Namenskonventionen werden eingehalten, die Schlüssel kommen von Grails, nicht aus einer Sequence etc. Weiter unten finden sich Hinweise, wie man mit bestehenden, nicht grailskonformen Datenbanken und vorhandenen Daten arbeitet.
Das Beispiel geht (wie die Grails Reference Documentation) von den folgenden zwei Domain-Klassen aus:
- Author ist die Mastertabelle (der 1-Teil der Beziehung)
- Book ist die Detailtabelle (der n-Teil der Beziehung)
Die Mastertabelle sieht folgendermassen aus:
class Author {
static hasMany = [bookList : Book]
String name
String toString() {
return "${id} ${name}"
}
}
Die Detailklasse dagegen benötigt zwei Sachen: erstens ein Feld vom Typ der Masterklasse und zweitens den Vermerk static belongsTo...
class Book {
static belongsTo = [Author]
String title
Author author
String toString() {
return "${id} ${title}"
}
}
Die Methode toString() dient übrigens dazu, dass DropDown-Felder gleich von Anfang an einen brauchbaren Text anzeigen. Die Zeile mit belongsTo ist nur notwendig, wenn beim Löschen von Datensätzen in der Tabelle Author auch eine Löschweitergabe an Book erfolgen soll. Für die Datenbanktabellen bedeutet dies, dass ein Delete in der Master-Tabelle Author ein cascading Delete in der Detailtabelle Book auslöst.
Der Befehle "static belongsTo" darf in einer Klasse nur einmal vorkommen. Hat die Klasse Fremdschlüssel von mehreren anderen Klassen, dann lautet die Syntax folgendermassen:static belongsTo = [User, Category, Keyword]
Um diese 1:n-Beziehung mit Daten zu füllen, benützt man addTo....
Author authorInstance = new Author(name: "Silvia Rothen")
Book bookInstance = new Book(title: "Kohlendioxid und Energie")
authorInstance.addToBookList(bookInstance)
Achtung, hier hat das API seit Version 0.5 geändert, der Aufruf
authorInstance.addBook(bookInstance) ist deprecated! In älteren
Grails-Büchern und Artikeln findet man manchmal noch die veraltete
Schreibweise.
m:n-Beziehung
Bei der m:n-Beziehung läuft es ähnlich wie im Fall der 1:n-Beziehung. Es kommt einfach noch ein "static hasMany" dazu. Die Zwischentabelle, mit der eine m:n-Beziehung datenbankseitig realisiert wird, tritt in Grails nicht als Domain-Klasse in Erscheinung. Nur bei einer nichtgrailskonformen Namensgebung tritt sie im static mapping der beiden anderen Tabellen in Erscheinung. Wie bei der 1:n-Beziehung muss aber genau eine Klasse mit belongsTo als untergeordnete Klasse gekennzeichnet werden. Wird ein Master-Objekt gelöscht, dann werden alle zugehörigen Detail-Objekte mitgelöscht!
Es gibt die folgenden 2 Tabellen, zwischen denen eine Many-to-Many-Relation besteht:
- AUTHOR: Mastertabelle, PK ID
- BOOK: Detailtabelle, PK ID
Die Zwischentabelle heisst GRAILS-konform AUTHOR_BOOK. Sie hat einen zusammengesetzten Primärschlüssel, der aus den folgenden 2 Felder besteht:
- BOOK_ID
- AUTHOR_ID
Nun kann das Mapping direkt in den Domain-Dateien erstellt werden. Die entsprechenden Einträge in den Domain-Klassen sehen folgendermassen aus.
Author.groovy:
class Author {
static mapping = {
table 'AUTHOR'
bookList column:'BOOK_ID', joinTable:'AUTHOR_BOOK'
columns {
id (column:'ID')
version (column:'VERSION')
name (column:'NAME')
}
}
java.lang.String name
static hasMany = [ bookList : Book ]
static constraints = {
id(nullable: false, size: 0..19)
version(nullable: false, size: 0..19)
name(size: 1..1020, blank: false)
bookList()
}
}
Book.groovy
class Book {
static mapping = {
table 'BOOK'
authorList column:'AUTHOR_ID', joinTable:'AUTHOR_BOOK'
columns {
id (column:'ID')
version column:'VERSION'
title column:'TITLE'
}
}
java.lang.String title
static hasMany = [ authorList : Author ]
static belongsTo = [Author]
static constraints = {
id(nullable: false, size: 0..19, type:java.math.BigDecimal)
version(nullable: false, size: 0..19,
type:java.math.BigDecimal)
title(size: 1..1020, blank: false)
authorList()
}
}
Für die Hilfstabelle AUTHOR_BOOK ist keine Domainklasse nötig.
Beim Scaffolding gegen Oracle XE wird eine Zwischentabelle generiert, z.B. mit dem Tabellennamen BOOK_KEYWORD, wobei die zweite Namenskomponente jene Klasse mit dem belongsTo ist. Diese Zwischentabelle enthält erwartungsgemäss nichts anderes als die zwei Fremdschlüssel zu den anderen Tabellen.
Achtung:
Das Scaffolding funktioniert für die Views bei einer m:n-Beziehung nicht (zumindest nicht in Version 1.0.3). Die entsprechenden Seiten müssen manuell angepasst werden.
Mapping bestehender Datenbanken mit ORM DSL ab Version 1
Der Text in diesem Abschnitt bezieht sich auf Grails ab Version 1.0.4
HQL mit executeQuery
In gewissen Situationen kommt man an native SQL nicht vorbei. Eine Möglichkeit, die dem schon relativ nahe kommt, ist die Verwendung der Methode executeQuery mit HQL in einer Domain-Klasse. Allerdings gibt es ein paar Stolpersteine:
- Wenn die Domain-Klasse nicht gleich heisst wie die Datenbank-Tabelle, dann steht hinter FROM der Name der Domain-Klasse, nicht jener der Tabelle.
- executeQuery kennt im Gegensatz zu list keine Sortierung, d.h. man muss sie selbst zusammenbasteln
- executeQuery liefert standardmässig eine ArrayList zurück und nicht wie list eine Map. Wenn man trotzdem eine Map zurückhaben möchte, dann muss man dies im SQL-Statement einbauen.
- HQL sieht zwar im ersten Moment wie native SQL aus, aber bei proprietären Elementen, z.B. bei Oracle Datenbanken mit DECODE oder CONNECT BY, steigt HQL aus
Schauen wir uns zuerst die Domain-Klasse an:
class Book2ExecuteQuery {
String title
String description
static mapping = {
table 'BOOK2'
}
static constraints = {
title size: 1..255, blank: false
description nullable: true, size: 0..255
}
}
Speziell ist daran nur das Mapping der Tabelle. Beim Kontroller wird es viel interessanter. Die Methode executeQuery dürfte im Normalfall v.a. in der Closure list zum Zuge kommen, da man dort am ehesten die angezeigten Datensätze beschränken möchte. Damit ist die vorgestellte Technik eine Alternative zur WHERE-Clause, die sich ja bisher nur mit Annotations realisieren lässt.
class Book2ExecuteQueryController {
...
def list = {
if(!params.max) params.max = "5"
if(!params.offset) params.offset = "0"
if(!params.sort) params.sort = "id"
if(!params.order) params.order = "desc"
def results
results = Book2ExecuteQuery.executeQuery(
"""SELECT new map(b.id as id,
b.version as version,
b.title as title, b.description as description)
FROM Book2ExecuteQuery b
WHERE b.id >= :minId
ORDER BY b.${params.sort} ${params.order}
""", [minId: 200L], [max:
params.max.toInteger(),
offset: params.offset.toInteger()])
return [book2ExecuteQueryInstanceList:
results]
}
}
Die vorliegende Lösung unterstützt sowohl die Sortierung über die Spaltenköpfe wie das Paging. Man beachte auch das params.max.toInteger(), das nötig ist, weil Parameter normalerweise Strings zurückliefern.
Native SQL mit createSQLQuery
Eine zweite Möglichkeit, native SQL für ein Domain-Objekt zu verwenden, ist die Methode createSQLQuery. Diese Methode gehört nicht zur Domainklasse, sondern zur Hibernate-Session. Mit der Methode addEntity kann das Resultat der Query aber auf eine Domain-Objekt gemapped werden.
Das Beispiel zeigt einige interessante Punkte:
- Oracle selfjoin, um Baumstruktur in einer Tabelle abzubilden
- native SQL mit createSQLQuery
- parametrisiertes SQL mit benannten Parametern
- Mapping auf eine Domainklasse mit addEntity
- Paging der resultierenden Liste
- Sortierbare Liste
- read-only Domainklasse
- in Domainklasse zentralisiertes SQL
Fangen wir mit der Domainklasse an. Hier sind eigentlich nur die zwei SQL Statements sowie das "cache 'read-only'", mit dem die Klasse nur lesbar gemacht wird, interessant. Das static mapping "mutable false", das eigentlich der Hibernate-Syntax entsprechen würde, hat laut einer Nabble-Diskussion gar nie funktioniert, wurde aber in Version 1.0.4 aufgrund eines Bugs nicht abgefangen. Ab Version 1.1. funktioniert es definitiv nicht mehr.
Im SQL sollten Sie ein paar Punkte beachten:
- das SQL-Statement findet alle Angestellten einer Hierarchiestufe mit Hilfe der errechneten Spalte LEVEL
- die SQL-Statements für list und get sind nicht identisch, da für get aus Performance-Gründen nur mit dem Primärschlüssel zugegriffen wird, für die Liste dagegen mit einem beliebigen Kriterium. Im aktuellen Beispiel ist dies :pLevel
- der Primärschlüssel wird bereits im SQL auf den grailskonformen Namen gemapped
- es ist ein benannter Parameter vorhanden
- das SQL enthält kein ORDER BY, da die Sortierung im Controller dynamisch angehängt wird
class EmployeeCreateSqlQuery {
static final SQL_LIST = """
SELECT
LEVEL,
emp.employee_id as id,
man.last_name || ' ' || man.first_name AS
manager,
emp.last_name || ' ' || emp.first_name AS
employee,
dep.department_name as departmentname
FROM employees emp,
employees man,
departments dep
WHERE LEVEL = :pLevel
AND emp.manager_id = man.employee_id (+)
AND emp.department_id = dep.department_id
START WITH emp.manager_id IS NULL
CONNECT BY PRIOR emp.employee_id = emp.manager_id
"""
static final SQL_GET = """
SELECT
LEVEL,
emp.employee_id as id,
man.last_name || ' ' || man.first_name AS
manager,
emp.last_name || ' ' || emp.first_name AS
employee,
dep.department_name as departmentname
FROM employees emp,
employees man,
departments dep
WHERE emp.manager_id = man.employee_id (+)
AND emp.department_id = dep.department_id
AND emp.employee_id = :pId
START WITH emp.manager_id IS NULL
CONNECT BY PRIOR emp.employee_id = emp.manager_id
"""
static mapping = {
table 'EMPLOYEES'
version false
cache 'read-only'
}
Byte level
String manager
String employee
String departmentname
static constraints = {
id
(nullable: false, size: 1..6)
level
(nullable: false, inList:[1,2,3,4])
manager (nullable:
true, size: 0..46)
employee (nullable:
true, size: 0..46)
departmentname (nullable: true, size: 1..30)
}
}
Wenn wir die Klasse soweit definiert haben, dann lässt sich das Gerüst für Controller und View mit "grails generate-all" erstellen. Da wir eine Domain-Klasse haben, die nicht aktualisierbar ist, können wir im Controller alle Closures ausser jenen für list und show löschen. Auch bei den Views bleiben nur list.gsp und show.gsp übrig.
Beim Controller muss man als erstes die Session Factory einfügen. Für die Liste braucht es zwei SQL-Statements, nämlich neben jenem für die Liste selbst noch eines, welches das Total für das Paging berechnet.
class EmployeeCreateSqlQueryController {
org.hibernate.SessionFactory sessionFactory
def index = {redirect(action: list, params: params)}
def list = {
if (!params.max) params.max = 10
if (!params.offset) params.offset = 0
if (!params.level) params.level = 3
if (!params.sort) params.sort = "id"
if (!params.order) params.order = "ASC"
String sqlList = EmployeeCreateSqlQuery.SQL_LIST +
" ORDER BY ${params.sort} ${params.order}"
String sqlTotal = """
SELECT COUNT(*) total
FROM (${EmployeeCreateSqlQuery.SQL_LIST})"""
def query = sessionFactory.currentSession
.createSQLQuery(sqlList)
.addEntity("employeeCreateSqlQuery",
EmployeeCreateSqlQuery.class)
query.setParameter("pLevel", params.level)
query.setFirstResult(params.offset.toInteger())
query.setMaxResults(params.max.toInteger())
def list = query.list()
def queryCount =
sessionFactory.currentSession
.createSQLQuery(sqlTotal)
queryCount.setParameter("pLevel", params.level)
def listCount = queryCount.list()
return [employeeCreateSqlQueryInstanceList:
list,
total: listCount[0]]
}
def show = {
def query = sessionFactory.currentSession
.createSQLQuery(EmployeeCreateSqlQuery.SQL_GET)
.addEntity("employeeCreateSqlQuery",
EmployeeCreateSqlQuery.class);
query.setParameter("pId", params.id)
def employeeCreateSqlQueryInstance = query &&
query.list().size() == 1 ? query.list()[0] :
null;
if (!employeeCreateSqlQueryInstance) {
flash.message = """EmployeeCreateSqlQuery
not found
with id ${params.id}"""
redirect(action: list)
}
else {return [employeeCreateSqlQueryInstance:
employeeCreateSqlQueryInstance]}
}
}
Die show.gsp-Seite wird so verwendet, wie sie generiert wurde, die list.gsp-Seite erfährt dagegen ein paar minimale Änderungen, damit das Paging und das Sortieren via Spaltenköpfe funktioniert.
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html;
charset=UTF-8"/>
<meta name="layout" content="main"/>
<title>Employee List</title>
</head>
<body>
<div class="nav">
<span class="menuButton">
<a class="home" href="${createLinkTo(dir:
'')}">Home</a>
</span>
</div>
<div class="body">
<h1>EmployeeCreateSqlQuery
List</h1>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<p>Level:
<g:link action="list" params="[level:1]">1</g:link>
<g:link action="list" params="[level:2]">2</g:link>
<g:link action="list" params="[level:3]">3</g:link>
<g:link action="list" params="[level:4]">4</g:link>
</p>
<div class="list">
<table>
<thead>
<tr>
<g:sortableColumn property="id" title="Id"
params="${[level: params.level]}"/>
<g:sortableColumn property="level" title="Level"
params="${[level: params.level]}"/>
<g:sortableColumn property="manager"
title="Manager"
params="${[level: params.level]}"/>
<g:sortableColumn property="employee"
title="Employee"
params="${[level: params.level]}"/>
<g:sortableColumn property="departmentname"
title="Department Name"
params="${[level: params.level]}"/>
</tr>
</thead>
<tbody>
<g:each
in="${employeeCreateSqlQueryInstanceList}"
status="i" var="employeeCreateSqlQueryInstance">
<tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
<td>
<g:link action="show"
id="${employeeCreateSqlQueryInstance.id}">
${fieldValue(bean: employeeCreateSqlQueryInstance,
field: 'id')}
</g:link>
</td>
<td>
${fieldValue(bean: employeeCreateSqlQueryInstance,
field: 'level')}
</td>
<td>
${fieldValue(bean: employeeCreateSqlQueryInstance,
field: 'manager')}
</td>
<td>
${fieldValue(bean: employeeCreateSqlQueryInstance,
field: 'employee')}
</td>
<td>
${fieldValue(bean: employeeCreateSqlQueryInstance,
field: 'departmentname')}
</td>
</tr>
</g:each>
</tbody>
</table>
</div>
<div class="paginateButtons">
<g:paginate
total="${total}" params="${params}"/>
</div>
</div>
</body>
</html>
Zugewiesener Primärschlüssel
Die folgende Methode bezieht sich auf Version 1.3.5 oder später. Dies ist wichtig, da es in Vorversionen zum Teil tückische Bugs gab. Das Ziel ist es, einem Primärschlüssel manuell Werte zuzuweisen. Der Primärschlüssel entspricht dabei abgesehen von der manuellen Zuweisung den Konventionen, d.h. er ist numerisch und heisst id. Der Trick ist, dass man im Mapping für id eine Property generator:"assigned" definiert. Die Domain-Klasse sieht so aus:
class Genre {
String title
static mapping = {
id(generator:'assigned')
}
}
Wichtig ist, dass generator:'assigned' innerhalb von mapping steht und nicht etwa innerhalb von constraints. Dass ich dieses kleine Detail übersehen habe, hat mich ziemlich viel Zeit gekostet!
Einen Bug in Bezug auf zugewiesene Primärschlüssel gibt es in Grails in Version 1.3.5 immer noch: der Wert für id wird nicht automatisch aus den Parametern geholt, sondern man muss ihn explizit zuweisen. Für die closure save() muss also zwingend die fett markierte Zeile im folgenden enthalten:
def save = {
def genreInstance = new Genre(params)
//explizite Zuweisung nötig wegen Bug, zumindest bis Version 1.3.5
genreInstance.id = params.id.toInteger()
if (genreInstance.save(flush: true)) {
flash.message = "${message(code: 'default.created.message',
args: [message(code: 'genre.label', default:
'Genre'),
genreInstance.id])}"
redirect(action: "show", id: genreInstance.id)
} else {
render(view: "create", model: [genreInstance: genreInstance])
}
}
Dass es sich hierbei um einen Bug handelt sieht man auf dieser Seite.
Offene Punkte in ORM DSL unter Grails 1.0.3
Für die folgenden Probleme habe ich in ORM DSL noch keine Lösung gefunden (aber teilweise mit Hibernate Annotations):
- Domain class mit WHERE-Clause (Lösung mit Annotations)
- Read-Only Column, z.B. für ein Einfügedatum, dass in der Datenbank mit SYSDATE aktualisiert wird und in der Applikation nur angezeigt werden soll (Lösung mit Annotations)
Mapping bestehender Oracle-Datenbanken mit Annotations
Zur Zeit (d.h. November 2008) und mit der Grails-Version 1.0.4 ist das Mapping mit Annotations vermutlich der beste Kompromiss für Applikationen mit existierenden Datenbanken: Die Version mit Annotations ist besser in Grails-Domain-Klassen integriert als ein Mapping mit Hibernate-XML-Dateien. Gleichzeitig kann man aber viele anspruchsvolle Mapping-Aufgaben erledigen, die mit ORM DSL noch nicht möglich sind, z.B. Einbezug von Tabellenspalten, die read-only sind oder Beschränkung auf Zeilen anhand einer WHERE-Clause.
Registrierung in hibernate.cfg.xml
Damit die annotierten Klassen verwendet werden können, müssen Sie in grails-app/conf/hibernate/hibernate.cfg.xml registriert werden:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
...
<mapping class="AthleteAnnotated" />
</session-factory>
</hibernate-configuration>
Einträge in Datasource.groovy
Damit mit annotierten Domain-Klassen gearbeitet werden kann, braucht es die folgenden zwei Einträge in DataSource.groovy:
- Am Dateianfang muss das folgende Import-Statement stehen:
import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration - Die folgende Zuweisung platziert man am besten innerhalb des Blocks
DataSource
configClass = GrailsAnnotationConfiguration.class
Import-Statement
Soll das Mapping in den Domain-Klassen mit Annotations erfolgen, dann muss unbedingt das folgende Import-Statement am Anfang der Klasse stehen:
import javax.persistence.*
Leider wird dies bei ganz vielen Beispielen im Web unterschlagen!
Mit Oracle-Sequence generierter Primärschlüssel
Der folgende Code aus einer Domainklasse zeigt
- Primärschlüssel, der nicht der Namenskonvention entspricht
- aus Oracle-Sequence generierten Primärschlüssel
- sequenceName ist der Name der Sequence in der DB
- allocationSize=1 bewirkt, dass jeder Wert von der Datenbank geholt wird. Das verhindert Sprünge in der Nummerierung bei Neustart der Applikation, kostet aber Performance, weil jeder Wert von der DB geholt wird.
import javax.persistence.*
@Entity
@Table(name="ATHLETE")
@SequenceGenerator(name="athlete_seq",
sequenceName="ATHLETE_SEQ",
allocationSize=1)
class AthleteAnnotated implements Serializable {
@Id
@GeneratedValue (strategy=GenerationType.SEQUENCE,
generator="athlete_seq")
@Column (name="ATHLETEID")
Long id
....
}
Optimistic Locking mit Feld Version
Wenn für eine Tabelle optimistic locking zugelassen sein soll, dann benötigt sie ein Feld für die Version (im Normalfall ein numerisches Feld). Dieses Feld muss auch entsprechend annotiert werden, nämlich mit
class AthleteAnnotated implements Serializable {
...
@Version
Long version
...
}
Read-Only-Tabelle
Soll eine Klasse nur lesbar, aber nicht änderbar sein, dann lässt sich dies in der annotierten Entity mit dem Attribut mutable festlegen:
import javax.persistence.*
@Entity (mutable=false)
@Table(name="emp")
class Employee implements Serializable {
...
Read-only Spalte in aktualisierbarer Tabelle
Im konkreten Fall ging es um eine Tabelle, die ein mit SYSDATE abgefülltes Einfügedatum enthielt. Die Tabelle ist aktualisierbar, dieses Feld dagegen soll zwar angezeigt werden, aber den ursprünglichen Wert behalten. Die ORM-DSL-Lösung scheitert hier beim Insert: der einfache Trick, für das Feld kein Eingabefeld im Formular anzuzeigen, bringt nichts, da das Insert-Statement gnadenlos null in das Feld schreibt. Mit Annotations klappt es dagegen, weil das Feld in Insert- und Update-Statements ausgenommen wird:
@Column(name="INSERTDATE",
insertable=false, updatable=false)
Date insertdate
Where-Clause für Subset einer Tabelle
Arbeitet man mit einer existierenden Datenbank (in meinem Fall mit einem ausgewachsenen ERP), dann hat man das Problem, dass u.U. Tausende von Datensätzen existieren, aber für die Webanwendung nur ein paar Dutzend davon relevant sind. In diesem Fall möchte man die Datenmenge gleich von Anfang an mit einer Where-Clause beschränken. Mit ORM DSL geht dies zumindest in Grails 1.0.3 noch nicht, aber mit Annotations kriegt man es hin:
import javax.persistence.*
import org.hibernate.annotations.Where
@Entity
@Table(name="ATHLETE")
@Where(clause="""ATHLETEID
> 6""")
class AthleteAnnotated implements Serializable {
...
}
Und das schönste daran: Es klappt sogar mit den dreifachen Anführungszeichen, so dass man richtig umfangreiche WHERE-Clauses schreiben kann.
Achtung: Für diesen Fall benötigt man schon zwei Import-Statements.
Mit Annotation Datentyp CLOB erzwingen
Um für eine Spalte den Oracle-Datentyp CLOB zu erzwingen, versieht man die Spalte mit den folgenden Annotationen:
@Column(name="text", nullable=true, columnDefinition="clob")
String text
Es empfiehlt sich übrigens, die Spalte nullable zu machen, denn bei mehr als 4000 Zeichen lässt sich der Feldinhalt nicht mehr direkt mit einem INSERT einfügen. Es kommt dann zu dieser
Offene Fragen
Folgende Fragen konnte ich bis jetzt noch nicht beantworten:
- setzen Annotations die Validierung ausser Kraft?
- wie arbeiten ORM DSL und Annotations in der gleichen Klasse zusammen?
- wie arbeitet man mit @NamedNativeQuery?
Mapping bestehender Oracle-Datenbanktabellen mit XML
Soll Grails an eine bestehende Datenbank angebunden werden, die nicht den Grails-Konventionen entspricht, dann muss ein Mapping gemacht werden. Mit ORM DSL lässt sich noch nicht ganz alles realisieren. Und manchmal ist es auch von Vorteil, das Mapping nicht im Code, sondern in externen XML-Dateien zu haben. In diesen Fällen kommt ein Mapping mit Hibernate-XML-Dateien zum Zuge. Vor der Grails-Version 1 war dies ohnehin die einzige Variante, nicht-grails-konforme Datenbanken einzubinden.
Wenn man für existierende Datenbanken diese Mapping-Dateien automatisch erzeugen möchte, dann greift man zu den "Hibernate Tools for Eclipse and Ant", mit diesen Tools lässt sich Reverse Engineering von bestehenden Tabellen erzeugen. Für Grails erstellt man normalerweise pro Tabelle oder Abfrage eine Hibernate-Mapping-Datei. Die Hibernate-Mapping-Dateien (*.hbm.xml) kommen ins Verzeichnis \hibernate. Die XML-Datei ist ein Ersatz für eine Domain-Klasse, sie kann vom zugehörigen Controller direkt verwendet werden.
Registrierung in hibernate.cfg.xml
Die Mapping-Dateien werden ebenso wie annotierte Domain-Klassen in die Konfigurations-Datei eingetragen, aber nicht als Klassen sondern als Ressourcen. Die Konfigurationsdatei hibernate.cfg.xml muss auch bei Verwendung einer externen Datenbank keine Einträge für Treiber haben, dies übernehmen nach wie vor die Datasource-Dateien.
So sieht die Konfigurationsdatei hibernate.cfg.xml aus, wenn nicht grailskonforme Tabellen eingebunden werden:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration
PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<mapping resource="Country.hbm.xml"/>
</session-factory>
</hibernate-configuration>
Jede nicht konforme Tabelle erhält eine eigene XML-Mapping-Datei (siehe unten), deren Dateiname in die Konfigurationsdatei als resource eingetragen wird.
Das Hibernate-Mapping dient übrigens nicht nur dazu, bestehende Datenbank-Tabellen zu mappen, sondern kann auch beim Generieren von Tables dafür eingesetzt werden, Datentypen und Feldlängen präzise festzulegen, statt dies Grails zu überlassen.
Mapping des Primärschlüssels
Hier das XML-Mapping für eine Tabelle, bei welcher der Name des Schlüsselfeldes nicht mit der Grailskonvention, d.h. "id", übereinstimmt.
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="Country" table="hibernate">
<id name="id" column="mykey"
unsaved-value="null">
<generator class="native"></generator>
</id>
<version name="version" column="version"
type="java.lang.Long"/>
<property name="countryname" column="mytext"
/>
</class>
</hibernate-mapping>
Das optionale Untertag <generator bestimmt darüber, wie der Schlüssel generiert wird. Die folgenden Werte für class lassen sich im Zusammenhang mit Oracle-Datenbanken nutzen:
- assigned: Der Primärschlüssel wird nicht generiert, sondern muss manuell gesetzt werden, bevor die Methode save aufgerufen wird.
- foreign: Benutzt bei einer 1:1-Beziehung den Primärschlüssel der verbundenen Tabelle.
- hilo: Ein hi/lo-Algorithmus wird verwendet, um eindeutige Schlüssel zu erzeugen. Die erzeugten Werte sind nur innerhalb einer spezifischen Datenbank eindeutig.
- native: Verwendet identity (geht nicht für Oracle), sequence oder hilo, je nach den Fähigkeiten der zugrundeliegenden Datenbank
- select: liefert den eindeutigen Schlüssel, der mit einem Trigger erzeugt wird, wenn man den Datensatz mit einem eindeutigen Kriterium auswählt.
- sequence: Eine Sequence wird verwendet. Der Primärschlüssel kann vom Typ long, short oder int sein.
Eine vollständige Liste für das class-Property von generator findet sich auf www.roseindia.net.
Das nächste Beispiel ist eine Tabelle, die einen nichtnumerischen Primary Key aufweist, dessen Spaltenname in der DB ausserdem nicht id heisst. In der Domainklasse kommt das Property id gar nicht vor. Das XML-Mapping sieht folgendermassen aus:
...
<class name="Country" table="hibernate">
<id name="id" unsaved-value="null">
<column name="country_id"
sql-type="char(2)"
not-null="true">
</column>
<generator class="assigned"></generator>
</id>
...
</class>
</hibernate-mapping>
Interessant ist auch, dass ein Formularfeld für id mit "generator assigned" nicht automatisch in das Datenbankfeld gespeichert wird, sondern dass es noch eine zusätzliche explizite Zuweisung braucht:
def save =
{
def country = new Country()
flash.message = "Id in Formular: ${params.id}"
country.properties = params
country.id
= params.id
if(country.save()) {...
Jetzt noch ein Beispiel, wie man Grails zwingt, für den Primärschlüssel eine bestimmte Oracle-Sequence zu verwenden:
<class
name="Author" table="author">
<id name="id" column="id"
unsaved-value="null">
<generator class="native">
<param name="sequence">AUTHOR_SEQ</param>
</generator>
</id>
...
Read-Only-Tabelle
Soll eine Klasse nur lesbar, aber nicht änderbar sein, dann lässt sich dies in XML mit dem Attribut mutable der Klasse festlegen:
<class name="Employee" table="emp" mutable="false">
Mapping auf Oracle-BLOB
Auch das Mapping auf einen Oracle-BLOB habe ich bis jetzt nur mit Hibernate hingekriegt.
Felddefinition in Domain-Klasse:
byte[] imgflag
Constraints sind in der Domain-Klasse nicht notwendig.
XML-Mapping für das BLOB-Property
...
<class name="Country" table="hibernate">
...
<property name="imgflag">
<column name="bildfahne" sql-type="blob">
</column>
</property>
</class>
</hibernate-mapping>
Stored Procedure einbinden
Im folgenden zeige ich ein Beispiel, wie eine Stored Procedure aus einer Oracle-Datenbank eingebunden werden kann. Das Besondere daran ist, dass nicht einfach einzelne Werte zurückgeliefert werden, sondern ein ganzes Dataset. Die beschriebene Lösung ist nicht sehr groovy-like, aber leider die einzige, die ich bis jetzt zum Laufen gebracht habe. Die Stored Procedure wird als Dienst angeboten. Damit setzt sich die Lösung aus folgenden Elementen zusammen:
- Stored Procedure
- Methode in einem Service
- Closure in einem Controller
- GSP-Seite
Stored Procedure
Das einzig Spezielle an der Stored Procedure ist der Out-Parameter vom Typ sys_refcursor:
TEST.findAthletesLike(
res OUT sys_refcursor, str IN string)
AS
BEGIN
OPEN res FOR
SELECT
ath.athleteid,
ath.firstname,
ath.lastname,
ath.dateofbirth,
ath.stoppedtime,
ath.points
FROM athlete ath
WHERE ath.firstname LIKE str || '%'
OR ath.lastname LIKE str || '%';
END findAthletesLike;
/
Methode in einem Service
Der Service verwendet CallableStatement und castet dieses auf OracleCallableStatement, um auf den Cursor zugreifen zu können:
import groovy.sql.Sql
import java.sql.*;
class OrmService {
...
def getFromSqlStoredProc(String str) {
Sql sql = new Sql(dataSource)
CallableStatement stmt =
dataSource.connection.prepareCall(
"BEGIN findAthletesLike(?,?);
END;")
stmt.setString(2, str)
stmt.registerOutParameter(1, OracleTypes.CURSOR)
stmt.execute()
ResultSet rs =
((oracle.jdbc.driver.OracleCallableStatement)
stmt)
.getCursor(1)
return rs
}
}
Closure in einem Controller
Die Closure im Controller ist verglichen mit dem Service absolut unspektakulär:
class OrmController {
OrmService ormService
...
def listSqlCreateSQLQueryWithStoredProc = {
String str = "G"
if (str) {
def data = ormService.getFromSqlStoredProc(str)
return ['data': data]
}
}
}
GSP-Seite
Da wir ein ResultSet zurückerhalten, sieht die Ausgabe etwas anders aus, als gewohnt:
<html>
<body>
<div class="body">
<p>Flash ${flash.message}</p>
<table >
<%
while (data.next()) {
println(data.getString("firstname") +
" " +
data.getString("lastname") + "<br />" )
}
%>
</table>
</div>
</body>
</html>
Standardmappings von Grails- auf Oracle-Datentypen
Die folgende Tabelle enthält das Mapping der Datentypen zwischen Grails und Oracle XE (bezieht sich auf Grails 0.5.6 und Oracle XE):
Grails-Datentyp | Oracle-XE-Datentyp |
Boolean | Number(1) |
byte/Byte | Number(3) |
int/Integer | Number(10) |
long/Long | Number(19) |
float/Float | Float |
double/Double | Float |
String | Varchar2(255) |
byte[] mit maxSize:10000 | Long Raw |
Date | Timestamp(6) |
Hier ein paar Tricks, wie man Grails in den Domainklassen ohne Hibernate dazu bringt, in einer Oracle-Datenbank andere Datentypen zu mappen. Die Auswahl richtet sich vor allem an Datentypen aus, die bei uns in der Firma konkret im Einsatz sind. Mappings, die sich nur mit Hibernate realisieren lassen wie BLOBs oder Primary Key umbenennen, finden sich weiter oben.
Varchar2 > 255
String langtxt
..
static def constraints = {
langtxt(maxSize:1000)
}
Varchar2(1) als Pseudo-Boolean mit J/N
String mybool
static constraints = {
mybool(size:1..1, inList:["J", "N"])
}
Selfjoin (Autojoin)
In Oracle arbeitet man oft mit rekursiven Beziehungen, weil der SQL-Dialekt dies gut unterstützt. Damit lassen sich Baumstrukturen beliebiger Tiefe in einer einzigen Tabelle abbilden. Ein solcher Selfjoin lässt sich in Grails auch ohne Hibernate erzeugen oder abbilden.
class Keyword {
static hasMany = [keywords:Keyword]
Keyword parent
...
static constraints = {
parent(nullable:true)
// nötig für den Root
}
}
DB-Views in Grails
Bei der Arbeit mit existierenden Datenbanken gibt es Aufgabenstellungen, für die Grails auch in der Version 1.0.3 nur schlecht gerüstet ist. Eine davon ist die Arbeit mit Views. Domains sind defaultmässig mit einzelnen Tabellen verbunden und gehen davon aus, dass auf diesen Tabellen auch Update-, Insert- und Delete-Statements abgesetzt werden können. Aus den folgenden Gründen arbeitet man bei bestehenden Datenbanken oft mit Views:
- Reine Nachschlagelisten, die nicht geändert werden können
- Beschränkung auf gewisse Datensätze (z.B. nur solche mit Eigenschaft "aktiv")
- Komplexe Abfragen mit Daten aus vielen verbundenen Tabellen
- Optimierung der Abfrageperformanz mit Hints
In Grails habe ich bis anhin auf folgende Workarounds zurückgegriffen, um mit komplexen Queries zu arbeiten:
- Domains auf Views statt auf Tabellen aufsetzen und Controller-Closures und Views für update, insert und delete manuell beseitigen
- Service erstellen, der direkt SQL absetzt und Daten zurückliefert
Allgemeine Tipps und Tricks mit Datenbanken
Die folgenden Informationen gelten für Datenbanken generell.
Mindestanforderungen für bestehende Datenbanken (version)
Mit den verschiedenen Bordmitteln von Grails kann man inzwischen auch für bereits existierende Datenbanken recht gut Grails-Applikationen erstellen. Allerdings gibt es eine Mindestanforderung:
- Jede Tabelle, die via CRUD-Operationen aktualisiert werden soll, muss mindestens ein numerisches Feld für die Version aufweisen (dieses muss aber nicht zwingend "version" heissen). Falls es nicht möglich ist, die Tabelle mit einem Feld "version" zu ergänzen, muss wenigstens ein freies numerisches Feld vorhanden sein, dass dafür verwendet werden kann. Dieses Feld wird für das optimistic locking benötigt.
Für Tabellen, die nur lesbar sein müssen, gilt diese Einschränkung nicht. Hier ist es möglich, in ORM DSL version false zu setzen, also z.B.
class Athleteview {
static mapping = {
table 'ATHLETEVIEW'
version false
...
}
...
}
Einfüge- und Aktualisierungsdatum
Werden in einer Domain die folgenden zwei Felder erstellt, dann verwaltet Grails völlig selbständig Einfüge- und Aktualisierungsdatum eines Datensatzes:
- dateCreated
- lastUpdated
Mit folgendem Code lassen sich diese zwei Felder auf bestehende Datenfelder mappen:
class MyTable {
...
Date dateCreated
Date lastUpdated
static mapping = {
table 'DBTABLE'
columns {
...
dateCreated column:'INSERTDAT'
lastUpdated column:'MUTDAT'
}
}
Achtung:
Mit dieser Technik verwaltet Grails die Felder, es sollte deshalb in der Datenbank für diese Felder keine Insert- oder Update-Trigger geben.
Transient-Felder
Felder, die nicht auf ein Feld der Datenbanktabelle gemapped werden, lassen sich mit folgendem Code kennzeichnen:
class Basket {
...
BigDecimal price
Integer amount
BigDecimal getTotal() {
if (price && amount) {
return price * amount
}
}
static transients = ['total']
}
IDE für Grails einrichten
Die ideale Entwicklungsumgebung für Grails suche ich noch, im Moment probiere ich sowohl mit dem JDeveloper wie mit Eclipse und IntelliJ herum, aber so richtig überzeugt mich noch nichts. Vergessliche Leute wie ich haben sich an die Annehmlichkeiten der Code Completion gewöhnt und rümpfen über das Programmieren im Text-Editor die Nase. Groovy lässt sich problemlos in den JDeveloper integrieren, aber bei den GSP-Seiten hapert es noch.
IntelliJ
Ich habe mir sogar IntelliJ zugelegt, da es im Moment bezüglich Grails-Unterstützung, z.B. Code completion, einhellig als die beste IDE bezeichnet wird. In dieser Hinsicht habe ich sehr gute Erfahrungen gemacht. Allerdings lässt sich mit einem Notebook mit 1GB RAM und einer ziemlich vollen Festplatte kaum mit IntelliJ arbeiten: Der Start meines Grails Workspace benötigt ca. 10 Minuten und es kommt immer wieder zu Hängern, v.a. wenn ich noch andere Java Applikationen starte. Gelegentlich schnappt sich IntelliJ auch mehr als 90% des CPUs und dann legt es den ganzen Rechner lahm!
Achtung: Falls IntelliJ zum Debuggen von Grails-Applikationen verwendet werden soll, muss man beim Erstellen der Run- und Debug-Konfiguration unbedingt das Häkchen bei Make entfernen!
Für mehrsprachige Applikationen erweist sich die folgende Einstellung als äusserst nützlich: "File - Settings - IDE Settings - General - Properties Files: Transparent native-to-ascii conversion". Damit kann man auch Texte mit Umlauten in die Property-Files schreiben. Diese werden im Hintergrund automatisch umgewandelt. Damit entfällt die Notwendigkeit, die Texte mit der Methode "jä".encodeAsHTML() umzuwandeln.
Eclipse
Eine ausführliche englische Beschreibung für Eclipse findet man auf dem Grails-Website. Die vorliegende Beschreibung habe ich im November 2008 für die Grails-Version 1.0.3 und Eclipse 3.4.1 aktualisiert.
Vorgehen, um Eclipse für Grails einzurichten:
- Grails-Plugin in Exclipse installieren:
Menu http://dist.codehaus.org/groovy/distributions/update/
- im Repository und markieren - Schaltfläche
anklicken
Achtung: Falls Sie auch installieren möchten, benötigen Sie auch das TestNG Plugin von http://beust.com/eclipse. - Workspace für Grails eröffnen: Am besten sammelt man alle Grails-Projekte in einem eigenem Workspace, also - an geeigneter Stelle ein neues Verzeichnis erstellen - OK - OK
- Classpath GRAILS_HOME einrichten: - Verzeichnis eingeben, z.B. C:/Programme/programming/grails-1.0.3/grails
- Projekte
importieren: Grails erzeugt automatisch Eclipse Projekt- und
Classpath-Dateien, so dass sich bestehende Grails-Projekte ganz einfach
importieren lassen:
Achtung:- Wenn in älteren Versionen die letzterwähnte Einstellung vergessen wurde (merkt man daran, dass generate-all nicht mehr geht), dann kann man die Applikation retten, indem man alle Class-Files im Stammverzeichnis der Applikation löscht.
- Ein bestehendes Projekt kann nur importiert werden, wenn es im Stammverzeichnis die Datei .project aufweist. Ist diese z.B. bei einem heruntergeladenen Projekt (wie AuthorBook) nicht vorhanden, dann lässt sie sich aus einem anderen Projekt kopieren. Einzig dass Tag <name> muss man mit dem aktuellen Projektnamen anpassen.
- Verzeichnis des Projektes suchen - Einstellungen wie oder -
scheinen damit automatisch vorhanden zu sein. - GSP-Seiten eintragen: GSP-Seiten sind
nichts anderes als JSP-Seiten, die auf eine spezielle Taglib
zurückgreifen, deshalb kontrolliert man, ob die folgenden zwei
Einträge unter Window - Preferences vorhanden sind
- : *.gsp hinzufügen und JSP-Editor auswählen
- in Rubrik Text aufklappen - *.gsp hinzufügen
Auf einer anderen Webseite wird stattdessen vorgeschlagen, *.gsp für XML einzutragen. Habe ich nicht ausprobiert, aber macht evtl. auch Sinn.
- Beim Update von Projekten aus älteren Grails-Versionen den den Pfad für das Testverzeichnis in der Datei .classpath im Stammverzeichnis der Applikation kontrollieren und evtl. ändern. Vorgehen: Rechtsklick auf das Projekt - - Register - ..grails-tests anklicken - - test\integration und test\unit anklicken - OK - OK
Achtung: Wenn Sonderzeichen wie ä, ö, ü auf den gsp-Seiten nicht richtig angezeigt werden, dann liegt das möglicherweise am Encoding der Seiten. Am besten fährt man, wenn man alle Seiten im Defacto-Standard UTF-8 speichert.
Achtung: Grails-Website beschrieben.
soll nur angekreuzt sein, solange kein Debugging in Eclipse erfolgt. Will man dagegen Debuggen, dann muss man das Häkchen entfernen und einen spezielles Output-Verzeichnis einrichten wie auf demGrails-Applikation in Eclipse starten
Grails-Applikationen lassen sich direkt aus Eclipse starten. Falls man (z.B. wegen Oracle XE) die Grails-Applikationen unter einer anderen Port-Nummer als 8080 laufen lässt, muss man dies vor dem ersten Start noch anpassen: Dserver.port=9090. Anschliessend lässt sich die Applikation mit direkt starten.
und dort den Port ändern, z.B.Architektur einer Grails-Anwendung
Grails-Applikationen setzen das MVC-Konzept um
(Model - View - Controller)
Eine Grails-Applikation besteht u.a. aus folgenden wichtigen Teilen
- domains:
Klassen zur Kapselung der Daten
liegen normalerweise im Verzeichnis applikationsname\grails-app\domain - services:
Methoden für die Business-Logik
\grails-app\services - controllers:
steuern die Abläufe in der Applikation,
defaultmässig 1 Controller pro view
\grails-app\controllers - views:
die einzelnen Webseiten
\grails-app\views
Daneben können weitere Elemente vorkommen:
- command objects: Klassen zur
Validierung von Formularfeldern, die nicht direkt mit Attributen von
Domain-Klassen verbunden sind. Command objects können in
folgenden 2 Verzeichnissen stehen:
- src/groovy/
- grails-app/controllers/
Dies spiegelt sich auch in der Verzeichnisstruktur, die mit "grails create-app" angelegt wurde.
- Bootstrap-Klassen: Mit einer oder mehreren Bootstrap-Klassen im Verzeichnis %PROJECT_HOME%\grails-app\conf lassen sich Grails-Applikationen beim Start konfigurieren. Entspricht ungefähr demApplication_OnStart in der Datei global.asa einer ASP-Applikation. Allerdings sollte sie nicht wie global.asa zum Definieren von Konstanten verwendet werden, dafür gibt es Config.groovy (siehe unten)
Achtung: Bei der Bootstrap-Klasse besteht eine Namensverwirrung: Sie hiess früher ApplicationBootStrap.groovy und wurde dann in BootStrap.groovy umgetauft. Aufgrund dieser Namensunsicherheit sollten globale Konstanten für die Applikation nicht mehr in BootStrap.groovy definiert werden! In Version 1.0.4 wird zwar beim Scaffolding die Datei BootStrap.groovy erzeugt, aber Konstanten lassen sich nur aus ApplicationBootStrap.groovy auslesen! Fazit: Besser Config.groovy verwenden!
Eine weitere Möglichkeit für die generelle Konfiguration einer Grails-Applikation ist die Datei Config.groovy im Verzeichnis grails-app\conf\. Hier lassen sich Einstellungen und Konstanten mit derselben Syntax wie in Java-Property-Dateien festlegen:
vorspann.meinevar = "Mein Text"
Mit folgendem Code greift man anschliessend darauf zu:
x = grailsApplication.config.vorspann.meinevar
Verzeichnisstruktur einer Grails-Applikation
mybookmarks: Verzeichnis der Applikation
ab Version 0.6
- grails-app: hier stecken die
Elemente der Grails-Applikation
- conf: Konfigurationsdateien, u.a. für die Datenbankanbindung
- hibernate: eigene Hibernate-Mapping-Dateien (*.hbm) für die Einbindung bestehender Datenbanken, die nicht den Grails-Konventionen entsprechen
- spring:
- controllers: Alle Kontroller-Dateien der Applikation
- domain: Model-Klassen (entsprechen defaultmässig den Tabellen in der Datenbank)
- i18n: Die Property-Dateien mit den Übersetzungen
- services: Klassen für Dienste (Funktionen für die Business-Logik)
- taglib: eigene Taglibs
- utils: Verzeichnis für eigene Codec-Klassen (siehe http://grails.codehaus.org/Dynamic+Encoding+Methods)
- views:
- mydomainclass: pro Domainklasse gibt es normalerweise ein Unterverzeichnis, das die GSP-Seiten (Groovy Server Pages) zu dieser Domain enthält. Entsprechend den CRUD-Operationen können das create.gsp, edit.gsp, list.gsp und show.gsp sein (delete wird normalerweise von list.gsp aus aufgerufen)
- layouts: enthält Sitemesh-Layouts der Applikation, z.B. main.gsp
- lib: zusätzliche Java-Archive, z.B. für JDBC-Treiber oder weitere Packages
- scripts: anfänglich leer
- scr: projektspezifische Quelldateien
- test: Verzeichnis für Unit-Tests und andere Tests
- web-app:
- css: CSS-Dateien für die Applikation
- images: Bilddateien, z.B. Logos etc.
- js: Javascript-Dateien, z.B. für AJAX
- META-INF:
- WEB-INF:
- classes:
- tld:
Grails-Konventionen
Allgemeine Konventionen
- Der Name von Objekten wie Domains, Dienste, Controller steht immer in der Einzahl, der Plural ist ausschliesslich für Auflistungen vorgesehen
- Gross-Kleinschreibung
scheint über die verschiedenen Groovy-Versionen nicht
einheitlich zu sein. Es dürften aber die gleichen Regeln wie
bei Java gelten:
- Objekte fangen mit Grossbuchstaben an und haben gemischte Gross-Kleinschreibung, z.B. UserController oder SucheService
- Properties der Domain-Klassen werden klein geschrieben
- Verzeichnisnamen sind prinzipiell klein (Ausnahmen WEB-INF und META-INF)
Model/Datenbank (Persistenzschicht)
- Jede Tabelle hat die folgenden 2 Felder, die
automatisch erzeugt werden, auch ohne dass man sie in der Domainklasse
angibt
- id (Typ Long): eindeutiger Schlüssel
- version (Typ Long): Versionsnummer des Objekts
- Jede Tabelle muss ein Feld für die Versionsnummer haben (mit Hibernate kann man es umbenennen, aber es geht gemäss Graeme Rocher nicht ohne dieses Feld.
- Felder müssen defaultmässig ausgefüllt werden. Wenn ein Feld leergelassen werden darf, muss man dies in der Domainklasse angeben.
Business-Logik (Domain-Klassen und Services)
- Der Name einer Serviceklassen sollte aus dem
Namen der Service und der Endung Service bestehen, z.B.
SucheService
Webschicht (Controller und Views)
- Controller-Dateien enden mit Controller. Wenn sie
zu einem Domain-Object gehören, dann steht als erstes der Name
des Domain-Objekts. Das ergibt z.B.
UserController.groovy - Dateinamen von Code Snippets beginnen mit _ und enden mit .gsp, z.B. _formular.gsp
Grails-Befehle
grails create-app
Erzeugt das Gerüst für eine Applikation mit allen Verzeichnissen und Unterverzeichnissen
Eingaben:
- Name der Applikation
grails create-domain-class
Erzeugt das Gerüst einer Domain-Klasse.
Eingaben:
- Name der Domain-Klasse
grails generate-all
Erzeugt für eine bereits existierende Domain-Klasse mit vorhandenen Feldern, z.B. \domain\User.groovy, das folgende:
- Tabelle in DB mit dem gleichen Tabellennamen User
- Controller-Datei \controllers\UserController.groovy
- Unterverzeichnis \views\user\
- 4
GSP-Seiten im Verzeichnis \user\ für CRUD-Operationen
- create.gsp: neuen Datensatz einfügen
- edit.gsp: Datensatz editieren
- list.gsp: Liste aller Datensätze
- show.gsp: Anzeige eines einzelnen Datensatzes
grails create-controller
Erzeugt einen einzelnen Controller
Eingaben:
- Name des Controllers (in Kleinschrift)
grails generate-views
Erzeugt Views zu einer Domain-Klasse. Views ohne Domain-Klasse muss man manuell erzeugen.
Eingaben:
- Domain-Klasse
grails run-app
Startet die Applikation in einer Testumgebung mit dem Jetty-Applikationsserver. Wenn der normale Port 80 bereits belegt ist, kann mit folgendem Befehl ein neuer Port zugewiesen werden:
grails -Dserver.port=9090 run-app
grails test-app
Startet die Applikation im Testmodus.
grails create-integration-test
Erzeugt eine Testklasse für Integrationstests. Hiess in früheren Versionen vermutlich create-test-suite.
Eingaben:
- Name der neuen Testklasse
grails create-tag-lib
Erzeugt eine neue Tag-Library.
Eingaben
- Name der Tag-Library
grails console
Startet eine Grails-Konsole zum Testen einer kompletten Web Applikation (aber ohne den Jetty-Server).
grails bug-report
Erzeugt ein ZIP-File auf dem Root-Verzeichnis der Applikation mit allen GSP-, groovy- und Java-Dateien sowie gewissen XML-Dateien. Allerdings scheinen die Hibernate-Dateien nicht mitzugehen, die Datasources dagegen schon (Sicherheitsloch!). Zweck ist es, alle Dateien für einen Bug Report zusammenzuhaben.
Eingaben
- Keine
grails install-plugin yui
Installiert das Ajax-Plugin für Yahoo UI.
GSP-Syntax
Grails Server Pages (GSP) funktionieren ähnlich wie ASP oder JSP, d.h. normale HTML-Seiten werden via GSP-Tags mit dynamischen Elementen ergänzt, die in der Sprache Groovy geschrieben sind.
Die dynamischen Elemente können auf drei Arten in die Seite eingefügt werden
- ${..}: in ${} stehen
Ausdrücke, welche einen Wert ausgeben, d.h. Variablen,
Ausdrücke oder Methoden. Entspricht <%=...%> in ASP. Da Groovy Zugriff auf das Java-API hat und gewisse Pakete
automatisch importiert, sind solche Ausdrücke
möglich:
${new java.text.SimpleDateFormat("HH:mm").format(new Date())} - <% %>: Längere Codeabschnitte stehen wie bei ASP oder JSP in <%..%>. Allerdings ist es wenig sinnvoll, längeren Code tatsächlich in die GSP-Seiten zu legen. In der Architektur von Grails gehört Code je nach Zweck in Domain- und Service-Klassen oder Controller.
- Groovy-Tags: Groovy-Tags beginnen immer mit <g:... Für die wichtigsten Aufgaben wie Selektionen, Schleifen, Verarbeitung von Kollektionen existieren bereits Tags. Das Spezielle an Grails ist aber, dass sich eigene Tag-Libraries erstellen lassen, und zwar sehr viel einfacher als mit JSP.
Testen von Grails-Applikationen
Das Testen ist in das Grails-Framework bereits eingebaut. Jedesmal, wenn Sie mit einem Grails-Befehl ein Artefakt generieren, dann wird im Verzeichnis \test\integration\ auch eine Testklasse dazu erstellt. Diese Testklasse erhält den Namen des zugehörigen Artefakts, ergänzt mit Tests, also z.B. DesignTagLibTests.groovy.
Für die Unit-Tests greift Grails auf das bewährte JUnit zurück. Groovy bietet aber zusätzlich eine Klasse groovy.util.GroovyTestCase an, welche mit zusätzlichen praktischen assert-Methoden ergänzt.
Vorgehen:
- leere Testklasse mit gewünschten Testmethoden ergänzen
- Tests laufen lassen mit grails test-app
- HTML-Testberichte ansehen im Verzeichnis \test\reports\
Grails Snippets
In diesem Kapitel geht es nicht um Code-Snippets im Sinne von Grails (siehe unten), sondern um eine Zusammenstellung von gängigen Aufgaben und ihrer Umsetzung in GSP/Groovy.
Groovy allgemein
Weil es keine primitiven Typen gibt, sind Ausdrücke wie der folgende möglich:def produkt = 2
1.upto(7) { produkt *= it }
Regex in Groovy
Regex in Groovy bietet die gleichen Möglichkeiten wie in Java, ist aber um ein paar zusätzliche Goodies erweitert. Setzt man Regex allerdings direkt in GSP-Seiten ein, dass muss man beachten, dass bei gewisse Zeichen, z.B. beim $-Zeichen und bei Klammern, GSP und Regex sich gegenseitig in die Quere kommen können, so dass die Markierung gelegentlich zu akribischem Erbsenzählen ausartet.Unterschied zwischen Find- und Match-Operator
Groovy führt für den Vergleich mit Regex zwei neue Operatoren ein:
- ==~ entspricht match, d.h. überprüft, ob das Pattern dem gesamten String entspricht; liefert true oder false zurück
- =~ entspricht find, d.h. sucht nach einmaligem Auftreten des Patterns innerhalb des zu durchsuchenden Strings; liefert ein Matcher-Objekt zurück, kann aber wie ein Boolean verwendet werden, d.h. der folgende Ausdruck ist gültig
assert "text" =~ /ex/
Der pattern-Operator ~String
Groovy führt für Regex zur Verbesserung einen weiteren Operator ~String ein. Mit diesem Operator lässt sich ein String in ein Objekt vom Typ java.util.regex.Pattern umwandeln. Patterns werden vorkompiliert, was bei wiederholter Anwendung zu einer besseren Performance führt. Man beachte, dass zwischen = und ~ zwingend ein Leerschlag stehen muss, um den Operator vom find-Operator =~ zu unterscheiden!
Das folgende Code-Beispiel zeigt die Performance-Vorteile des Pattern-Operators
cannonFilename = "IMG_1516.JPG"
cannonRegex = /(?i)img_\d\d\d\d\.jpg/
startZeit = System.currentTimeMillis()
25000.times{
cannonFilename =~ cannonRegex
}
println "Variante ohne Pattern: " +
(System.currentTimeMillis() - startZeit)
//**********************************************
//Diese Variante ist mehr als doppelt so schnell
startZeit = System.currentTimeMillis()
cannonPattern = ~cannonRegex
25000.times {
cannonPattern.matcher(cannonFilename)
}
println "Variante mit Pattern: " +
(System.currentTimeMillis() - startZeit)
Regex-Operatoren
Operator | Bedeutung | Beispiel |
==~ | Pattern matching | "groovy.gsp" ==~ /g.*/ |
/ | begrenzt das Pattern | "groovy.gsp" ==~ /groovy\.gsp/ |
\ | maskiert das nächste Zeichen | "groovy.gsp" ==~ /groovy\.gsp/ |
a | genau 1 Vorkommen | "mymail@xy.ch" ==~ /.+@.+\...?.?/ |
a? | 0 bis 1 Vorkommen | Länderendung
hat 2 bis 4 Buchstaben "mymail@xy.ch" ==~ /.+@.+\....?.?/ |
a* | 0 bis n Vorkommen | String
fängt mit _ an und ist beliebig lang "_inc" ==~ /\_[a-zA-Z]*/ |
a+ | 1 bis n Vorkommen | String
besteht nur aus Kleinbuchstaben und enthält mindestens ein
Zeichen "gugus" ==~ /[a-z]+/ |
. | beliebiges Zeichen ausser Zeilenumbruch \n oder \r\n | String besteht aus 3 beliebigen Zeichen "x y" ==~ /.../ |
.* | beliebiges
Zeichen kommt 0 bis n mal vor | String beginnt mit a und
enthält 0 bis n weitere Zeichen Achtung: greedy, siehe unten! "a25x" ==~ /a.*/ |
[abc] | irgendeines dieser Zeichen | String
ist "0", "5" oder "9" "0" ==~ /[059]/ |
a|b | das eine oder das andere Zeichen | String
endet mit ".li" oder ".ch" "mymail@xy.li" ==~ /.+(\.li|\.ch)/ |
(aa) | Gruppe von Zeichen | Findet Haar und Heer "Haar" ==~ /H((aa)|(ee))r/ |
[oO]{2} | Anzahl Vorkommen (muss nicht hintereinander sein) | Wahr
für alle Strings, in denen mindestens einmal zwei Selbstlaute
hintereinander vorkommen "Eier" ==~ /.*[aeiou]{2}.*/ |
[a-z] | irgendein Zeichen aus der Folge | String
mit genau 2 Buchstaben "ch" ==~ /[a-zA-Z][a-zA-Z]/ |
[^01] | irgenein Zeichen ausser den aufgezählten | String der
keine geraden Ziffern enthält "a 1553" ==~ /[^02468]+/ |
^a | Zeile beginnt mit diesem Zeichen | Zeile beginnt mit einer
Ziffer "1. Punkt" ==~ /^[1-9].*/ |
a$ | Zeile endet mit diesem Zeichen | Zeile endet mit ">" "<br />" ==~ /.*>$/ |
Spezielle Zeichen
wie in Java, die vollständige Liste ist hier\n | Zeilenumbruch unter Unix (Linefeed) |
\r\n | Zeilenumbruch unter Windows (Carriage Return und Linefeed) |
\s | Whitespace-Zeichen, d.h. [\t\n\x0B\f\r] |
\S | kein Whitespace-Zeichen |
\t | Tabulator |
\d | eine Zahlenziffer, d.h. [0-9] |
\D | keine Zahlenziffer, d.h. [^0-9] |
\w | ein Wortzeichen, d.h. [a-zA-Z_0-9] |
\W | kein Wortzeichen |
\b | ein Wortende |
\B | kein Wortende |
Flags
Flags gelten von ihrem Auftreten bis ans Ende oder bis sie aufgehoben werde.(?s) | . schliesst auch spezielle Zeichen wie den Zeilenumbruch ein |
(?-s) | hebt (?s) wieder auf |
(?i) | Gross- und Kleinschreibung spielt keine Rolle |
(?-i) | hebt (?i) wieder auf |
(?x) | ignoriert unmaskierten Whitspace und Kommentare |
/aa(?si).*(?-si)\n/ | Mehrere Flags gleichzeitig einstellen und wieder aufheben |
/ab(?i:cde)fg/ | Flag nur für einen Teil des Textes einstellen |
Zu maskierende Zeichen
- /
- \
- ?
- *
- +
- .
- |
- ~
- $
- [ und ]
- ( und )
- { und }
Beispiele zum Parsen von GSP/Groovy
Die Beispiele gehen davon aus, dass der Suchstring htmlencoded ist. Der gefundene String wird in der nicht encodeten Schreibweise angezeigt, d.h. so, wie er auf einer Webseite angezeigt wird.Pattern | gefundene Strings |
/(?s)<%.*?%>/ | <%intcounter++%> |
/(?s)<g:.*?>/ | <g:def var="name" value="${'Silvia'}" /> |
/(?i)img_\d\d\d\d\.jpg/ | IMG_1516.JPG |
/(?i)p(\d){7}\.jpg/ | p1000314.jpg |
Beispiele von Befehlen:
- mytext
= mytext.replaceAll("\t",
{Object[] it -> " "})
Ersetzt jeden Tabulator mit 2 HTML-Leerschlägen
Greedy und reluctant Qualifiers
Um GSP-Seiten mit verschiedenen Arten von Tags und Ausdrücken zu parsen, muss man den Unterschied zwischen "greedy quantifiers" wie .* und "reluctant qualifiers" wie .*? kennen. Am folgenden Befehl lässt sich dieser Unterschied gut erläutern.Richtige Variante, d.h. reluctant:
mytext = mytext.replaceAll(/(?s)<%.*?%>/,
{"<span class=\"groovyexpr2\">\$0</span>"})
Findet erstes Anfangs- und Schluss-Tag und umhüllt sie mit <span>-Tags, findet zweites Anfangs- und Schlusstag etc.
Falsche Variante, d.h. greedy:
mytext = mytext.replaceAll(/(?s)<%.*%>/,
{"<span class=\"groovyexpr2\">\$0</span>"})
Findet erstes Anfangs- und letztes Schluss-Tag und umhüllt sie mit <span>-Tags. Alle Anfangs- und Schluss-Tags dazwischen werden ignoriert.
Metainformationen zur Applikation
Ausgangslage: Der URL für die aktuelle Seite isthttp://localhost:9090/beispiele/versuche/renderpage/snippets
Das heisst:
- Applikation: beispiele
- Controller: versuche
- Action: renderpage
- View oder GSP-Seite: snippets
Applikationsname
metadata.get('app.name')Resultat: beispiele
Controllername
grails.util.GrailsWebUtil.getControllerFromRequest(request).controllerNameResultat: versuche
Actionname
request.getAttribute('org.codehaus.groovy.grails.ACTION_NAME_ATTRIBUTE')Resultat: renderpage
Domainklassen
Snippets zum Mapping von bestimmten Datentypen findet man im Abschnitt Mapping.
Feld ist nicht persistent, d.h. wird nicht in DB gespeichert
static transients = ["feldname1", "feldname2"]
Feld kann leergelassen werden
static constraints = {
birthday(nullable:
true)
}
Feld kann nur Klein- oder Grossbuchstaben oder Leerschläge enthalten
static constraints = {
name(matches:"[A-Za-z
]+")
}
Validierung einer eindeutigen Emailadresse (kann sogar gültige wie .info von nichtgültigen wie .x Endungen entscheiden)
String email
static constraints = {
email(email:true, unique:true)
}
Controllers
Redirect auf Root-Verzeichnis:
redirect(uri:"/")
Views
Seite mit Links auf alle Domain-Klassen
<g:each var="c" in="${grailsApplication.domainClasses}">
<li class="controller">
<a href="${c.logicalPropertyName}">${c.fullName}</a>
</li>
</g:each>
Flash-Message anzeigen
<g:if
test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
Umwandeln eines beliebigen Strings in HTML
encodeAsHTML()
Services
Services haben einen Scope, der als static-Eigenschaft festgelegt wird, z.B.static scope = "singleton"
Es gibt die folgenden Werte für scope
- singleton: einmal pro Applikation
- session: einmal pro Session
- conversation: einmal pro Conversation (siehe spring web flow)
- flow: einmal pro Flow (siehe spring web flow)
- flash: für einen Request und den nächsten
- request: einmal pro Request
- prototype: es wird jedes Mal ein neuer Service erzeugt, wenn er in eine neue Klasse eingespritzt (injected) wird
static class StringService {
static def leftToFirst = {
mytext, mychar ->
if (mytext != null && mytext.indexOf(mychar) > -1) {
return mytext.substring(0, mytext.indexOf(mychar))
}
return mytext
}
}
Services scheinen nicht dynamisch geladen zu werden. Es braucht normalerweise einen Neustart der Applikation, um Änderungen einzulesen.
Grails Tipps und Tricks
Konfiguration
Dauer der Http-Session einstellen
Die Dauer einer http-Session stellt man normalerweise in web.xml ein. Leider findet man diese Datei in einer Grails-Applikation nicht wie gewohnt im Verzeichnis WEB-INF. Mit folgendem Trick kann man trotzdem Einfluss auf die Dauer nehmen:
- auf der Kommandozeile ins Verzeichnis der gewünschten Applikation wechseln und mit grails install-templates die Templates nachinstallieren
- im Verzeichnis \src\templates\war web.xml mit folgendem Eintrag ergänzen:
<session-config>
<session-timeout>15</session-timeout>
</session-config>
Die Zahl steht natürlich für die Session-Dauer in Minuten.
Domains
Nachträgliche Änderungen an Domainklassen
Achtung: Wenn Sie nachträglich ein Feld aus einer Klasse entfernen, so wird das Feld zumindest bei Oracle XE in der Datenbank nicht automatisch ebenfalls entfernt. Da defaultmässig die Felder nicht null sein dürfen, scheitert der nächste Versuch, einen Datensatz einzufügen, weil das Feld nicht mehr im Create-Formular enthalten ist.
Lösung: das Feld manuell in der Datenbank löschen
Views
Premature end of file error umgehen
Die folgende Fehlermeldung erscheint nur auf der Konsole und anscheinend nur mit gewissen Browsern, z.B. Firefox ab Version 3.
[Fatal Error] :-1:-1: Premature end of file.
Der Bug sollte in Grails 1.4 behoben werden. Um den Fehler zu umgehen, kann man in der Zwischenzeit in config.groovy folgende Zeile auskommentieren:
//xml: ['text/xml', 'application/xml'],
Quelle: http://jira.codehaus.org/browse/GRAILS-3088
Templates
Mit Sitemesh verfügt Grails über einen praktischen Template-Mechanismus. Um z.B. mehrere GSP-Seiten mit derselben Struktur (Seitenlayout, Navigation, Logo etc.) zu versehen, benötigt man folgende Elemente (werden beim Scaffolding automatisch erzeugt):
- \grails-app\views\layouts\main.gsp ist die Template-Datei, die alle gemeinsamen Elemente enthält
Tags, die mit <g:.. starten, kennzeichnen als Platzhalter jene Stellen, die durch die eigentlichen Seiteninhalte ersetzt werden - Die GSP-Seiten enthalten im Header folgendes Tag, das auf das
verwendete Template hinweist
<meta name="layout" content="main" />
Codebeispiel für die Template-Datei
<html>
<head>
<title>
<g:layoutTitle default="Grails"
/>
</title>
<link rel="stylesheet"
href="${createLinkTo(dir:'css',file:'main.css')}"></link>
<g:layoutHead
/>
<g:javascript library="application" />
</head>
<body>
<div id="spinner" class="spinner"
style="display:none;">
<img src="${createLinkTo(dir:'images',file:'spinner.gif')}"
alt="Spinner" />
</div>
<div class="logo">
<img src="${createLinkTo(dir:'images',file:'grails_logo.jpg')}"
alt="Grails" />
</div>
<g:layoutBody />
</body>
</html>
Fallstricke bei der Benennung von Templates und Controllern
Durch eigene schmerzlicher Erfahrung bin ich zu der folgenden Einsicht gelangt: Im Zusammenhang mit AJAX sollte man ein Template nicht gleich benennen wie einen Kontroller, also z.B. BeispielController.groovy und layouts\beispiel.gsp. Sonst wird das Template nämlich auch bei einem AJAX-Request geladen, der nur einen Teil der Seite aktualisiert, so dass die Template-Elemente zweimal auf der Seite vorkommen (siehe Screenshot).
Wenn man trotzdem wissen möchte, zu welchem Controller ein Layout gehört, kann man es ja stattdessen \layouts\beispiellayout.gsp nennen.
Wiederverwendung von Code Snippets
Kleine wiederverwendbare statische Code-Snippets erstellt man folgendermassen
- Erzeugen Sie eine Datei _meinsnippet.gsp im
Verzeichnis views für ein Snippet, das allen Views zur
Verfügung stehen soll, z.B.
\grails-app\views\_nachspann.gsp - Geben Sie in der
Datei den statischen oder dynamischen HTML-Code ein, der
wiederverwendet werden soll, z.B.
Zurück zu
<a href="http://www.ecotronics.ch/">Ecotronics</a> - Fügen Sie auf jeder Seite, welche dieses Snippet
verwendet, folgendes Tag ein:
<g:render template="/nachspann" />
Der Schrägstrich in .."/nachspann steht übrigens dafür, dass die Datei im Verzeichnis /views gespeichert ist. Lässt man / weg, dann wird immer in jenem Unterverzeichnis von \views\ gesucht, dessen Name dem zugehörigen Controller entspricht.
Lösch-Links in Liste einfügen
Wenn Controller und die zugehörigen Views mit Scaffolding erzeugt wurden, dann werden im Controller normalerweise Aktionen, die Daten verändern, auf mit allowedMethods auf POST beschränkt.
Damit ist es nicht möglich, auf einer Liste einen Hyperlink zum Löschen einzufügen. Abhilfe schafft man, indem man das delete:'POST' entfernt, d.h.
def allowedMethods = [save:'POST', update:'POST']
Fremdschlüssel in Kombinationsfeldern
In den Create- oder Edit-Seiten werden Fremdschlüssel normalerweise als Kombinationsfelder angezeigt. Allerdings wird die defaultmässig das Attribut id angezeigt, was wenig sinnvoll ist. Mit dem Attribut optionValue lässt sich ein anderes Attribut verwenden, z.B.:
optionValue="name"
Quelle:
http://www.nejug.org/2007/include/GetGroovierWithGrails.pdf Folie 24
Als Alternative zu diesem Vorgehen versieht man den Controller mit einer toString-Methode, welche die Informationen für die Anzeige liefert.
In Grails einen Session-Counter programmieren
Es gab ein Zeit lang ein Plugin, das diverse statistische Auswertungen macht, unter anderem auch Sessions zählt. Der zuständige Programmierer musste es leider vom Netz nehmen. Mit der im Folgenden beschriebenen Vorgehensweise lässt sich aber ein einfacher Session- oder Hit-Counter ab der Grails-Version 1.0.2 selbst erstellen. Man kann sowohl die Gesamtzahl der Sessions wie die Zahl der im Moment aktiven Sessions zählen. Dazu habe ich folgende Grails-Elemente verwendet:- Einen Application-Service (scope singleton) mit statischen Variablen für die Anzahl der laufenden und der aktiven Sessions.
- Einen Session-Service (scope session), der im Constructor die laufenden und die aktiven Sessions um 1 erhöht und in der Methode finalize() die aktiven Sessions wieder um 1 erniedrigt.
- Eine GSP-Seite, die die zwei Variablen für die Sessions anzeigt
- Das Hochzählen von neuen Sessions klappt wunderbar. Da sich das Runterzählen der Sessions auf finalize() stützt, und die Garbage Collection unter Java sich bekanntlich nicht erzwingen lässt, kann es ziemlich lange dauern, bis eine Session effektiv wieder verschwindet.
- Von ASP weiss ich noch, dass man Variablen auf Applikationsebene sehr spärlich verwenden soll, weil man damit einen potentiellen Performance-Engpass schafft. Ob die hier beschriebene Technik bei Grails tatsächlich ohne Performanceeinbussen im Produktivbetrieb verwendet werden kann, entzieht sich meiner Kenntnis!
ApplicationService.groovy
Diese Datei muss im Verzeichnis \grails-app\services\ unterhalb des Applikationsverzeichnisses liegen.class ApplicationService {
static scope = "singleton"
static transactional = false
static activeSessions = 0L
static allSessions = 0L
}
SessionService.groovy
Diese Datei muss im Verzeichnis \grails-app\services\ unterhalb des Applikationsverzeichnisses liegen.class SessionService {
static scope = "session";
static transactional = false
//Constructor
public SessionService() {
ApplicationService.allSessions
= ApplicationService.allSessions + 1;
ApplicationService.activeSessions
= ApplicationService.activeSessions + 1;
}
protected void finalize() {
if (ApplicationService.activeSessions > 0) {
ApplicationService.activeSessions
= ApplicationService.activeSessions - 1;
}
}
}
Die GSP-Seite sessioncounter.gsp
Diese Datei liegt in einem zum Controller gehörigen Unterverzeichnis von \grails-app\views\.<%@ taglib prefix="g"
uri="http://grails.codehaus.org/tags" %>
<html>
<head>
<title>Session-Counter in Grails</title>
</head>
<body>
<div class="body">
<h1>Session-Counter in Grails</h1>
<p>Anzahl Sessions insgesamt:
${ApplicationService.allSessions}</p>
<p>Anzahl aktive Sessions:
${ApplicationService.activeSessions}</p>
</div>
</body>
</html>
AJAX nach Projekt-Updates zum Laufen bringen
In einem alten Projekt, das ich auf Version 1.0.3 aktualisiert habe, gelang es mir nicht, die allereinfachsten AJAX-Beispiele zum Laufen zu bringen. Mit Trial und Error fand ich heraus, dass das Problem verschwindet, wenn ich die Tag-Libraries UITagLib.groovy und JavascriptTagLib.groovy aus dem Verzeichnis \taglib\ entferne. Diese werden beim Erzeugen einer neuen Applikation in Version 1.0.3 nicht mehr erzeugt. Bis jetzt ist mir nichts aufgefallen, was nicht mehr laufen würde.
Weder mit einer Suche auf grails.org noch via Google fand ich heraus, wozu diese Tag-Library genau verwendet wird und ob sie in neueren Versionen noch nötig ist.
Passwortgeschützter Website mit Acegi-Plugin
Im folgenden finden Sie das Vorgehen, um das Acegi-Security-Plugin zu installieren (im Oktober 2008 ist die aktuelle Version 0.3) und einen Website in einen passwortgeschützten Website umzuwandeln. Die Anleitung folgt dem ausführlichen Tutorial auf http://www.infoq.com/articles/grails-acegi-integration.
Acegi heisst übrigens neu "Spring security" und unter diesem Titel findet man auch die notwendige Dokumentation, z.B. unter http://static.springframework.org/spring-security/site/reference/pdf/springsecurity.pdf. Das Plugin heisst aber (zumindest in der Grails-Version 1.0.3) nach wie vor acegi.
Meine Anleitung geht davon aus, dass bereits eine funktionierende Grails-Applikation besteht, nämlich eine kleine Buchverwaltung mit den 2 Domainklassen Author und Book. Zwischen Author und Book besteht eine 1:n Beziehung, wobei Author die Mastertabelle ist. Die zweite wichtige Voraussetzung ist, dass der User, mit dem in der Datasource die Verbindung zur Datenbank erfolgt, genügend Rechte hat, damit neue Tabellen erstellt werden können, denn für die Absicherung des Sites werden neue Tabellen für Benutzer und Rollen erstellt. In der Datasource-Datei muss ausserdem dbCreate = "create" stehen. Aber Achtung, dies darf keinesfalls bei bereits existierenden Datenbanken mit vorhandenen Daten angewandt werden, weil dieser Befehl alle Daten löscht! Sollen bestehende Websites mit vorhandenen Daten gesichert werden, dann müssen die für Acegi notwendigen Tabellen von Hand erstellt werden!
- Kommandozeile öffnen und ins Stammverzeichnis der Applikation wechseln
- In der Kommandozeile das Acegi-Plugin installieren mit dem Befehl
grails install-plugin acegi - Als nächstes erzeugen wir die Benutzer und Rollen. Die Domains (und
Tabellen dazu) nenne ich "AuthUser" und "Role", man
kann sie aber auch anders nennen.
grails create-auth-domains AuthUser Role
Dieser Befehl erzeugt drei Domainklassen, nämlich die zwei genannten sowie "Requestmap.groovy". Ausserdem wird eine neue Konfigurationsdatei namens "SecurityConfig.groovy" ins conf-Verzeichnis eingefügt. - Mit dem nächsten Befehl erzeugen wir die Controller und Views, damit
wir Benutzer und Rollen erfassen können. Auch für Login und Logout
werden Controller und Views erstellt.
grails generate-manager - Ein weiterer Befehl erstellt alle notwendigen Dateien für die
Registrierung.
grails generate-registration - Nun starten Sie die Applikation, denn das Erfassen von Benutzern, Rollen und Zugriffsregeln erfolgt dort. Erscheint statt der Startseite nur eine leere Seite ohne irgendwelche Fehlermeldungen, dann heisst das wahrscheinlich, dass die notwendigen Klassen nicht erstellt werden können, weil entweder der Benutzer für die Datenbankverbindung nicht genügend Rechte hat oder weil dbCreate nicht auf create gesetzt ist. Versichern Sie sich, dass Sie auf die Controller für User und Role zugreifen können, aber warten Sie noch mit der Dateneingabe. Nach diesem Schritt sollten die notwendigen Tabellen in der Datenbank vorhanden sein.
- Wenn Sie nicht bei jedem Start wieder Rollen, Benutzer und andere Daten
eingeben wollen, dann empfiehlt es sich, die Applikation zu stoppen, dbCreate
= "update" zu setzen oder die Zeile ganz
auszukommentieren, damit die Daten erhalten bleiben. Defaultmässig werden
Sie gezwungen, alle Felder einer Domainklasse einzugeben. Sichergestellt
wird dies nicht nur in den Domainklassen, sondern auch auf der Datenbank
mit Check-Constraints. Dies ändern Sie nun, indem Sie in den
Domainklassen im Block constraints für die gewünschten Felder nullable:
true eingeben, z.B.
description(nullable: true)
Allerdings müssen Sie dann auch auf der Datenbank die Check-Constraints für diese Felder löschen. - Eine andere Variante, den Benutzerkomfort zu erhöhen, ist es, Defaultwerte
für nicht ausgefüllte Werte zu setzen. Dazu bearbeitet man die
save-Method im Controller. Mit folgendem Code setzt man beispielsweise die
Beschreibung gleich dem Wert im Feld authority:
if (!authority.description) {
authority.description = params.authority
} - Sollen neue Benutzer standardmässig aktiviert sein, dann empfiehlt es
sich, bereits in der Domainklasse AuthUser.groovy den Defaultwert zu
setzen:
boolean enabled = true - Starten Sie die Applikation nun wieder. Auf der Einstiegsseite wechseln Sie in den RoleController. Es sollen zwei Rollen erstellt werden, "admin" für Benutzer/innen, die Editieren dürfen und "user" für solche, welche die Daten nur ansehen dürfen. Fügen Sie also diese zwei Rollen ein.
- Dann gehen Sie auf den UserController und geben mindestens zwei Benutzer ein, einem mit Rolle admin und user und einen nur mit Rolle user. Wenn sie die Ratschläge aus Schritt 7 und 8 angewendet haben, dann sollten nun das Feld Description leer lassen können, ohne eine hässliche Fehlermeldung zu erhalten.
- Um die Regeln für den Zugriff festzulegen, öffnen Sie den
RequestmapController. Die Eingaben hängen von ihren Domains und Views ab.
Um der admin-Rolle beispielsweise alle Rechte für Author zu geben, machen
Sie folgende Eingaben:
URL: /author/**
Role: admin
User sollen dagegen nur auf die Autorenliste und die Detailansicht Zugriff haben, aber weder Datensätze einfügen noch editieren dürfen. Das ergibt:
URL: /author/list/**
Role: user, admin
Auch für die index- und die show-Seite muss eine solche Regel erstellt werden.
Gleich verfahren Sie mit allen Seiten, bei denen Sie den Zugriff beschränken möchten. - Klicken Sie nun auf der Startseite aus AuthorController. Sie werden zu einer Login-Seite umgeleitet. Geben Sie die Logindaten für den gewöhnlichen Benutzer ein. Mit korrekten Eingaben werden Sie zur Seite mit der Liste weitergeleitet. Versuchen Sie jedoch, einen Datensatz zu erzeugen oder zu ändern, dann erhalten Sie eine hässliche Fehlerseite.
- Besser wäre es natürlich, die Links dazu je nach Benutzerrechten ein-
und auszublenden. Genau für diesen Zweck gibt es nun Tags, die man in der
Grails-Acegi-Dokumentation unter dem seltsamen Titel Artifacts
findet. Um Beispielsweise den Create-Link auf der Author-Seite nur für
Administratoren einzublenden, öffnet man im View-Verzeichnis die
entsprechende Seite und umgibt den Link mit dem ifAllGranted-Tag:
<g:ifAllGranted role="ROLE_ADMIN">
<g:link class="create" action="create">
New Author
</g:link>
</g:ifAllGranted> - Falls nun jemand trotz allem noch auf "Access denied" landet, sollte die Seite nicht einfach die spröde Fehlerseite des Webservers sein, sondern eine normale, vom Design her an die übrigen Seiten angepasste Seite. Die Sache mit der verschönerten "Access denied"-Seite lässt sich leider zumindest im Oktober 2008 mit der Plugin-Version 0.3 aufgrund eines Bugs nicht realisieren.
Allgemeine Hinweise:
Das Standardverfahren führt dazu, dass die Zugriffsregeln via Requestmap in der Datenbank gespeichert werden. Das ist sehr flexibel und eignet sich für Websites, wo häufig Änderungen an Benutzern und Rechten vorkommen, z.B. bei Foren, wo sich die Anwender selbst anmelden können. Es führt aber auch dazu, dass für jeden Seitenzugriff ein Datenbankzugriff erfolgt. Wenn der damit einhergehende Performanceverlust nicht akzeptabel ist, kann man stattdessen mit weniger flexiblen statischen Zugriffsregeln in der Datei SecurityConfig.groovy arbeiten.
Die Checkbox "Remember me" auf der Loginseite setzt ein Cookie. Damit können Sie den Browser schliessen und trotzdem beim nächsten Mal eine Seite ohne Login aufrufen. Wenn Sie ausloggen, wird das Cookie allerdings wieder gelöscht.
Internationalisierung (i18n)
Übersetzung mit Property Files
Die übliche Art, Websites mit Grails zu übersetzen, besteht aus Property files. Für jede gewünschte Sprache muss im Verzeichnis grails-app\i18n ein Property file mit dem Namen messages_xx.properties liegen, in dem die Sprachstrings als Properties abgelegt sind. Im Dateinamen steht xx für die Sprache und evtl. das Land, also z.B. messages_de.properties oder messages_pt_BR.properties.
Verwendung in GSP-Seiten mit message-Tag:
<g:message code="my.message.code" />
Verwendung in Groovy-Code von Controllern (aber nicht von Services!):
flash['message']='${message(code:'my.message.code')}'
Normalerweise wird die Sprache automatisch an die Einstellungen des Browsers angepasst. Man kann aber auch Links anbieten, mit denen die Benutzerinnen die Sprache wechseln können. Ein Sprachwechsel benötigt folgende Zutaten:
- Links, um die Sprache zu wechseln
- Einen HomeController, in dessen View-Verzeichnis die Seite index.gsp vom Verzeichnis web-app verschoben wird
- Ein mapping für die Startseite in grails-app/conf/urlMappings.groovy
Die letzten zwei Massnahmen sind nötig, um den Sprachwechsel auch mit abgesicherten Websites (z.B. mit Acegi) betreiben zu können.
Die Links sehen z.B. folgendermassen aus:
<g:link controller="${params.controller}"
action="${params.action}"
params="${params + [lang:'de']}">D </g:link>|
<g:link controller="${params.controller}"
action="${params.action}"
params="${params + [lang:'en']}">E </g:link>|
Das ${params + [lang:'en']} ist nötig, damit die bestehenden Parameter, z.B. für die Seite show nicht einfach ersetzt, sondern mit dem lang-Parameter ergänzt werden.
Ist der HomeController mit grails create-controller Home erzeugt und index.gsp vom Verzeichnis web-app in das View-Verzeichnis dieses Controllers verschoben, dann braucht es für die Startseite noch folgendes Mapping in grails-app/conf/urlMappings.groovy (zumindest bei passwortgeschützten Applikationen im Zusammenhang mit dem Acegi-Plugin):
static_mappings = {
/ {
controller = 'home'
view = 'index'
}
...
}
Umlaute und Sonderzeichen
Eine ständige Quelle für Ärger ist bei mehrsprachigen Applikationen auch der Zeichensatz. Damit Umlaute und Sonderzeichen korrekt dargestellt werden, bietet Grails eine Reihe von Codec-Klassen an, mit denen Text nicht nur in die korrekte Form für HTML, sondern auch für URLs oder für JavaScript (AJAX mit JSON) gebracht werden kann. Die entsprechenden Methoden lauten:
"bäh".encodeAsHTML()"bäh".encodeAsJavaScript()
"bäh".encodeAsURL()
Ausserdem gibt es die entsprechenden Methoden zum Dekodieren, also z.B.
"bäh".decodeHTML()
Beachten Sie zu diesem Thema auch das Kapitel "IDE für Grails einrichten"Groovy
Die Programmiersprache in Rail ist Groovy.Groovy-Features
- Groovy läuft in der Java VM
- Groovy nutzt die Bibliotheken des JDK
- Groovy erweitert das JDK
- Groovy kann Java-Klassen aufrufen und aus Java aufgerufen werden
- Groovy kann zu Java Classfiles kompiliert werden
- Leichte, risikolose Migration von Java nach Groovy
- Automatisch importierte Pakete:
- groovy.lang.*
- groovy.util.*
- java.lang.*
- java.util.*
- java.net.*
- java.io.*
- java.math.BigInteger
- java.math.BigDecimal
Unterschiede zwischen Groovy und Java
Hier eine Liste der wichtigsten Syntaxregeln von Groovy, die anders als in Java sind:
- Semikolon (meistens) optional
- Optionale Klammern bei Methodenaufrufen
- Typangaben sind optional
- Deklaration von Exceptions sind optional
- public ist default
- in Groovy gibt es nur Objekte, keine primitiven Typen; als Typbezeichnung kann int oder Integer verwendet werden, der Typ ist immer Integer
- für jene Objekttypen, die primitiven Datentypen entsprechen, gibt es literale Deklaration. Oder einfacher erklärt: 1 ist in Groovy die Kurzschreibweise für new Integer(1)
- Operatoren sind auch für Objekttypen verwendbar, sie
sind eine Kurzschreibweise für den Aufruf einer
Operatormethode, d.h. 1 + 1
ist in Groovy die Kurzschreibweise für
new Integer(1).add(new Integer(1)) - Die Division von zwei Integer-Werten ergibt ein Resultat vom Typ BigDecimal anstatt einen Integer wie bei Java
- Strings können in doppelten oder einfachen Hochkommas stehen, d.h. "Hallo" oder 'Hallo'
- Strings, die über mehr als eine Zeile gehen, werden von dreifachen Hochkommas umschlossen (das hätte wirklich irgendwem schon früher einfallen können! Wie viele Tausend " + oder " & _ habe ich in meinem Leben schon geschrieben!)
- Groovy unterstützt Closures (siehe Glossar)
- Syntax für die Arbeit mit Collections ist viel einfacher
Völlig unverständlich ist mir, dass Groovy die Bedeutung von == in Bezug auf Objekte geändert hat:
Java | Groovy | |
Objektvergleich | obj1.equals(obj2) | obj1 == obj2 |
Objektidentität | obj1 == obj2 | obj1.is(obj2) |
Groovy weist
gegenüber Java einige radikale Vereinfachungen
auf, die im ersten Moment bestechend wirken. Allerdings frage ich mich,
ob optionale Klammern und Typangaben tatsächlich
Verbesserungen sind, oder ob man im gleichen Schlamassel wie bei
VBScript landet (oder bei der neuen deutschen Rechtschreibung).
PS: Das Schlamassel von VBScript ist, dass man im allgemeinen immer
alles darf, aber es dann im Speziellen gerade an dieser Stelle aus
obskuren Gründen doch wieder nicht funktioniert.
PS2: Das Schlamassel der neuen deutschen Rechtschreibung besteht darin,
dass
Sowohl-als-auch-Fälle in der Orthographie unser visuelles
Gedächtnis für die
richtige Schreibweise untergraben.
Groovy Datentypen
Groovy kennt, wie bereits oben erwähnt, nur Objekttypen.Als Ersatz für primitive Typen gibt es die folgenden Objekttypen:
Integer | 1 |
Long | 1L |
BigInteger | 1G |
Float | 1.0 oder 1F |
Double | 1.5D |
BigDecimal | 1.0 |
Character | 'x'
as char oder 'x'.toCharacter() Achtung: im Gegensatz zu Java reichen die Apostroph nicht, um chars zu kennzeichnen, denn diese sind für normale Strings vorgesehen. |
Keyword def: das Keyword def steht für "dynamisch typisiert" (dynamically typed)
Groovy kennt zwei Arten von Strings:
- Normale
Strings (java.lang.String) stehen in Apostroph, also z.B.
x = 'hallo' - GStrings
(groovy.lang.GString) stehen in Anführungszeichen, also z.B.
x = "hallo $name"
GStrings sind um die Möglichkeit, Teile dynamisch zu ersetzen, erweitert.
- instanceof prüft, ob eine Instanz vom Typ oder Untertyp der genannten Klasse ist
- die Methode getClass() liefert den Typ der Klasse zurück
${grails.util.GrailsWebUtil.getControllerFromRequest(request) instanceof GroovyObject}
Um zu überprüfen, ob ein dynamisch typisiertes Objekt eine Zahl ist, lässt sich folgender Code verwenden, denn Number steht für den Java-Datentyp java.lang.Number, den Obertyp aller numerischer Wrapper-Klassen von Byte bis BigDecimal:
def x = 15
if (x instanceof Number) {...
?:-Operator (Elvis Operator)
Wie in Java gibt es als Kurzform für ein if .. then .. else den Operator ?. Groovy hat hier allerdings noch eine elegante zusätzliche Variante: In vielen Fällen will man ja den Wert einer Variablen zurückgeben, falls dieser vorhanden ist, und andernfalls einen andern Wert setzen. Das führt zu einer Redundanz bei der Variablen, also z.B.
def geschlecht = benutzer.maennlich ? benutzer.maennlich : "unbekannt"
Mit dem sogenannten Elvis-Operator ?: lässt sich das in Groovy verkürzen zu
def geschlecht = benutzer.maennlich ?: "unbekannt"
Auf der folgenden Seite findet man übrigens eine gute Zusammenstellung aller Groovy-Operatoren: http://groovy.codehaus.org/Operators
?.-Operator (Safe Navigation Operator)
Bei der Java-Programmierung schreibt man oft Code, um sicherzustellen, dass ein Objekt wirklich vorhanden ist, bevor man auf seine Methoden zugreift. In vielen Fällen ist ein if oder try-catch-Block eine relativ schwerfällige Lösung, weil man eigentlich nur möchte, dass der Code ausgeführt wird, wenn das Object vorhanden ist, und andernfalls gar nichts passiert. Mit dem ?.-Operator hat Groovy eine elegante Lösung für dieses Problem gefunden.Collections
Wie bereits erwähnt, hat Groovy im Vergleich zu Java die Arbeit mit Collections stark vereinfacht. Es gibt drei Typen von Collections:- Ranges: Default: groovy.lang.IntRange extending java.util.AbstractList und implementing groovy.lang.Range
- Lists: Default: java.util.ArrayList
- Maps: Default: java.util.HashMap
- Sets: Default: java.util.HashSet
Arbeit mit Ranges
Ranges sind Listen von aufeinanderfolgenden geordneten Elementen.
def range2 = 1..5
assert range2.class == groovy.lang.IntRange
assert range instanceof java.util.List
Mit Ranges lässt sich eine kompakte For-Schleife programmieren:
def result = 0
def result2 = ""
(1..5).each {
result = result + it
result2 = result2 + it
}
assert result == 15
assert result2 == "12345"
Allgemein nützliche Methoden für Ranges:
def range = 'a'..'g'
assert range.from == 'a'
assert range.to == 'g'
Arbeiten mit Listen
Listen sind Ansammlungen von geordneten Elementen. Der Standardtyp einer Groovy-Liste ist java.util.ArrayList. Die Elemente müssen nicht eindeutig sein.
assert [1, 2] != [2, 1]
assert [1, 2] instanceof ArrayList
Liste definieren, Anzahl Elemente abfragen, alle Elemente löschen:
def emptyList = []
def normalList = [1, 2, 3]
assert normalList.size() == 3
normalList.clear()
assert normalList == []
assert normalList.isEmpty()
assert normalList.size() == 0
Auf Elemente der Liste zugreifen (Zugriff über Positionsindex, nullbasiert):
def normalList = [1, 2, 3]
assert normalList[1] == 2
assert normalList.get(1) == 2
assert normalList.getAt(1) == 2
assert normalList[3] == null
Ein Element mit Index, Methode oder Operator hinzufügen:
def list = []
list[0] = 'a'
list << 'b'
list = list + 'c'
list += 'd'
list += ['e']
list = list + ['f', 'g']
list.add('h')
assert list == ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
Werden beim Abfüllen mit dem Index Elemente übersprungen, dann werden diese mit null aufgefüllt:
def list = [1, 2, 3]
list[4] = 4
assert list == [1, 2, 3, null, 4]
Einzelne Elemente mit Index, Operatoren oder Methoden entfernen:
def list = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
list.remove(0)
list -= 'b'
list -= ['c', 'd', 'h']
assert list == ['e', 'f', 'g']
//pop() fetches and removes last element
assert list.pop() == 'g'
list = list - ['f', 'e']
assert list.isEmpty()
Umwandlung von Listen in Sets
def list = ['a', 'b', 'c']
def set = list as Set
assert set.class == HashSet
Mit Closures über Listen iterieren
def result = 0
[1, 2, 3, 4].each {
result += it
}
assert result == 10
Allgemein nützliche Methoden für Listen
//Überprüfen, ob Element in Liste vorkommt
assert [3, 5, 2].contains(2)
//Liste umdrehen
assert [1, 2, 3, 4].reverse() == [4, 3, 2, 1]
//Liste sortieren
def list = [5, 7, 1, 6].sort()
assert list == [1, 5, 6, 7]
list = ['salut', 'hi', 'hello'].sort()
assert list == ["hello", "hi", "salut"]
//Lottozahlen generieren mit shuffle
list = (1..49).toList()
Collections.shuffle(list)
println list[0..5]
//verschachtelte Liste in einfache umwandeln
assert [1, 2, [3, 4]].flatten() == [1, 2, 3, 4]
//Liste in String umwandeln mit Trennzeichen
def text =[1, 2, 3, 4].join("; ")
assert text == "1; 2; 3; 4"
assert text.class == String
//Liste auf die eindeutigen Elemente reduzieren
def list = [1, 3, 3, 5, 5 ,5]
assert list.unique() == [1, 3, 5]
Arbeit mit Maps
Eine Map besteht aus Key-Value-Paaren. Die Keys müssen nicht zwingend denselben Typ haben. Die Keys sind eindeutig, die letzte Definition hat Vorrang:
assert [1: 'a', 2: 'b', 2: 'c'] == [1: 'a', 2: 'c']
Maps sind standardmässig HashMaps:
assert [:] instanceof java.util.HashMap
Map definieren, Anzahl Elemente abfragen, alle Elemente löschen:
def emptyMap = [:]
def normalMap = [1: 'a', 2: 'b', 3: 'c']
assert normalMap.size() == 3
normalMap.clear()
assert normalMap == [:]
assert normalMap.isEmpty()
Auf Elemente der Map zugreifen:
def normalMap = [1: 'a', 2: 'b', 'x':5, 3: 'c']
assert normalMap.x == 5
assert normalMap['x'] == 5
//normalMap.3 throws a MissingPropertyException
assert normalMap[3] == 'c'
assert normalMap.get(1) == 'a'
assert normalMap.get(4) == null
assert normalMap.get(4, 'd') == 'd' //setzt Default
Einzelne Elemente hinzufügen oder entfernen:
def myMap = [:]
myMap[1] = 'a'
myMap[null] = 'b' //darf null als Key enthalten
assert myMap == [1: 'a', (null): 'b']
myMap.remove(1)
assert myMap == [(null): 'b']
Operatoren mit Maps (im Gegensatz zu Listen ist weder - noch << erlaubt)
assert ['a':4, 'x':3] + ['d':7] ==
['a':4, 'x':3, 'd':7]
//letzte Map hat Vorrang
assert ['a':4, 'x':3] + ['d':7, 'a':1] ==
['a':1, 'x':3, 'd':7]
Abfragen, ob Keys oder Values vorhanden sind:
assert ['lbl.cust': 'Kunde',
'lbl.user': 'Benutzer'].containsKey('lbl.user')
assert ['lbl.cust': 'Kunde',
'lbl.user': 'Benutzer'].containsValue('Kunde')
assert map["lbl.cust"] != null
assert map.'lbl.cust' != null
Umwandlung in Listen
assert map.keySet().toList() instanceof List
assert map.values().toList() instanceof List
Umwandlung in Sets
assert [1: 'a', 2: 'b', 3: 'c'].keySet() ==
[1, 2, 3] as Set
assert [1: 'a', 2: 'b', 3: 'c'].values() as Set ==
['a', 'b', 'c'] as Set
assert ['a': 1, 'b': 2].keySet() instanceof Set
Mit Closures über Maps iterieren
def map = [1:2, 2:3, 3:4]
def result = 0
map.each {key, value ->
result = result + ( key * value)
}
assert result == 20
def map = [ax:2, bx:3, cx:4]
result = ""
map.each {item ->
result = result + item.key + item.value
}
assert result == "ax2bx3cx4"
Arbeit mit Sets
Sets sind Ansammlungen von ungeordneten, eindeutigen Elementen.
assert ([1, 2] as Set) instanceof java.util.HashSet
Liste definieren, Anzahl Elemente abfragen, alle Elemente löschen:
def emptySet = [] as Set
def normalSet = [1, 2, 3, 2] as Set
assert normalSet.size() == 3
normalSet.clear()
assert normalSet == [] as Set
assert normalSet.isEmpty()
assert normalSet.size() == 0
def s1 = [1, 2, 2, 3, 3, 4] as Set
def s2 = [1, 2, 3, 4] as Set
def s3 = new HashSet([1,2,3,3,3,4])
assert s1 == s2
Elemente hinzufügen oder entfernen:
def set = [] as Set
set.add('x')
set += 'y'
set << 'z'
assert set == ['x', 'y', 'z'] as Set
set.remove('z')
set -= 'y'
assert (set - set).isEmpty()
Umwandlung in Listen:
def s1 = new HashSet([1, 3, 2, 4, 1])
assert s1.toList() == [1, 2, 3, 4]
Operatoren mit Sets
def s1 = new HashSet([1, 3, 4, 1])
def s2 = [5, 7, 4, 1] as Set
assert s1 + s2 == [1, 3, 4, 5, 7] as Set
assert (s1 + s2).class == HashSet
assert s1 - s2 == [3] as Set
assert s2 - s1 == [5, 7] as Set
Allgemein nützliche Methoden für Sets
//Überprüfen, ob Element in Set vorkommt
def set = [3, 5, 2] as Set
assert set.contains(5)
Methoden, die nur für Listen, aber nicht für Sets existieren:
- getAt(..)
- pop()
- putAt(..)
- reverse()
Weitere Codebeispiele zu Collections findet man unter
http://groovy.codehaus.org/Collections oder
http://groovy.codehaus.org/JN1015-Collections
Glossar
- Closures: Eine Closure ist ein Datentyp, der keinen Zustand, sondern nur Verhalten hat, d.h. eine Closure ist eine Methode in Objektform. In anderen Sprachen nennt man Closures auch Methodenreferenzen oder Funktionspointer. In Java entspricht eine Closure einer anonymen inneren Klasse. Typische Einsatzgebiete von Closures sind z.B. Eventhandler. In Grails werden alle automatisch erzeugten Actions eines Controllers als Closures implementiert. Eine ausführliche Erklärung gerade auch für Java-Programmierer/innen findet sich im Groovy-Codehaus.
- CRUD: Abkürzung für Create, Read, Update und Delete, d.h. die vier zentralen Befehle zur Manipulation von Datensätzen
- Dependency
injection: Architekturmodell in der
Programmierung, bei dem die Verantwortung für Objekterzeugung
und die Zusammenarbeit von Objekten aus den Objekten herausgenommen und
an Factories delegiert wird (siehe Wikipedia-Artikel). Den
Objekten werden die abhängigen Objekte bzw. Ressourcen
zugewiesen. Ziel dieses Konzepts ist es, die Abhängigkeit
zwischen Komponenten oder Objekten zu minimieren. Dependency injection
ist eine spezielle Form von "Inversion of Control".
Der Name ist eigentlich falsch, denn es wird durch die Übergabe von Objektion Funktionalität eingespritzt (injected), um die Abhängigkeit (Dependency) zu reduzieren. - Flash: Flash ist in Grails ein Konstrukt zwischen Session und Request, d.h. ein Container, der Informationen von einer Seite bis zur nächsten aufbewahrt. Das ermöglicht, Informationen von einer Seite zur andern zu transferieren, ohne die Session mit Datenmüll zu verstopfen.
- GORM: Grails Object-Relational Mapping, d.h. das Mapping von Objekten zu Tabellen in einer relationalen Datenbank
- Hibernate: Persistenzframework
- HQL: Hibernate Query Language
- HSQLDB: In-Memory-Datenbank von Grails für die Entwicklung und zum Testen
- Inversion of Control: Konzept hinter dem Spring-Framework. Es geht um die Beziehung zwischen Objekten und Frameworks (Containern). Eine klare Definition ist schwierig, da sich die Autoren z.T. widersprechen. Zentral ist, dass nicht die Komponenten oder Objekte den Ablauf einer Applikation steuern, sondern ein Framework (siehe Wikipedia-Artikel).
- Scaffolding: Automatisches Erzeugen von Verzeichnissen, Klassen, GSP-Seiten und weiteren Elementen der Applikation (in Englisch nennt sich das "auto-generation of artifacts"). Der oben erwähnte Befehl "grails generate-all" ist ein typisches Beispiel für Scaffolding.
- Spring: Open-Source-Applikationsframework für Java. Spring ist dabei der Leim, der die verschiedenen Komponenten zusammenklebt (hat doch Microsoft von ASP vor Urzeiten auch behauptet).
- UTF-8: Am weitesten verbreitete Codierung für Unicode-Zeichen. UTF-8 setzt sich im Internet (Web und Mail) zunehmend als Standardkodierung durch, die von allen Browsern und Mailprogrammen unterstützt wird.
Quellen und Links
- Website für Grails: http://grails.org/
- Website für Groovy: http://groovy.codehaus.org/
- Grails-Forum: http://www.nabble.com/codehaus---grails-f11860.html
- Deutschsprachiges Grails- und Groovy-Forum: http://www.groovy-forum.de
- Graeme Rocher's Blog: http://graemerocher.blogspot.com/
- Guillaume Laforge's Blog: http://glaforge.free.fr/weblog/?catid=2
Diese Webseite wurde am 03.05.18 um 22:34 von rothen ecotronics erstellt oder überarbeitet.
Printed on 100% recycled electrons!