Groovy als Web-Backend

Aus Jonas Notizen Webseite
Version vom 5. August 2020, 22:23 Uhr von Admin (Diskussion | Beiträge) (→‎Wichtige Notiz zu dbCreate und Datenbank-Migration: Added link to official comparison of both db-migration-tools)
Zur Navigation springen Zur Suche springen

Inhaltsverzeichnis

Einführung

Quellen


Über Grails

Grails Logo

Für die Java-Technologie gibt es eine große Anzahl Web-Frameworks. Diese sind aber meist komplizierter als sie sein müssten, und folgen meist nicht dem DRY-Prinzip ("Don't Repeat yourself").


Dynamische Frameworks wie Django (Python) und Rails (Ruby) haben neue Moderne Einblicke gesetzt, wie man über Web-Applikationen denken sollte.

Grails baut auf dessen Konzepte auf und reduziert die Komplexität beim Erstellen von Webanwendungen auf der Java-Plattform erheblich. Das Besondere daran ist, dass es auf bereits etablierten Java-Technologien wie Spring und Hibernate aufbaut.

Zu den hervor-zu-hebenden Features zählen:

All dies wird durch die Leistungsfähigkeit der Groovy-Sprache und den umfassenden Einsatz domänenspezifischer Sprachen (DSLs) wesentlich vereinfacht.


Installation von Grails 3.3

Installations-Anforderungen

Für Grails 3 wird eine minimale JDK-Version von 1.8 gefordert: Wenn man JDK 1.7 verwenden möchte, müsste man Gradle mit Java 1.7.0_131-b31 oder höher ausführen, um die Auflösung von Gradle-Abhängigkeiten zu beheben, wenn die Unterstützung von TLS v1.1 und v1.0 eingestellt wird.

Automatische Grails-Installation mithilfe des SDKMAN's-Projekts

SDKMAN! ist ein Werkzeug zur Verwaltung paralleler Versionen mehrerer Software Development Kits (SDKs) auf den meisten Unix-basierten Systemen. Es bietet eine bequeme Befehlszeilenschnittstelle (CLI) und API zum Installieren, Wechseln, Entfernen und Auflisten von Kandidaten (SDKs) an.

Nachdem man SDKMAN! mithilfe der offiziellen Anleitung erfolgreich installiert hat kann man mit dem folgenden Befehl Grails auf sein System installieren:

$ sdk install grails 3.3.9

Optional noch Java und Groovy SDK installieren, falls nicht schon getan:
$ sdk install groovy 2.4.15
$ sdk install java 8.0.252.j9-adpt

Falls dies die erste Installation von Grails mithilfe des SDKMAN! ist, wird diese Version direkt als Standard in allen Umgebungen gesetzt und somit direkt angewendet.

App erstellen

$ grails create-app APP-NAME erstellt im derzeitigen Verzeichnis innerhalb eines neuen Ordner mit dem angegebenen Namen das Grundgerüst für eine neue Grails-Anwendung.

Grails-Befehle ausführen

Wenn man jetzt in den erstellten Projekt-Ordner wechselt ($ cd APP-NAME) gibt es 2 Möglichkeiten, Grails-Befehle aufzurufen:

  1. $ grails BEFEHL
    • Nachteil: Für jeden Befehl muss eine neue JVM-Instanz erstellt werden (= Bei jedem Befehl muss man eine gewisse Zeit extra warten)
    • Nachteil: Keine Auto-Vervollständigung mithilfe der Tabulator-Taste
  2. Mithilfe von $ grails in die Interaktive-Konsole wechseln, und dort seine Befehle (Ohne grails vorne dran) ausführen
    • Vorteil: Es wird nur einmal die JVM gestartet und die benötigten Projekt- und Grails-Daten geladen (= Weniger Ladezeit)
    • Vorteil: Auto-Vervollständigung mithilfe der Tabulator-Taste


Das heißt: Wenn man in einer Dokumentation grails <<command>> kann man:

  1. Direkt grails <<command>> in seiner Shell eingeben
  2. Zuerst mithilfe von grails in die Interaktive Grails-Shell eindringen und danach den <<command>> eingeben.


Grails-Integrationen für diverse IDE's

IntelliJ IDEA Logo

IntelliJ-IDEA

IntelliJ IDEA in der "Ultimate"-Edition ist eine ausgezeichnete IDE für die Entwicklung von Grails (Sowie auch für viele andere (Web-)Frameworks, siehe offizieller Vergleichs-Tabelle "Ultimate vs Community").

Ich kann es jedem nur empfehlen - ich könnte nicht-mehr ohne die Funktionen von IntelliJ IDEA Ultimate leben.

TextMate, Sublime, VIM, ...

Siehe offizielle Dokumentation

Konventionen über Konfiguration

"Konventionen über Konfiguration" bedeutet, dass der Ort/Name einer Datei zur Identifikation seiner Rolle/Eigenschaften genutzt wird.
Je nach Speicherort einer Klasse erhält diese spezifische Variablen/Methoden/Beans (durch die Verwendung von Dependency-Injections und Groovy Domänspezifischen-Sprachen (DSLs)) injeziert. (Beispiel: constraints, hasMany).

Da der Ort einer Klasse entscheidet, wofür/wie sie von Grails genutzt wird, sollte man sich mit dem Aufbau einer Grails-Applikation bekannt werden lassen:

App starten

Grails-Apps können durch den eingebauten Tomcat-Server mit dem Befehl $ grails run-app "von Grund auf" gestartet werden.

  • Standard-Port dieses Web-Servers ist 8080, kann aber durch den Parameter -port=[PORT] abgeändert werden.
  • Unter welcher URL die App erreichbar ist wird in der Konsole angezeigt. (Standardmäßig http://localhost:8080/APP-NAME/ - kann aber auch durch die Laufzeit-Konfigurationsvariable server.contextPath geändert werden.)

App testen

Alle $ grails create-* Befehle generieren eine Unit oder Integrations-Testing Klasse im src/test/groovy Ordner.

Wenn man nun die Logik hinter den Tests hinzugefügt hat, kann man diese mit $ grails test-app starten.

App bereitstellen

Grail-Apps können auf mehrere Weisen bereitgestellt werden.
Wenn man einen Traditionellen Container (Wie Apache's Tomcat, Jetty, etc.) benutzt, kann man ganz einfach ein sog. "Web Application Archive" (.war-Datei) erstellen, welches alle Anwendungsdaten in eine Archiv-ähnliche Datei zusammenfasst. (Vergleichbar mit einem .jar-Archiv)

Grails und Tomcat

Grails bindet standardmäßig einen Tomcat-8 Container mit jeder WAR-Datei ein.
Da dies aber zu Problemen führen kann, wenn der Server bereits einen aktiven Tomcat-Container mit einer anderen Version besitzt, kann man diese Abhängigkeit (Dependency) innerhalb der build.gradle-Datei von compile auf provided umstellen:

provided "org.springframework.boot:spring-boot-starter-tomcat"

Die Tomcat-Version kann man ebenfalls unter build.gradle innerhalb der dependencies {}-Sektion wie folgt auf zB. Tomcat-7 ändern:

ext['tomcat.version'] = '7.0.59'

WAR-Datei erstellen und starten

Mit dem Grails-Befehl $ grails war baut Grails die ".war-Version" der App und speichert das Resultat unter build/libs.

Mit dem Java-Befehl java -Dgrails.env=prod -jar [DEINE-WAR-DATEI].war kann man nun ganz einfach seine erstellte .war-Datei ausführen. Beispiel: java -Dgrails.env=prod -jar build/libs/mywar-0.1.war

Unterstützte Java EE-Container

Grails 3.3.x läuft auf jeden Container der die Java Servlet API in der Version 3.0 (oder darüber) unterstützt. Darunter gelten:

  • Tomcat 7
  • GlassFish 3
  • Resin 4
  • JBOSS 6
  • Jetty 8
  • Oracle Weblogic 12c
  • IBM WebSphere 8.0


Automatisches generieren von Artefakten für eine Domänen-Klasse (Scaffolding)

Die Scaffolding-Befehle sind eine Art Starthilfe um..

  • die benötigten Views (.gsp),
  • die dazugehörigen Controller-Klassen für CRUD-Operationen,
  • die dazugehörigen Unit-Tests

.. ganz einfach (mithilfe eines von Grails im Hintergrund bereits vordefinierten Templates) mit einem Befehl erstellen zu können

Mit dem Befehl $ grails generate-all (PACKET.)KLASSEN-NAME werden von alle Komponenten das Skelett erstellt.



Konfiguration

Es mag merkwürdig erscheinen, dass man dieses Thema in einem Framework angeht, welches sich dem "Konvention-über-Konfiguration"-Leitfaden widmet.

Mit den Standardeinstellungen von Grails kann man tatsächlich eine Anwendung entwickeln, ohne irgendeine Konfiguration vorzunehmen (wie der schnell-Einstieg zeigte), aber es ist wichtig zu lernen, wo und wie man die Konventionen bei Bedarf außer Kraft setzen kann.


Möchte man dann aber z.B. seine eigene MySQL-Verbindung einrichten, kann man dies mithilfe simpler Konfigurationsdateien berwerkstelligen.

Grundlegende Konfiguration

Grails Konfigurationsdateien sind in 2 Sektionen aufgeteilt:

  • Build (build.gradle)
  • Runtime (grails-app/conf/application.yml oder application.groovy und runtime.groovy wenn man den alten Groovy-Syntax/Groovy-Angehensweiße benutzen will)

Standard-Variablen

Für die Groovy-Konfigurationsdateien (build.gradle) stehen folgende Variablen zur Verfügung: (Eingebunden/Benutz werden sie wie in jedem GString: ${grailsHome})

Variable Beschreibung
userHome Ordner-Pfad beim starten des Servers
grailsHome Ordner-Pfad indem Grails installiert ist. (WENN GRAILS_HOME gesetzt wurde, wird diese genutzt)
appName Der Applikations-Name, gelesen von build.gradle
appVersion Die Applikations-Version, gelesen von build.gradle


Grails 2.0 Konfigurations-Syntax verwenden

Wenn man es vorzieht, eine Groovy-Konfiguration im Stil von Grails 2.0 zu verwenden, dann ist es möglich, die Konfiguration mit dem Groovy-ConfigSlurper-Syntax zu spezifizieren.

  • ConfigSlurper ist eine Utility-Klasse zum Lesen von Konfigurationsdateien, die in Form von Groovy-Skripten definiert sind.
    • Wie es bei Java *.properties-Dateien der Fall ist, erlaubt ConfigSlurper eine Punktnotation. Zusätzlich erlaubt es aber auch Closure-Scoped Konfigurationswerte und beliebige Objekttypen.

Zwei Groovy-Konfigurationsdateien sind hierbei verfügbar:

Konfigurationswerte mithilfe des GrailsApplication-Objekts abrufen

Um innerhalb von Controllern/Tag-Libraries auf die Laufzeit-Konfiguration zuzugreifen, gibt es eine spezielle injezierte Variable namens grailsApplication vom Typ GrailsApplication.

Dessen config-Eigenschaft vom Typen grails.config.Config bietet nützliche Funktionen um Werte aus der Konfigurationsdatei zu erhalten:

class MyController {

    def hello(Recipient recipient) {
        // ...
        // Eigenschaft 'foo.bar.max.hellos' (Vom Typen "Integer") abrufen, falls nicht gesetzt "5" nehmen
        def max = grailsApplication.config.getProperty('foo.bar.max.hellos', Integer, 5)

        // Eigenschaft 'foo.bar.greeting' (Ohne definierten Typen, aka. "String"), falls nicht gesetzt "Hello"
        def greeting = grailsApplication.config.getProperty('foo.bar.greeting', "Hello")

        def message = (recipient.receivedHelloCount >= max) ? "Sorry, you've been greeted the max number of times" :  "${greeting}, ${recipient}"

        render message
    }
}

Die config-Eigenschaft des grailsApplication-Objekts ist eine Instanz der Config-Schnittstelle und bietet eine Reihe nützlicher Methoden zum Auslesen der Konfiguration der Anwendung.

Insbesondere die getProperty-Methode (siehe oben) ist nützlich, um Konfigurationseigenschaften effizient abzurufen, während der Eigenschaftstyp angegeben wird (der Standardtyp ist String) und/oder ein Standard/Fallback-Wert bereitgestellt wird.


Zu beachten ist, dass die Config-Instanz eine zusammengeführte Konfiguration ist, die auf dem PropertySource-Konzept von Spring basiert und die Konfiguration aus

  • der Umgebung,
  • den Systemeigenschaften und
  • der lokalen Anwendungskonfiguration (bspw. application.yml)

liest und sie zu einem einzigen Objekt zusammenführt.


Das GrailsApplication-Objekt kann auch ganz einfach in service's und andere Grails-Artifakte wie folgt injected werden:

import grails.core.*

class MyService {
    GrailsApplication grailsApplication

    String greeting() {
        def recipient = grailsApplication.config.getProperty('foo.bar.hello')
        return "Hello ${recipient}"
    }
}


Konfigurationswerte mit Variablen verknüpfen

Die Value-Annotation von Spring kann dazu verwendet werden, einer Variable zur Laufzeit mit dem jeweiligen Konfigurationswert einzuspeisen

import org.springframework.beans.factory.annotation.*

class MyController {
    // Beim starten wird die Variable auf den Wert von der Konfig gesetzt
    @Value('${foo.bar.hello}')
    String recipient

    def hello() {
        render "Hello ${recipient}"
    }
}

Beachte: Im Groovy-Code müssen für den Wert der Value-Annotation einfache Anführungszeichen (') um die Zeichenfolge verwenden, andernfalls wird sie als GString und nicht als Spring-Ausdruck interpretiert.


Logging-Konfiguration

Standardmäßig wird die Protokollierung in Grails 3.0 mithilfe des Logback-Frameworks (Offizielle Dokumentation) verarbeitet und kann mit der Datei unter grails-app/conf/logback.groovy konfiguriert werden.

Logger-Namen

Grails-Artifakte (Controller, Services, ...) wird automatisch eine log Methode injeziert.

  • Vor Grails 3.3.0 folgte der Name des Loggers für Grails Artefakt der Konvention grails.app.<type>.<className>, wobei type für den Artefakt-Typ steht, z.B. Controller oder Dienste, und className für den voll qualifizierten Namen des Artefakts.
  • Ab Grails 3.3.x wurden die Logger-Namen vereinfacht.

Das nächste Beispiel veranschaulicht die Änderung:

Logger Name

(Grails 3.3.x oder höher)

Logger Name

(Grails 3.2.x oder niedrieger)

BookController.groovy in grails-app/controllers/com/company OHNE @Slf4j-Annotation com.company.BookController grails.app.controllers.com.company.BookController
BookController.groovy in grails-app/controllers/com/company MIT @Slf4j-Annotation com.company.BookController com.company.BookController


Logging-Konfigurationsdatei bestimmen

Um zu bestimmen, von welcher Datei Logback seine Konfigurationswerte lesen soll, gibt es 3 Möglichkeiten:

  • Innerhalb der Runtime-Konfiguration:
logging:
    config: /Users/me/config/logback.groovy
  • Mithilfe einer Umgebungsvariable:
$ export LOGGING_CONFIG=/Users/me/config/logback.groovy
$ ./gradlew bootRun
  • Mithilfe einer System-Eigenschaft:
$ ./gradlew -Dlogging.config=/Users/me/config/logback.groovy bootRun


Anfrage-Parameter im Log-Stacktrace verstecken

Wenn Grails eine Stapel(speicher)zurückverfolgung protokolliert, kann die Protokollnachricht die Namen und Werte aller Anforderungsparameter für die aktuelle Anforderung enthalten. Parameter, die in dem Protokoll nicht aufgeführt werden sollen, können innerhalb der Konfigurationsvariable grails.exceptionresolver.params.exclude in Form einer Liste hinterlegt werden:

grails:
    exceptionresolver:
        params:
            exclude:
                - password
                - creditCard

Die Protokollierung von Anforderungsparametern kann ganz abgeschaltet werden, indem der Konfigurationswert grails.exceptionresolver.logRequestParameters auf false gesetzt wird.

Der Standardwert ist

  • true, wenn die Anwendung im Modus DEVELOPMENT läuft, und
  • false für alle anderen Umgebungen.


Umgebungs-Abhängige Konfigurationswerte

Wenn man einen Konfigurationspfad direkt in die Datei einfügt (Am Anfang der Zeile, aka. als Parent-Node) gilt diese für alle gestarteten Umgebungen.


Die Dateien application.yml und application.groovy im Verzeichnis grails-app/conf verstehen das Konzept der "Konfiguration pro Umgebung", wobei entweder YAML oder die von ConfigSlurper bereitgestellte Syntax verwendet werden kann.

Als Beispiel betrachte man die folgende von Grails bereitgestellte Standard application.yml-Definition:

environments:
    development:
        dataSource:
            dbCreate: create-drop
            url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    test:
        dataSource:
            dbCreate: update
            url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    production:
        dataSource:
            dbCreate: update
            url: jdbc:h2:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        properties:
           jmxEnabled: true
           initialSize: 5
        ...

Der obige Code kann im Groovy-Syntax (application.groovy) wie folgt ausgedrückt werden: (Bemerke, wie die allgemeine Konfiguration auf der obersten Ebene bereitgestellt wird und dann ein Umgebungsblock-pro-Umgebung Einstellungen für die Eigenschaften dbCreate und url der DataSource angeben.)

dataSource {
    pooled = false
    driverClassName = "org.h2.Driver"
    username = "sa"
    password = ""
}
environments {
    development {
        dataSource {
            dbCreate = "create-drop"
            url = "jdbc:h2:mem:devDb"
        }
    }
    test {
        dataSource {
            dbCreate = "update"
            url = "jdbc:h2:mem:testDb"
        }
    }
    production {
        dataSource {
            dbCreate = "update"
            url = "jdbc:h2:prodDb"
        }
    }
}

Befehl in gewünschter Umgebung ausführen

Tipp: Grails hat die Kapabilität, Befehle mit einer gesetzten Umgebung auszuführen: grails <<Environment>> <<Command Name>>. Beispiel: grails test war

Umgebung programmatisch auslesen

Innerhalb des Codes, z.B. in einem Gant-Skript oder einer Bootstrap-Klasse, kann man die Umgebung mit Hilfe der Environment-Klasse erkennen:

import grails.util.Environment

...

switch (Environment.current) {
    case Environment.DEVELOPMENT:
        configureForDevelopment()
        break
    case Environment.PRODUCTION:
        configureForProduction()
        break
}

DataSource

Weil Grails auf Java aufbaut sollte man ein bisschen Verständnis von JDBC (Java Database Connectivity, die Datenbankschnittstelle der Java-Plattform) besitzen.

Treiber einfügen

Wenn man eine andere Datenbank als H2 nimmt benötigt man noch einen passenden JDBC-Treiber. Für MySQL wäre dies Connector/J.
Solche Treiber kommen normalerweise in Form eines Java-Archivs. Am besten fügt man diese Treiber jedoch anhand ihres Maven-Repos in seine dependencies ein: (Gefunden in build.gradle)

dependencies {
    runtime 'mysql:mysql-connector-java:5.1.29'
}

Wenn man die JAR erfolgreich eingebunden hat (und diese auch erfolgreich aufgelöst wird) kann man sich jetzt mit der Weise bekannt machen wie Grails die Datenbank-Konfigurationen handhabt.

dataSource einrichten

In Grails 2.0 wurde die Datei grails-app/conf/DataSource.groovy zum einstellen der Datenbank-Konfigurationen verwendet.

Seit Grails 3.0 muss eine Runtime-Konfigurationsdatei wie die application.yml verwendet werden.

Für die Konfigurations-Eigenschaft dataSource stehen folgende Eigenschaften zur Einstellung bereit:

  • url - Die JDBC-URL der Datenbank
  • driverClassName - Klassenname des JDBC-Treibers (zB. "com.mysql.jdbc.Driver")
  • username, password - Login-Daten, um die JDBC-Verbindung aufzubauen
  • jndiName - Der Name der JNDI-Ressource für die DataSource
  • dbCreate - Gibt an, ob die Datenbank automatisch aus dem Domänen-Modell generiert werden soll: "create-drop", "create", "update" oder "validate".
    • create - Falls sich das Datenbank-Schema verändert hat, wird die Tabelle entleert und mithilfe des neuen Schemas erstellt
    • create-drop - Gleich wie create, leert die Datenbank (!) hingegen bei jedem neustart (Egal ob sich das Schema geändert hat oder nicht, nützlich zum Testen)
    • update - Aktualisiert das Model der Tabelle, falls es sich verändert hat (Die Daten bleiben erhalten, Die Domainklassen-Tabelle muss existieren) Notiz: Solch eine Weise veträgt natürlich nicht viele Änderung des Schemas. (zB. Beim ändern des Namens einer Spalte bleibt die alte Spalte mit den existierenden Daten und die neue wird einfach hinzugefügt)
    • validate - Warnt vor Änderungen, aber verändert das Datenbank-Schema bei Veränderung nicht
  • pooled - Ob ein Pool von Verbindungen aufgebaut werden sollte (Standard: true)
  • lazy - Ob ein LazyConnectionDataSourceProxy verwendet werden soll
  • transactionAware - ob ein TransactionAwareDataSourceProxy verwendet werden soll
  • readOnly - Wenn true, ist die DataSource schreibgeschützt, was dazu führt, dass der Verbindungspool für jede Verbindung setReadOnly(true) aufruft
  • type - Die Verbindungspoolklasse, wenn Sie Grails zwingen möchten, sie zu verwenden, wenn mehr als eine verfügbar ist.
  • logSql - Leitet SQL-Logs zum stdout um
  • formatSql - Ob SQL-Logs formatiert werden sollen
  • dialect - Eine Zeichenfolge oder Klasse, die den Ruhezustand-Dialekt darstellt, der für die Kommunikation mit der Datenbank verwendet wird. Informationen zu verfügbaren Dialekten findet man im Paket org.hibernate.dialect.
  • transactional - Wenn false, wird die DataSource-Transaktionsmanager-Bean außerhalb der verketteten BE1PC-Transaktionsmanager-Implementierung belassen. Dies gilt nur für zusätzliche Datenquellen.
  • persistenceInterceptor - Die Standarddatenquelle wird automatisch mit dem Persistenz-Interceptor verbunden, andere Datenquellen werden nicht automatisch verbunden, es sei denn, dies ist auf true gesetzt
  • properties - Extra Einstellungen für den DatenSource-Bean. (Siehe Tomcat-Pool Dokumentation sowie die dazugehörige Javadoc)
  • jmxExport - Wenn false, wird die Registrierung von JMX-MBeans für alle DataSources deaktiviert. Standardmäßig werden JMX-MBeans für DataSources mit jmxEnabled = true in den Eigenschaften hinzugefügt.


Wichtige Notiz zu dbCreate und Datenbank-Migration

Die Eigenschaft dbCreate der DataSource-Definition ist wichtig, da sie vorgibt, was Grails zur Laufzeit hinsichtlich der automatischen Generierung der Datenbanktabellen aus GORM-Klassen tun soll.

Im Entwicklungsmodus (development) ist dbCreate standardmäßig auf "create-drop" eingestellt, aber irgendwann in der Entwicklung (und auf jeden Fall, wenn Sie zur Produktion übergehen) müssen Sie aufhören, die Datenbank jedes Mal, wenn Sie Ihren Server starten, fallen zu lassen und neu zu erstellen.

Es ist verlockend, zu "update" zu wechseln, so dass Sie vorhandene Daten beibehalten und das Schema nur aktualisieren, wenn sich Ihr Code ändert, aber die Aktualisierungsunterstützung von Hibernate ist sehr konservativ. Es werden keine Änderungen vorgenommen, die zu Datenverlusten führen könnten, und umbenannte Spalten oder Tabellen werden nicht erkannt, so dass Sie das alte Schema behalten und auch das neue haben.


Grails unterstützt Datenbankmigrationen mit Liquibase oder Flyway (Siehe offizieller Vergleich) über Plugins:


Typische MySQL Konfiguration

Eine typische DataSource eines Grails 2.0 Projekts könnte so aussehen:

dataSource {
    pooled = true
    dbCreate = "update"
    url = "jdbc:mysql://localhost:3306/my_database"
    driverClassName = "com.mysql.jdbc.Driver"
    dialect = org.hibernate.dialect.MySQL5InnoDBDialect
    username = "username"
    password = "password"
    properties {
       jmxEnabled = true
       initialSize = 5
       maxActive = 50
       minIdle = 5
       maxIdle = 25
       maxWait = 10000
       maxAge = 10 * 60000
       timeBetweenEvictionRunsMillis = 5000
       minEvictableIdleTimeMillis = 60000
       validationQuery = "SELECT 1"
       validationQueryTimeout = 3
       validationInterval = 15000
       testOnBorrow = true
       testWhileIdle = true
       testOnReturn = false
       jdbcInterceptors = "ConnectionState;StatementCache(max=200)"
       defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
    }
}

Komplexere MySQL Konfiguration

Ein Beispiel einer komplexeren DataSource-Konfiguration:

dataSource {
    pooled = true
    dbCreate = "update"
    url = "jdbc:mysql://localhost:3306/my_database"
    driverClassName = "com.mysql.jdbc.Driver"
    dialect = org.hibernate.dialect.MySQL5InnoDBDialect
    username = "username"
    password = "password"
    properties {
       // Documentation for Tomcat JDBC Pool
       // http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#Common_Attributes
       // https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/tomcat/jdbc/pool/PoolConfiguration.html
       jmxEnabled = true
       initialSize = 5
       maxActive = 50
       minIdle = 5
       maxIdle = 25
       maxWait = 10000
       maxAge = 10 * 60000
       timeBetweenEvictionRunsMillis = 5000
       minEvictableIdleTimeMillis = 60000
       validationQuery = "SELECT 1"
       validationQueryTimeout = 3
       validationInterval = 15000
       testOnBorrow = true
       testWhileIdle = true
       testOnReturn = false
       ignoreExceptionOnPreLoad = true
       // http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#JDBC_interceptors
       jdbcInterceptors = "ConnectionState;StatementCache(max=200)"
       defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED // safe default
       // controls for leaked connections
       abandonWhenPercentageFull = 100 // settings are active only when pool is full
       removeAbandonedTimeout = 120
       removeAbandoned = true
       // use JMX console to change this setting at runtime
       logAbandoned = false // causes stacktrace recording overhead, use only for debugging
       // JDBC driver properties
       // Mysql as example
       dbProperties {
           // Mysql specific driver properties
           // http://dev.mysql.com/doc/connector-j/en/connector-j-reference-configuration-properties.html
           // let Tomcat JDBC Pool handle reconnecting
           autoReconnect=false
           // truncation behaviour
           jdbcCompliantTruncation=false
           // mysql 0-date conversion
           zeroDateTimeBehavior='convertToNull'
           // Tomcat JDBC Pool's StatementCache is used instead, so disable mysql driver's cache
           cachePrepStmts=false
           cacheCallableStmts=false
           // Tomcat JDBC Pool's StatementFinalizer keeps track
           dontTrackOpenResources=true
           // performance optimization: reduce number of SQLExceptions thrown in mysql driver code
           holdResultsOpenOverStatementClose=true
           // enable MySQL query cache - using server prep stmts will disable query caching
           useServerPrepStmts=false
           // metadata caching
           cacheServerConfiguration=true
           cacheResultSetMetadata=true
           metadataCacheSize=100
           // timeouts for TCP/IP
           connectTimeout=15000
           socketTimeout=120000
           // timer tuning (disable)
           maintainTimeStats=false
           enableQueryTimeouts=false
           // misc tuning
           noDatetimeStringSync=true
       }
    }
}


Die Application-Klasse

Jede neue Grails-Anwendung verfügt über eine Application innerhalb des Verzeichnisses grails-app/init.

Die Application-Klasse ist eine Unterklasse der GrailsAutoConfiguration-Klasse und verfügt über die static void main, d.h. sie kann als reguläre Anwendung ausgeführt werden.

Die Application-Klasse ausführen

Wenn man eine IDE verwendet, kann man ganz einfach mit der rechten Maustaste auf die Klasse klicken und sie direkt von der IDE aus starten, wodurch Ihre Grails-Anwendung gestartet wird.

Dies ist auch für das Debugging nützlich, da Sie direkt von der IDE aus debuggen können, ohne einen Remote-Debugger anschließen zu müssen, wenn Sie den Befehl run-app --debug-jvm von der Befehlszeile aus ausführen.

Man kann die Anwendung auch z.B. in eine lauffähige WAR-Datei packen (nützlich, wenn man plant die Anwendung mit einem containerlosen Ansatz zu implementieren.):

$ grails package
$ java -jar build/libs/myapp-0.1.war



Grails Scripte

Das Befehlszeilensystem von Grails 3.0 unterscheidet sich stark von früheren Versionen von Grails und verfügt über APIs zum Aufrufen von Gradle für build-bezogene Aufgaben sowie zur Codegenerierung.

Wenn man den folgenden Befehl eingibt, durchsucht Grails das Profil-Repository auf der Grundlage des Profils der aktuellen Anwendung. Wenn das Profil für eine Web-Anwendung ist, werden Befehle aus dem Web-Profil und dem Basis-Profil, von dem es erbt, gelesen.

$ grails <<command name>>


Es wird zuerst die Anwendung und dann das Profil nach Befehlen durchsucht. Beispiel am Befehl run-app:

  • PROJECT_HOME/src/main/scripts/run-app.groovy
  • [profile]/commands/run-app.groovy
  • [profile]/commands/run-app.yml

Grails-Scripte erstellen

Mit dem Befehl $ grails create-script NAME kann man ein Grund-Gerüst für sein neues Skript unter src/main/scripts/ erstellen lassen.

Im Allgemeinen sollten Grails-Skripte für die Skripterstellung des Gradle-basierten Build-Systems und für die Code-Generierung verwendet werden. Skripte können keine Anwendungsklassen laden und sollten dies auch nicht tun, da Gradle zur Konstruktion des Anwendungs-Klassenpfades erforderlich ist.


description()

Jedes Skript erbt (unter anderem)

  • von GroovyScriptCommand, welches eine API für viele nützliche Aufgaben bereitstellt, sowie
  • von TemplateRenderer, welches die zur Code-Generierung genutzten render/template-Methoden zur Verfügung stellt. (Siehe Beispiel unten)


Die description()-Methode wird für die Ausgabe vom grails help Befehl verwendet, um den Nutzern zu helfen.
Beispiel am generate-all Befehl:

description( "Generates a controller that performs CRUD operations and the associated views" ) {
  usage "grails generate-all <<DOMAIN CLASS>>"
  flag name:'force', description:"Whether to overwrite existing files"
  argument name:'Domain Class', description:'The name of the domain class'
}


model()

Wenn man model() mit einer Klassen/String/Datei/Resource-Parameter aufruft, erhält man eine neue Instanz der Klasse Model welche hiflreiche Methoden zur Quellcode-Generierung besitzt.

Beispiel:

def domain = model(com.foo.Bar)

domain.className == "FooBar"
domain.fullName == "com.foo.FooBar"
domain.packageName == "com.foo"
domain.packagePath == "com/foo"
domain.propertyName == "fooBar"
domain.lowerCaseName == "foo-bar"

Darüber hinaus steht die asMap-Methode zur Verfügung, mit der alle Eigenschaften in eine Map verwandelt werden können, die an die render-Methode übergeben wird.

Scripts innerhalb eines Scripts aufrufen

Grails hat schon von Anfang an eine große Anzahl an Skripts, welche man ganz einfach aufrufen kann.
Der folgende Code ruft den Befehl testApp mit dem Parametern --debug-jvm auf.

testApp('--debug-jvm')

Gradle-Tasks aufrufen

Mithilfe der injezierten gradle-Variable können auch Gradle-Tasks getriggert werden:

gradle.compileGroovy()

Ant-Tasks aufrufen

Man kann auch Ant-Tasks aus Skripten heraus aufrufen, was beim schreiben von Codegenerierung und Automatisierungsaufgaben sehr nützlich sein kann:

(Ant ist über ein Plugin, dass bei so gut wie jeder Applikation dabei sein sollte, bereits integriert)

ant.mkdir(dir:"path")


Template-Generation + Beispiel-Befehl

Plugins und Anwendungen, die Aufgaben zur Vorlagenerstellung definieren müssen, können dies mit Hilfe von Skripten tun.

Hierzu sollten die Methoden benutzt werden, die vom geerbeten TemplateRenderer bereitgestellt werden.

Ein Beispiel anhand des create-script-Befehls:

description( "Creates a Grails script" ) {
  usage "grails create-script <<SCRIPT NAME>>"
  argument name:'Script Name', description:"The name of the script to create"
  flag name:'force', description:"Whether to overwrite existing files"
}

def scriptName = args[0]
def model = model(scriptName)
def overwrite = flag('force') ? true : false

render  template: template('artifacts/Script.groovy'),
        destination: file("src/main/scripts/${model.lowerCaseName}.groovy"),
        model: model,
        overwrite: overwrite


Wenn ein Skript in einem Plugin oder Profil definiert ist, wird die template(String)-Methode in der Anwendung nach der Vorlage suchen, bevor die von Ihrem Plugin oder Profil bereitgestellte Vorlage verwendet wird. Auf diese Weise können die Benutzer des jeweiligen Plugins oder Profils anpassen, was generiert wird.

Es ist üblich, den Benutzern auf einfache Weise zu ermöglichen, die Vorlagen aus seinem Plugin oder Profil zu kopieren. Hier ist ein Beispiel dafür, wie das Angular-Scaffolding-Script Vorlagen kopiert.

templates("angular/**/*").each { Resource r ->
    String path = r.URL.toString().replaceAll(/^.*?META-INF/, "src/main")
    if (path.endsWith('/')) {
        mkdir(path)
    } else {
        File to = new File(path)
        SpringIOUtils.copy(r, to)
        println("Copied ${r.filename} to location ${to.canonicalPath}")
    }
}


Grails Befehle

Unterschied Befehl und Skript

Im Gegensatz zu Skripten bewirken Befehle den Start der Grails-Umgebung, d.H. man hat den vollen Zugriff auf den Anwendungskontext und die Laufzeit.

Änderungen in Grails 3.2

Seit Grails 3.2.0 haben Befehle ähnliche Fähigkeiten wie Skripte in Bezug auf das Abrufen von Argumenten, die Erzeugung von Vorlagen, den Dateizugriff und die Modellerstellung.

Befehle, die in Grails 3.1.x oder niedriger erstellt wurden, implementieren standardmäßig die Eigenschaft ApplicationCommand, die erfordert, dass der Befehl die folgende Methode implementiert:

boolean handle(ExecutionContext executionContext)

Befehle, die in Grails 3.2.0 oder höher erstellt wurden, implementieren standardmäßig die Eigenschaft GrailsApplicationCommand, die erfordert, dass der Befehl die folgende Methode implementiert: (Auf diese Weise definierte Befehle haben über eine Variable namens executionContext weiterhin Zugriff auf den Ausführungskontext.)

boolean handle()

Befehle erstellen

Ähnlich wie bei Skripten kann man mit dem Befehl $ grails create-command NAME das Skelett eines Skriptes unter src/main/commands/ erstellen lassen.

Befehl ausführen

Selbst-geschriebene Befehle können

  • mithilfe von $ grails run-command NAME oder
  • mithilfe des Gradle-Tasks "runCommand" gradle runCommand -Pargs="NAME"
    • Wenn der Grails-Server ein Unterprojekt ist (z.B. in einem Projekt, das mit Angular erstellt wurde), kann der Unterprojekt-Befehl immer noch aus dem Gradle-Wrapper im übergeordneten Projekt aufgerufen werden: ./gradlew server:runCommand -Pargs="my-example"

aufgerufen werden.



Gradle und Grails

Grails 3.1 verwendet das Gradle Build System für Aufgaben, die mit dem Bauen zusammenhängen, wie z.B:;

  • der Kompilierung,
  • das ausführen von Tests und
  • die Erstellung von Binärdistributionen des Projekts.

Es wird empfohlen, Gradle 2.2 oder höher mit Grails 3.1 zu verwenden. Die Gradle-Konfiguration wird in der build.gradle-Datei realisiert. Hier steht u.A. die aktuelle Version der Anwendung, die benötigten Abhängigkeiten (Dependencies) und die Quell-Adressen woher diese Abhängigkeiten abgerufen werden.

Wenn man den Befehl grails ausführt, wird die Version von Gradle, die mit Grails 3.1 (derzeit 2.9) ausgeliefert wird, durch den grails-Prozess über die Gradle Tooling API aufgerufen:

# Macht das gleiche wie 'gradle classes'
$ grails compile

Man kann auch gradle selbst aufrufen um seine auf dem System installierte Version von Gradle nutzen. Dabei sollte man aber beachten, dass ab Grails 3.0 eine Mindestanforderung von Gradle 2.2 besteht.

$ gradle assemble

Abhängigkeiten bestimmen

Abhängigkeiten für das Projekt werden im dependencies-Block definiert. Im Allgemeinen kann man hierbei der offiziellen Gradle-Dokumentation zur Verwaltung von Abhängigkeiten folgen, um zu verstehen, wie man zusätzliche Abhängigkeiten konfigurieren kann.

Ein Beispiel anhand des web-Profils:

dependencies {
  compile 'org.springframework.boot:spring-boot-starter-logging'
  compile('org.springframework.boot:spring-boot-starter-actuator')
  compile 'org.springframework.boot:spring-boot-autoconfigure'
  compile 'org.springframework.boot:spring-boot-starter-tomcat'
  compile 'org.grails:grails-dependencies'
  compile 'org.grails:grails-web-boot'

  compile 'org.grails.plugins:hibernate'
  compile 'org.grails.plugins:cache'
  compile 'org.hibernate:hibernate-ehcache'

  runtime 'org.grails.plugins:asset-pipeline'
  runtime 'org.grails.plugins:scaffolding'

  testCompile 'org.grails:grails-plugin-testing'
  testCompile 'org.grails.plugins:geb'

  // Note: It is recommended to update to a more robust driver (Chrome, Firefox etc.)
  testRuntime 'org.seleniumhq.selenium:selenium-htmlunit-driver:2.44.0'

  console 'org.grails:grails-console'
}

Beachte: Die Versionsnummern sind in der Mehrzahl der Abhängigkeiten nicht vorhanden. Dies ist dem Abhängigkeitsverwaltungs-Plugin zu verdanken, welches eine Maven BOM konfiguriert, welche die Standard-Abhängigkeitsversionen für bestimmte häufig verwendete Abhängigkeiten und Plugins definiert:

dependencyManagement {
    imports {
        mavenBom 'org.grails:grails-bom:' + grailsVersion
    }
    applyMavenExclusions false
}

Gradle Tasks verstehen

Wie bereits erwähnt, verwendet der grails-Befehl eine eingebettete Version von Gradle und bestimmte grails-Befehle, die in früheren Versionen von Grails existierten, werden nun mithilfe ihrer Gradle-Äquivalente abgebildet. Die folgende Tabelle zeigt, welcher Grails-Befehl welche Gradle-Aufgabe aufruft:

Grails Befehle und dessen Gradle Task Äquivalent
Grails Befehl Gradle Task
clean clean
compile classes
package assemble
run-app bootRun
test-app test
test-app --integration integrationTest
war assemble

Um auch andere Gradle-Tasks mithilfe der in Grails integrierten Version von Gradle auszuführen, bietet Grails den Befehl $ grails gradle <<task>>. Es wird wie immer empfohlen, diesen Befehl im interaktiven-Modus auszuführen, um auf zusätzliche Privilegien wie Auto-Vervollständigung von Tasks und schnelles ausführen Zugriff zu bekommen. Tipp: gradle tasks zeigt alle verfügbaren Tasks an.

$ grails
| Enter a command name to run. Use TAB for completion:
 grails> gradle tasks

(Übersicht) Offizielle Grails-Plugins für Gradle

Wenn man ein neues Projekt mit dem $ grails create-app erstellt wird eine standardmäßige build.gradle generiert. Die standardmäßige build.gradle konfiguriert den Build mit einem Satz von Gradle-Plugins, die es Gradle ermöglichen, das Grails-Projekt zu erstellen:

apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"org.grails.grails-gsp"
apply plugin:"asset-pipeline"

Die Standard-Plugins sind wie folgt:

  • war - Das WAR-Plugin ändert den Packaging-Prozess, so dass Gradle aus der Anwendung eine WAR-Datei erzeugt. Man kann dieses Plugin auskommentieren, wenn man nur eine lauffähige JAR-Datei für den eigenständigen Einsatz erstellen möchten.
  • asset-pipeline - Das Asset-Pipeline-Plugin ermöglicht die Zusammenstellung von statischen Assets (JavaScript, CSS usw.)

Viele davon sind in Plugins eingebaut, die von Gradle- oder Drittanbieter-Plugins bereitgestellt werden. Die Gradle-Plugins, die Grails zur Verfügung stellt, sind die folgenden:

  • org.grails.grails-core - Das primäre Grails-Plugin für Gradle, das von allen anderen Plugins eingebunden wird und für den Betrieb mit allen Profilen ausgelegt ist.
  • org.grails.grails-gsp - Das Grails GSP-Plugin fügt die Vorkompilierung von GSP-Dateien für den Produktionseinsatz hinzu.
  • org.grails.grails-plugin-publish - Ein Plugin für die Veröffentlichung von Grails-Plugins im zentralen Repository.
  • org.grails.grails-profile - Ein Plugin zur Verwendung beim Erstellen von Grails-Profilen.
  • org.grails.grails-profile-publish - Ein Plugin zur Veröffentlichung von Grails-Profilen im zentralen Repository.
  • org.grails.grails-web - Das Grails-Web-Gradle-Plugin konfiguriert Gradle so, dass es die Grails-Konventionen und die Verzeichnisstruktur versteht.



TODO #6 erstmals ausgelassen

Objekt-Relationales Mapping (ORM)

Domänenklassen sind der Kern jeder Geschäftsanwendung. Sie halten Zustand über Geschäftsprozesse und implementieren hoffentlich auch Verhalten. Sie sind durch Beziehungen miteinander verbunden; eins-zu-eins, eins-zu-viele oder viele-zu-viele.


GORM ist die objektrelationale Mapping (ORM)-Implementierung von Grails.

Unter der Haube verwendet es Hibernate (eine sehr beliebte und flexible Open-Source-ORM-Lösung), und dank der dynamischen Natur von Groovy mit seiner statischen und dynamischen Typisierung sowie der Konvention von Grails ist bei der Erstellung von Grails-Domänenklassen weitaus weniger Konfiguration erforderlich.


In dieser Anleitung befasse ich mich mit (den in meinen Augen wichtigsten Wissen/..) der originalen Implementierung "GORM for Hibernate" in der Version 6.1.x (die mit Grails 3.3.9 ausgeliefert wird).

  • Manche für mich eher unwichtige Kapitel werde ich hier nur insofern ansprechen, in dem ich einen Hyperlink mit dem Text "Siehe Offizielle Dokumentation" einfüge.


Versionsverlauf

Siehe Offizielle Dokumentation

Upgrade-Hinweise

Siehe Offizielle Dokumentation

GORM einrichten

Falls nicht bereits bei der Erstellung der Applikation getan, kann man "GORM 6.1.12 for Hibernate" wie folgt zu seinem Grails-Projekt hinzufügen:

  • Folgende Abhängigkeiten in build.gradle einfügen:
// build.gradle
dependencies {
    compile "org.grails.plugins:hibernate5:6.1.12"
    compile "org.hibernate:hibernate-ehcache"
}
  • Falls man eine Grails-Version unter 3.3 nutzt ist man eventuell dazu gezwungen, die GORM-Version explizit zu setzen.
    • Bei der Nutzung von Grails 3.2.7 oder höher kann dies durch das setzen der gormVersion-Variable innerhalb von gradle.properties realisiert werden:
      # gradle.properties
      gormVersion=6.1.12.RELEASE
      

    • Anderenfalls muss das setzen der Version durch das einfügen des folgenden Codes überhalb des dependencies-Block innerhalb der build.gradle-Datei realisiert werden:
      // build.gradle
      configurations.all {
          resolutionStrategy.eachDependency { DependencyResolveDetails details ->
              if( details.requested.group == 'org.grails' &&
                  details.requested.name.startsWith('grails-datastore')) {
                  details.useVersion("6.1.12.RELEASE")
              }
          }
      }
      dependencies {
          ...
      }
      

Häufige Probleme

Wenn man einen Fehler erhält, der darauf hinweist, dass die grails-datastore-simple-Abhängigkeit nicht aufgelöst werden kann, muss man möglicherweise den folgenden Codeblock direkt überhalb den dependencies-Block innerhalb der build.gradle-Datei einfügen:

// build.gradle
configurations.all {
    exclude module:'grails-datastore-simple'
}
dependencies {
    ...
}


Eine andere Hibernate-Version definieren

Siehe Offizielle Dokumentation

GORM in einer Spring-Boot-Applikation verwenden

Siehe Offizielle Dokumentation

GORM ausserhalb von Grails oder Spring verwenden

Wenn man GORM für Hibernate außerhalb einer Grails-Anwendung verwenden möchten, sollte man die notwendigen Abhängigkeiten für GORM und die von einem verwendete Datenbank deklarieren, z.B. in Gradle:

compile "org.grails:grails-datastore-gorm-hibernate5:6.1.12.RELEASE"
runtime "com.h2database:h2:1.4.192"
runtime "org.apache.tomcat:tomcat-jdbc:8.5.0"
runtime "org.apache.tomcat.embed:tomcat-embed-logging-log4j:8.5.0"
runtime "org.slf4j:slf4j-api:1.7.10"

Das obige Beispiel verwendet die H2-Datenbank und den Tomcat-Verbindungspool. Es werden jedoch auch andere Pool-Implementierungen unterstützt, einschließlich commons-dbcp, tomcat pool oder hikari. Wenn kein Verbindungspool angegeben ist, wird org.springframework.jdbc.datasource.DriverManagerDataSource verwendet, die bei jeder Verbindungsanforderung eine neue Verbindung zur Datenbank herstellt.

  • Letzteres wird wahrscheinlich Probleme mit einer H2-In-Memory-Datenbank verursachen, da bei jeder Verbindungsanforderung eine neue In-Memory-Datenbank erstellt wird, wodurch zuvor erstellte Tabellen verloren gehen. Normale Datenbanken (MySql, Postgres oder sogar dateibasierte H2-Datenbanken) sind nicht betroffen.


Erstellen Sie dann Ihre Entities im Verzeichnis src/main/groovy und annotieren Sie sie mit der Annotation grails.gorm.annotation.Entity:

// Die Verwendung von GormEntity dient lediglich der Unterstützung der IDE außerhalb von Grails. 
// Bei Verwendung innerhalb eines Grails-Kontextes verwenden einige IDEs die Position der grails-app/domain als Hinweis, um die Code-Vervollständigung zu ermöglichen.
@Entity
class Person implements GormEntity<Person> { 
    String firstName
    String lastName
    static constraints = {
        firstName blank:false
        lastName blank:false
    }
}


Schlussendlich muss man die Bootstrap-Logik in seine Applikation einfügen, die HibernatDatastore verwendet: (Siehe die Offzielle Dokumentation für mehr Informationen zur Konfiguration von GORM)

import org.grails.orm.hibernate.HibernateDatastore
Map configuration = [
    'hibernate.hbm2ddl.auto':'create-drop',
    'dataSource.url':'jdbc:h2:mem:myDB'
]
HibernateDatastore datastore = new HibernateDatastore( configuration, Person)

Konfiguration

Siehe das bereits besprochene Kapitel "DataSource" und die Offizielle Dokumentation

Standard-Mappings und Constraints

Besondere Erwähnung jedoch verdienen die Einstellungen grails.gorm.default.mapping und grails.gorm.default.constraints. Diese definieren die Standard-ORM-Zuordnung und die von jeder Entität verwendeten Standard-Validierungseinschränkungen.

Ändern der Standard-Datenbankzuordnung

Möglicherweise haben Sie Grund, die Art und Weise zu ändern, wie alle Domänenklassen auf die Datenbank abgebildet werden.

Beispielsweise verwendet GORM standardmäßig die native "ID-Generierungsstrategie" der Datenbank, unabhängig davon, ob es sich dabei um eine Spalte mit automatischer Erhöhung oder eine Sequenz handelt.

Wenn man alle Domänenklassen global ändern möchten, um eine uuid-Strategie zu verwenden, kann man dies in der Standardzuordnung angeben:

// grails-app/conf/application.groovy (Da es sich bei der Einstellung um eine Groovy-Konfiguration handelt, muss sie in ein Groovy-bewusstes Konfigurationsformat gehen.)
grails.gorm.default.mapping = {
        cache true
        id generator:'uuid'
}

Wie man sieht, kann man eine Closure zuweisen, die dem mapping-Block entspricht, mit dem man normalerweise die Zuordnung einer Domänenklasse zu einer Datenbanktabelle anpassen würde.


Ändern der Standard-Constraints

Zur Validierung wendet GORM einen Standardsatz von Einschränkungen auf alle Domänenklassen an.

Beispielsweise sind standardmäßig alle Eigenschaften von GORM-Klassen nicht nullable. Das bedeutet, dass für jede Eigenschaft ein Wert angegeben werden muss (Ansonsten kommt es zu einem Validierungsfehler).

In den meisten Fällen ist es das, was Sie wollen, aber wenn Sie es mit einer großen Anzahl von Spalten zu tun haben, kann es sich als unbequem erweisen.

Man kann die Standardbeschränkungen jedoch über die Groovy-Konfiguration mit der Einstellung grails.gorm.default.constraints ändern:

// grails-app/conf/application.groovy
grails.gorm.default.constraints = {
    '*'(nullable: true, size: 1..20) // '*' makes EVERY PROPERTY have these contraints
}


Quick-Start Anleitung (Grundlegends CRUD)

Siehe Offizielle Dokumentation


Domain-Modelling in GORM: Einführung

Beim Erstellen von Anwendungen müssen man die Problemdomäne berücksichtigen, die man zu lösen versucht. Wenn man zum Beispiel eine Buchhandlung im Amazon-Stil aufbaut denkt man an Bücher, Autoren, Kunden und Verleger, um nur einige zu nennen.

Diese werden in GORM als Groovy-Klassen modelliert, so dass eine Buchklasse einen Titel, ein Erscheinungsdatum, eine ISBN-Nummer usw. haben kann. Die nächsten Abschnitte zeigen, wie Domänen in GORM modelliert werden können.

Betrachten Sie z.B. die folgende Domänenklasse:

Beispiel einer simplen Domänenklasse - ohne Assoziationen zu anderen Klassen. Nur simple Java-Eigenschaften.
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

package org.bookstore

class Book {
    String title
    Date releaseDate
    String ISBN
}
Grails-gorm-simple book example-db schema view.png

Diese Klasse wird automatisch auf eine Tabelle in der Datenbank namens book (derselbe Name wie die Klasse) abgebildet. (Dieses Verhalten ist über die ORM-Domänenspezifische Sprache (ORM-DSL) anpassbar)Jede Java-Eigenschaft wird auf eine Spalte in der Datenbank abgebildet, wobei die Konvention für Spaltennamen "Kleinbuchstaben die durch Unterstriche getrennt sind" lautet.

  • Zum Beispiel bildet releaseDate auf eine Spalte release_date ab.
  • Die SQL-Typen werden automatisch von den Java-Typen erkannt, können aber mit Constraints oder der ORM-DSL angepasst werden.

Domain-Modelling in GORM: Einführung

Assoziationen definieren wie Domänenklassen miteinander interagieren. Wenn nicht an beiden Enden explizit angegeben, existiert eine Beziehung nur in der Richtung, in der sie definiert ist.

Unidirektionales Many-To-One

In diesem Fall haben wir eine unidirektionale Many-to-One-Beziehung von Face-to-Nose.

Beispiel einer Unidirektionalen Many-to-One Beziehung von Face zu Nose.
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Face {
2     Nose nose
3 }
4 
5 class Nose {}
Grails-gorm-unidirectional many to one-db schema view.png
Beispiel-Code der Unidirektionalen Many-to-One Beziehung von Face zu Nose, inklusive die daraus resultierende Datenbankansicht (IntelliJ).
Beispiel-Code Resultat
Face erroredFace = new Face()
erroredFace.save(flush:true)
erroredFace.errors.allErrors.each { println it } // Prints "Field error in object 'grailstesting.Face' on field 'nose': rejected value [null]; codes [...(list of i18n-codes)...]"

Nose myNose = new Nose()
new Face(nose: myNose).save()
Face changingFace = new Face(nose: myNose).save()
changingFace.setNose(new Nose())
changingFace.save() // Successfully saving new data also means bumping up the internal 'version' property.
Grails-gorm-unidirectional many to one-example code-resulting table view.png
Face myFace = new Face()
Nose myNose = new Nose()
myFace.setNose(myNose)

myFace.save() // The saving of 'myFace' also triggers the saving of the associated 'nose' property

myFace.delete() // Only 'myFace' gets deleted from the Database. 'myNose' stays because it isn't hardly bond to belong to any domain-class, meaning it is allowed to exist on its own and live a happy live.


Bidirektionales Many-To-One

Um die obige Beziehung bidirektional zu gestalten, definieren wir die andere Seite (Nose) wie folgt

Beispiel einer Bidirektionalen Many-to-One Beziehung von Face zu Nose.
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Face {
2     Nose nose
3 }
4 
5 class Nose {
6     static belongsTo = [face:Face]
7 }
Bemerke: Die Struktur bleibt gleich wie beim unidirektionalen Many-to-One!
Grails-gorm-unidirectional many to one-db schema view.png
Bemerke: Die Struktur bleibt gleich wie beim unidirektionalen Many-to-One!

In diesem Fall verwenden wir die Einstellung belongsTo, um zu sagen, dass Nose zum Face "gehört". Das Ergebnis davon ist, dass wir ein Face erstellen, eine Nose-Instanz daran anhängen können und wenn wir die Face-Instanz speichern oder löschen, wird GORM die assozierte Nose speichern oder löschen. Mit anderen Worten, das Speichern und Löschen erfolgt in einer Kaskade vom Gesicht zur zugehörigen Nase:

Beispiel-Code der Bidirektionalen Many-to-One Beziehung von Face zu Nose, inklusive die daraus resultierende Datenbankansicht (IntelliJ).
Beispiel-Code Resultat
Face myFace = new Face()
Nose myNose = new Nose()
myFace.setNose(myNose)

myFace.save() // The saving of 'myFace' also triggers the saving of the associated 'nose' property

myFace.delete() // BOTH 'myFace' and its associated 'nose' get deleted from the Database, because a 'Nose' can't exist without a 'Face' thanks to its 'belongsTo'-Instruction.
Beachte: Die Umgekehrte Vorgehens-weiße des Löschens ist aufgrund des transienten Face's ungültig und resultiert in einen Fehler:
Nose myNose = new Nose()
Face myFace = new Face(nose: myNose)
myFace.save() // As above: The saving of 'myFace' also triggers the saving of the associated 'nose' property

myNose.delete() // Results in the error mentioned in the next column
org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations): [grailstesting.Nose#1]
Durch das Hinzufügen des folgenden Code's in die Domänenklasse Face...
static mapping = { 
    // See http://docs.grails.org/latest/ref/Database%20Mapping/cascade.html
    nose cascade: 'none' 
}
static constraints = { nose(nullable: true) }
...Kann die obige myFace-Instanz immer-noch bestehen bleiben wenn dessen nose-Instanz sich selber löscht.
Nose myNose = new Nose();
new Face(nose: myNose).save();

// Thanks to the bidirectionality, a Nose can also access the Face it's associated to/it belongs to:
println(myNose.face) // "grailstesting.Face : 1"
println(myNose.faceId) // "1" (Variable injected by GORM DSL for convencience)

Bidirektionales One-To-One

Um die obige Beziehung zu einer echten Eins-zu-Eins-Assoziation zu machen, verwendet man die Eigenschaft hasOne auf der besitzenden Seite, in diesem Beispiel Face:

  • Wichtig: hasOne funktioniert nur mit bidirektionalen Beziehungen!
Beispiel einer Bidirektionalen One-to-One Beziehung von Face zu Nose.
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Face {
2     static hasOne = [nose:Nose]
3 }
4 
5 class Nose {
6     Face face
7 }

Beachte: Die Verwendung dieser Eigenschaft verlagert den Fremdschlüssel auf die inverse Tabelle des Beispiels, so dass in diesem Fall die Fremdschlüsselspalte in der nose-Tabelle innerhalb einer Spalte namens face_id gespeichert wird.

(Alle Beispiel-Codes vom obigen "Bidirektionale Many-To-One" verhalten sich auch hier gleich)

Letztendlich ist es eine gute Idee, eine unique-Einschränkung auf einer Seite der Eins-zu-Eins-Beziehung hinzuzufügen:

class Face {
    static hasOne = [nose:Nose]

    static constraints = {
        nose unique: true
    }
}

Aufgehört bei "Controlling the ends of the association". TODO weitermachen


Traits

Grails stellt eine Reihe von Traits zur Verfügung, die Zugang zu Eigenschaften und Verhalten bieten, auf die von verschiedenen Grails-Artefakten sowie von beliebigen Groovy-Klassen aus zugegriffen werden kann, die Teil eines Grails-Projekts sind.

Viele dieser Eigenschaften werden automatisch zu den Grails-Artefakt-Klassen hinzugefügt (wie z.B. Controller und Taglibs) und lassen sich leicht zu anderen Klassen hinzufügen.

Von Grails bereitgestellten Traits

Grails-Artefakte werden zur Kompilierungszeit automatisch mit bestimmten Traits ergänzt.

Domain Class Traits

Controller Traits

Interceptor Trait

Tag Library Trait


Weitere Traits

Nachstehend findet man eine Liste weiterer Traits, die das Framework bietet. Die Javadocs bieten mehr Details über Methoden und Eigenschaften, die mit jedem Trait verbunden sind.

Trait Brief Description
grails.web.api.WebAttributes Common Web Attributes
grails.web.api.ServletAttributes Servlet API Attributes
grails.web.databinding.DataBinder Data Binding API
grails.artefact.controller.support.RequestForwarder Request Forwarding API
grails.artefact.controller.support.ResponseRedirector Response Redirecting API
grails.artefact.controller.support.ResponseRenderer Response Rendering API
grails.validation.Validateable Validation API

Beispiel einer Trait-Injektion

WebAttributes ist eine der Traits, die das Framework zur Verfügung stellt. Jede Groovy-Klasse kann dieses Trait implementieren, um alle Eigenschaften und Verhaltensweisen zu erben, die von der Eigenschaft bereitgestellt werden.

// src/main/groovy/demo/Helper.groovy
package demo

import grails.web.api.WebAttributes
import groovy.transform.CompileStatic

@CompileStatic // The traits are compatible with static compilation!
class Helper implements WebAttributes {

    List<String> getControllerNames() {
        // There is no need to pass grailsApplication as an argument
        // or otherwise inject the grailsApplication property.  The
        // WebAttributes trait provides access to grailsApplication.
        grailsApplication.getArtefacts('Controller')*.name
    }
}



Die Web-Ebene

Controller

Ein Controller verarbeitet Anforderungen und erstellt oder bereitet die Antwort vor. Ein Controller kann die Antwort direkt generieren oder an eine Ansicht (View, z.B..gsp) delegieren.

Controller erstellen

Alle Controller befinden sich im Stammbaum unter grails-app/controllers/APP_NAME. Die einzige Voraussetzung für eine Klasse innerhalb dieser Hierarchie ist es, mit Controller.groovy zu enden.


Mit dem Befehl $ grails create-controller (PAKET.)KLASSEN_NAME erstellt man das Skelett eines Controllers, welches dann entsprechend unter grails-app/controllers/APP_NAME/PAKET/KLASSEN_NAME.groovy gespeichert wird.

  • Dieser Befehl ist nur zur vereinfachten Generierung gedacht, man kann es auch manuell oder mit einer IDE machen.

Aktionen

Die Standard URL-Mapping-Konfiguration versichert, dass der Name des Controllers sowie jede Methode zum entsprechendem URI-Pfad gebunden wird.
Das folgende Beispiel ist hiermit unter localhost:8080/mahlzeit/index und localhost:8080/mahlzeit/list erreichbar:

package myapp

class MahlzeitController {
    // Standard-Aktion
    def index() { }
    
    // Aktion "/list"
    def list {
        //..
        // Controller-Logik
        //..
        return model
    }
}

Ein Controller kann mehrere öffentliche Methoden haben, welche sich (Wie oben beschrieben) jeweils zur einer URI binden.

Standard-Aktion

Wenn der Nutzer keine bestimmte Aktion in seiner Anfrage stehen hat (Also zB. nur den Namen des Controllers, wie "/mahlzeit/" anstatt zB. "/mahlzeit/login") versucht Grails eine Standard-Aktion ausfindig zu machen, welche sich dieser Anfrage zur Verfügung stellt.

  • Wenn es nur eine Aktion (aka. Methode) gibt, wird diese als Standard-Aktion anerkannt
  • Wenn es eine Aktion namens "index" gibt, wird diese als Standard-Aktion anerkannt

Alternativ kann man auch eine eigenen Standard setzten, indem man static defaultAction = "DEINE-METHODE" in den Quellcode des Controllers einfügt.


Namespaced-Controller

Wenn eine Anwendung mehrere Controller mit demselben Namen in verschiedenen Paketen definiert, müssen die Controller in einem Namensraum definiert werden. Die Art und Weise, einen Namensraum für einen Controller zu definieren, besteht darin, eine statische Eigenschaft namens namespace im Controller zu definieren und der Eigenschaft, die den Namensraum repräsentiert, einen String zuzuweisen.

package com.app.reporting

class AdminController {
    static namespace = 'reports'
    // ...
}
package com.app.security

class AdminController {
    static namespace = 'users'
    // ...
}


Bei der Definition von URL-Mappings, die mit einem Namespace-Controller verknüpft werden sollen, muss die Namespace-Variable Teil des URL-Mappings sein.

class UrlMappings {

    static mappings = {
        '/userAdmin' {
            controller = 'admin'
            namespace = 'users'
        }

        '/reportAdmin' {
            controller = 'admin'
            namespace = 'reports'
        }

        "/$namespace/$controller/$action?"()
    }
}
<!-- Reverse URL mappings also require that the namespace be specified. -->
<g:link controller="admin" namespace="reports">Click For Report Admin</g:link>
<g:link controller="admin" namespace="users">Click For User Admin</g:link>


Scope-Variablen

"Scope-Variablen" sind HashMap ähnliche Strukturen, in welche man Variablen speichern kann. Für Controller stehen folgende Scope-Variablen zur Verfügung:

servletContext: Statisch, für alle gleich

Dieser Scope lässt uns Daten speichern, welche über die gesamte Web-App gleich statisch erreichbar sind (Applikationskontext).
Das servletContext-Objekt ist eine Instanz vom Standardmäßigen Java(EE)-Objekt ServletContext. Grails injeziert zudem noch weitere Methoden.

Es ist nützlich um:

  • Applikations-Eigenschaften zu speichern
  • lokale Server-Ressourcen zu laden und
  • Informationen vom Servlet-Container zu erhalten.

session: Für jede Sitzung anders

Das session-Objekt ist eine Instanz der Klasse HttpSession der Java(EE) Servlet-API.

Es ist nützlich um Attribute der derzeitigen Sitzung eines Klientens zu speichern, wie zB. der Login (Name/Passwort).

request: Mitgesendete Informationen

Das request-Objekt ist eine Instanz von HttpServletRequest der Java(EE) Servlet API.

Es ist nützlich um:

  • Mitgesendete Request-Header-Felderdaten zu bekommen
  • Anfragen-bezogene Attribute zwischen-zu-speichern und
  • Informationen des aktuellen Klienten zu erhalten

Grails injeziert zudem einige zusätzliche Methoden in das request-Objekt, die das standardmäßige HttpServletRequest-Objekt nicht bietet. Darunter:

  • XML - Eine Instanz der GPathResult-Klasse vom XMLSluper, welche es erlaubt, einkommende XML-Anfragen zu verarbeiten (parsen) - Nützlich für REST
  • JSON - Eine Instanz der JSONObject-Klasse, welche es erlaubt, einkommente JSON-Anfragen zu verarbeiten (parsen) - Nützlich für JSON und REST
  • isRedirected() - Gibt true zurück, wenn eine Weiterleitung für diesen Antrag ausgestellt wurde
  • get - Gibt true zurück wenn die Anfrage ein GET-Request ist
  • post - Gibt true zurück wenn die Anfrage ein POST-Request ist
  • each - Implementation von Groovys each-Methode zur Iteration über Anfrage-Attribute
  • find - Implementation von Groovys find-Methode um ein gewisses Anfrage-Attribut zu finden (Ohne komplett richtigen Hashkey-Namen)
  • findAll - Implementation von Groovys findAll-Methode um Anfrage-Attribut zu finden (Ohne komplett richtigen Hashkey-Namen)
  • format - Das Anfrageformat, das für die Inhalts-Verhandlung (Content Negotiation) verwendet wird.
  • withFormat(Closure) - Die withFormat-Methode, die für die Inhalts-Verhandlung (Content Negotiation) verwendet wird.
  • xhr - Gibt true zurück wenn die Anfrage ein AJAX-Request ist.


Die XML- und JSON-Eigenschaften sind nützlich für XML-APIs und können zum Parsen eingehender XML- oder JSON-Pakete verwendet werden. Zum Beispiel kann der folgende Anfragekörper...

<book>
   <title>The Stand</title>
</book>

ganz einfach mithilfe des request-Objekts serialisiert und im Code genutzt werden:

def title = request.XML?.book.title // Mit dem "?" versichern wir uns, dass wenn die Variable "XML" "null" ist es zu keiner NullPointerException kommt und "title" einfach auch auf "null" gesetzt wird (Groovy feature :D)
render "The Title is $title"

(params): (Veränderbare,) Mitgesendete CGI Informationen

Eine veränderbare, mehrdimensionale HashMap aller Anforderungsparametern (CGI).
Obwohl das request-Objekt auch Methoden zum lesen der Anforderungsparametern verfügt, ist der params-Scope manchmal nützlich für die Daten-Bindung an Objekte.

Beispiel:

def save() {
    def book = new Book(params) // bind request parameters onto properties of book
}

flash: Speicher zwischen 2 Anfragen

Diagramm eines Anwendungsbeispiels der Post/Redirect/Get-Architektur. Quelle: Wikimedia

Eine temporäre Speicherzuordnung, in der Objekte innerhalb der Sitzung für die nächste Anforderung, und auch nur für die nächste Anforderung, gespeichert werden. Die darin enthaltenen Objekte werden nach Abschluss der nächsten Anforderung automatisch gelöscht.

Dies ist z.B. nützlich, um eine Nachricht direkt vor der Umleitung zu setzen: (Siehe Redirect after Post-Konzept/Problemstellung)

def delete() {
    def b = Book.get(params.id)
    if (!b) {
        flash.message = "User not found for id ${params.id}"
        redirect(action:list)
    }
    ... // remaining code
}

def list(){
    // use "flash.message" in the rendered gsp-template or something like that
}

Bemerke: Die Namen und Typen die man dem flash-Objekt setzt können willkürlich sein.


Scope eines Controllers

Neu erstellte Controller haben den Standard-Scope-Wert "singleton". Verfügbare Controller-Scopes sind:

  • prototype (Standard) - Für jeden Request wird ein neuer Controller erstellt
  • session - Für jede Sitzung wird nur ein Controller-Objekt erstellt
  • singleton - Für die gesamte Zeit gibt es nur eine Instanz des Controllers (Wird geraten für Aktionen mit Methoden)

Dieser Standard-Wert kann man unter grails-app/conf/application.yml wie folgt abändern:

grails:
    controllers:
        defaultScope: singleton

Man kann den Scope eines Individuellen Controllers auch manuell ändern, indem man static scope = "DEIN-SCOPE" in den Quellcode des Controllers einfügt.


Model und Ansicht (Model and View)

Diagramm der Interaktionen innerhalb des MVC-Musters.Quelle: Wikimedia

Das "Model" ist eine Map, die die Ansicht (View) beim Rendern verwendet.

  • Die Schlüssel der Map korrespondieren mit dem Variablen-Namen über dessen sie innerhalb einer View (z.B. einer .gsp-Datei) erreichbar sind.
  • Zu beachten ist, dass bestimmte Variablennamen im Modell nicht verwendet werden können:
    • attributes
    • application


Es gibt mehrere Wege um ein Model zu übergeben/zu erzeugen:

  • Explizit: Wenn seine Methode gleich wie die View-Datei heißt, übergibt man das Model einfach wie beim returnen mit folgendem Syntax: [Var1: Data1, Var2: Data2, ...]
def VIEW_NAME() {
   [book: Book.get(params.id)] // Gleich wie render(view: "VIEW_NAME", model: [book: Book.get(params.id)])
}
  • Ein fortgeschrittenerer Ansatz ist die Rückgabe einer Instanz der Spring ModelAndView-Klasse zu benutzen:
import org.springframework.web.servlet.ModelAndView

def index() {
    // get some books just for the index page, perhaps your favorites
    def favoriteBooks = ...

    // forward to the list view to show them
    return new ModelAndView("/book/list", [ bookList : favoriteBooks ])
}


View-Datei selber selektieren mit der render()-Methode

In den vorherhigen 2 Beispielen haben wir nirgendwo angegeben an welche View die Map-Daten übergeben werden soll.
Dies liegt an Grails Konventionen. Im folgenden Beispiel würde Grails unter grails-app/views/book/show.gsp nachsuchen: (Nach dem Schema grails-app/views/CONTROLLER/AKTION.gsp)

class BookController {
    def show() {
         [book: Book.get(params.id)]
    }
}

Falls wir aber nicht wollen, das Grails sich den Namen der View-Datei mithilfe des Methoden-Namens sucht, können wir die vielfältige render()-Methode einsetzen:

def show() {
    def map = [book: Book.get(params.id)]
    // Wenn man nur einen Dateinamen eingibt rechnet Grails damit, dass sich die Datei im Unterordner mit dem Namen des Controllers befindet.
    render(view: "display", model: map)           // Datei "display.[gsp|jsp]" wird im "grails-app/views/books"-Ordner gesucht. (Methode befindet sich im "BookController")
    // Selbsts den Unterordner bestimmen:
    render(view: "/shared/display", model: map) // Datei "display.[gsp|jsp]" wird im "grails-app/views/shared"-Ordner gesucht
}

(Anmerkung: Wenn Grails keine .gsp-Datei finden kann, sucht es auch nach .jsp-Dateien.)


Selektions-Verfahren von Controllern die einem speziellen Namensraum angehören

Wenn ein Controller einen eigenen Namensraum gesetzt hat (In diesem Beispiel 'business'), schaut Grails

  • zuerst nach ob die View-Datei unter grails-app/views/business/... zu finden ist
  • und falls nicht sucht es unter dem normalen Namespace grails-app/views/... nach.

Beispiel:

class ReportingController {
    static namespace = 'business'

    def humanResources() {
        // Diese Methode wird "grails-app/views/business/reporting/humanResources.gsp" rendern, falls die Datei existiert.
        //              Falls "grails-app/views/business/reporting/humanResources.gsp" NICHT existiert, wird "grails-app/views/reporting/humanResources.gsp" versucht.
        // Sprich: Die GSP im Namensraum hat Vorrang vor dem GSP ohne Namensraum.

        [numberOfEmployees: 9]
    }


    def accountsReceivable() {
        // Diese Methode wird "grails-app/views/business/reporting/numberCrunch.gsp" rendern, falls die Datei existiert.
        //              Falls "grails-app/views/business/reporting/numberCrunch.gsp" NICHT existiert, wird "grails-app/views/reporting/humanResources.gsp" versucht.
        // Sprich: Die GSP im Namensraum hat Vorrang vor dem GSP ohne Namensraum.

        render view: 'numberCrunch', model: [numberOfEmployees: 13]
    }
}


Weiterleitungen

Aktionen können mithilfe der Controller-Methode redirect() umgeleitet werden. Beispiel:

class OverviewController {

    def login() {}

    def find() {
        if (!session.user)
            redirect(action: 'login')
            return
        }
        ...
    }
}


Auf welche Seite redirect() umleitet, kann man Grails auf mehrere Weisen mitteilen:

  • Der Name der Aktion (Sowie den namens des Controllers, falls sich die Aktion in einem anderen Controller befindet)
// Weiterleitung zur "index()"-Aktion im "home"-Controller
redirect(controller: 'home', action: 'index')
  • URI zu einer Resource relativ vom Kontext-Pfad:
// Weiterleitung zu einer explizit definierten URI
redirect(uri: "/login.html")
  • Eine Komplette URL:
// Weiterleitung zu einer explizit definierten und vollständige URL
redirect(url: "http://grails.org")
  • Domain-Klassen-Instanz: (Grails konstruiert einen Link unter Verwendung der Domänenklassen-ID, falls vorhanden.)
// Weiterleitung zur Seite für die Domainklassen-Instanz
Book book = ... // ...Domainklassen-Instanz generieren/kriegen...
redirect book

Parameter können von einer zu der anderen Aktion mithilfe des params-Arguments übergeben werden.

Diese Daten befinden sich dann auch im params-Scope-Objekt der durch den Redirects resultierenden Anfrage der Aktion myaction.

redirect(action: 'myaction', params: [myparam: "myvalue"])
redirect(action: 'myaction', params: params) // Da sowohl der "params"-Parameter als auch der "params"-Controller-Scope beides Map's sind, ist sowas auch möglich

Mithilfe von fragment kann der Hash-Abteil der URL angegeben werden: (Im Standard URL-Mapping würde die folgende Methode in eine Weiterleitung auf /myapp/test/show#profile resultieren)

redirect(controller: "test", action: "show", fragment: "profile")


Aktionen aneinander-reihen

chain() fungiert ähnlich wie redirect(), indem es auf eine andere Aktion springt und diese ausführt.
Im Gegensatz zur Redirection erlaubt Chaining es uns das Model beizubehalten und Model-Daten zusätzlich zu übergeben.

Beispiel:

class ExampleChainController {

    def first() {
        chain(action: second, model: [one: 1])
    }

    def second () {
        chain(action: third, model: [two: 2])
    }

    def third() {
        [three: 3]
    }
}

Resultiert beim Aufrufen von "/examplechain/first" in das Model

[one: 1, two: 2, three: 3]


Auf das Modell kann in nachfolgenden Controller-Aktionen in der Kette über die chainModel-Map zugegriffen werden. Diese dynamische Eigenschaft ist nur in Aktionen vorhanden, die auf den Aufruf der Kettenmethode folgen:

class ChainController {

    def nextInChain() {
        def model = chainModel.myModel
        ...
    }
}


Wie bei der Weiterleitung kann man auch hier weitere Daten in das params-Scope einspeisen:

chain(action: "action1", model: [one: 1], params: [myparam: "param1"])


Die render()-Methode

Die render()-Methode ist sehr flexibel und kann viele Argumente entgegennehmen (Vom anzeigen eines einfachen Textes bis hin zur render eines Views/Templates.).

// Simplen Text rendern
render "some text"

// Rendert Text für einen bestimmten Inhaltstyp/eine bestimmte Codierung
render(text: "<xml>some xml</xml>", contentType: "text/xml", encoding: "UTF-8")

// Rendern einer Vorlage für die Antwort für das angegebene Modell
def theShining = new Book(title: 'The Shining', author: 'Stephen King')
render(template: "book", model: [book: theShining])

// Rendert jedes Elements in der Sammlung unter Verwendung der angegebenen Vorlage
render(template: "book", collection: [b1, b2, b3])

// eine Vorlage für die Antwort für die angegebene Bean rendern
def theShining = new Book(title: 'The Shining', author: 'Stephen King')
render(template: "book", bean: theShining)

//! Rendern der Ansicht mit dem angegebenen Modell
def theShining = new Book(title: 'The Shining', author: 'Stephen King')
render(view: "viewName", model: [book: theShining])

// Rendern der Ansicht mit dem Controller als Modell
render(view: "viewName")

// Eigenen Markup zur Response hinzurendern
render {
    div(id: "myDiv", "some text inside the div")
}

// etwas XML-Markup für die Antwort rendern
render(contentType: "text/xml") {
    books {
         for (b in books) {
             book(title: b.title, author: b.author)
         }
    }
}

//! Rendern einer JSON ( http://www.json.org ) Antwort mit dem Builder-Attribut:
render(contentType: "application/json") {
    book(title: b.title, author: b.author)
}

//! Rendern mit Statuscode
render(status: 503, text: 'Failed to update book ${b.id}')

//! Rendern einer Datei
render(file: new File(absolutePath), fileName: "book.pdf")

Mit JSON antworten

Die respond-Methode ist der bevorzugte Weg zur Rückgabe von JSON und integriert sich mit Inhaltsverhandlung und JSON Views.

Die respond-Methode bietet inhaltliche Verhandlungsstrategien, um auf intelligente Weise eine angemessene Antwort für den jeweiligen Client zu produzieren.

Beispiel:

package example

class BookController {
    def index() {
        respond Book.list()
    }
}

Die respond-Methode sieht folgende Schritte vor:

  • Falls der Accept-Header vorhanden ist (z.B. application/json), wird dieser verwendet
  • Wenn die Dateierweiterung der URI (z.B. /books.json) ein Format enthält, das in der Eigenschaft grails.mime.types (grails-app/conf/application.yml) definiert ist, verwendet es den in der Konfiguration definierten Medientyp

Die respond-Methode sucht dann in der RendererRegistry nach einem geeigneten Renderer für das Objekt und den berechneten Medientyp.


Grails enthält eine Reihe von vorkonfigurierten Renderer-Implementierungen, die Standarddarstellungen von JSON-Antworten für das übergebene Argument erzeugen.

  • Wenn Sie beispielsweise die URI /book.json aufrufen, werden JSON-Antworten wie folgt erzeugt:
[
    {id:1,"title":"The Stand"},
    {id:2,"title":"Shining"}
]


Die Priorität von Medientypen bestimmen

Wenn man einen Controller definiert, gibt es standardmäßig keine Priorität in Bezug darauf, welches Format an den Client zurückgeschickt wird, und Grails geht davon aus, dass man HTML als Antworttyp verwenden möchten.

Wenn seine Anwendung jedoch in erster Linie eine API ist, dann kann man die Priorität mit der Eigenschaft respondFormats festlegen:

package example

class BookController {
    static responseFormats = ['json', 'html']
    def index() {
        respond Book.list()
    }
}

Im obigen Beispiel antwortet Grails standardmäßig mit json, wenn der Medientyp, mit dem geantwortet werden soll, nicht aus dem Accept-Header oder der Dateierweiterung berechnet werden kann.


Views zum ausgeben von JSON benutzen

Wenn man eine Ansicht definiert (entweder GSP- oder eine JSON) rendert Grails bei Verwendung der respond-Methode eine Ansicht, indem er aus dem zur Antwort übergebenen Argument ein Modell berechnet.

Wenn man z.B. in der vorherigen Auflistung die Ansichten grails-app/views/index.gson und grails-app/views/index.gsp definiert, werden diese verwendet, wenn der Client die Medientypen application/json bzw. text/html anfordert. Auf diese Weise kann ein einziges Backend definiert werden, das in der Lage ist, Antworten an einen Webbrowser zu senden oder die API Ihrer Anwendung zu repräsentieren.


Die folgende Tabelle fasst diese Konventionen zusammen:

Beispiel Argument-Typ Name der berechneten Model-Variable
respond Book.list() java.util.List bookList
respond( [] ) java.util.List emptyList
respond Book.get(1) example.Book book
respond( [1,2] ) java.util.List integerList
respond( [1,2] as Set ) java.util.Set integerSet
respond( [1,2] as Integer[] ) Integer[] integerArray

Mit dieser Konvention kann man auf das übergebene Argument verweisen, um aus der View zu antworten:

  • Wenn Book.list() eine leere Liste zurückgibt, würde der Name der Modellvariable in emptyList übersetzt werden. Dies ist beabsichtigt, und man sollte in der Ansicht einen Standardwert angeben, wenn keine Modellvariable angegeben ist, wie z.B. im folgendem Beispiel:
//grails-app/views/book/index.gson:

// Wenn die in der respond-Methode übergebe Liste leer wäre, und wir diese Zeile nicht hätten, würde die Variable nicht existieren. (Sondern nur eine Variable namens 'emptyList')
// Falls die Variable nicht existiert erstellen wir Sie hiermit und setzen Ihren Wert direkt (Leere Liste, '[]').
@Field List<Book> bookList = []

json bookList, { Book book ->
    title book.title
}

Modell-Variablen-Namen selbst bestimmen

Es gibt Fälle, in denen man vielleicht expliziter sein und den Namen der Modellvariablen selbst kontrollieren möchten.

Wenn man z.B. eine Domänen-Vererbungs-Hierarchie hat, in der ein Aufruf von list() möglicherweise verschiedene Unterklassen zurückgibt, die auf automatischer Berechnung beruhen, ist dies möglicherweise nicht zuverlässig.

  • In diesem Fall sollte man das Modell direkt mit Hilfe von Antwort und einem Map-Argument übergeben:
respond bookList: Book.list()

Modell selbst erweitern

Wenn man das berechnete Modell einfach nur erweitern möchten, können Sie dies tun, indem man zudem ein model-Argument mit-übergibt. Das folgende Beispiel würde gerendert so aussehen: [bookList:books, bookCount:totalBooks]

respond Book.list(), [model: [bookCount: Book.count()]]


Die render-Methode zur Ausgabe von JSON verwenden

Die Render-Methode kann auch zur Ausgabe von JSON verwendet werden, sollte aber nur für einfache Fälle verwendet werden, die die Erstellung einer JSON-Ansicht nicht rechtfertigen: (Die Logik sollte generell so-gut-wie möglich aus den Controller-Klassen extrahiert werden, und z.B. der View-Layer mithilfe von JSON-Views überlassen werden)

// Aktion
def list() {

    def results = Book.list()

    render(contentType: "application/json") {
        books(results) { Book b ->
            title b.title
        }
    }
}

Beispiel-Render:

[
    {"title":"The Stand"},
    {"title":"Shining"}
]



Data Binding

Datenbindung ist der Akt des "Bindens" eingehender Anforderungsparameter an die Eigenschaften eines Objekts oder eines ganzen Graphen von Objekten.

  • Bei der Datenbindung sollten alle notwendigen Typkonvertierungen vorgenommen werden, da Anforderungsparameter, die typischerweise durch eine Formularübertragung geliefert werden, immer Zeichenketten (Strings) sind, während die Eigenschaften eines Groovy- oder Java-Objektes dies möglicherweise nicht sind.


Der Datenbinder ist in der Lage, Werte in einer Map zu konvertieren und Eigenschaften eines Objekts zuzuordnen. Der Datenbinder ordnet Einträge in der Map den Eigenschaften des Objekts zu, indem er die Schlüssel in der Map verwendet, deren Werte den Eigenschaftsnamen des Objekts entsprechen. Der folgende Code veranschaulicht die Grundlagen:

// Klasse 
class Person {
    String firstName
    String lastName
    Integer age
}

// Bindung
def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63]

def person = new Person(bindingMap)

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63


Um die Eigenschaften eines Domänenobjekts zu aktualisieren, kann man der Domänenklassen-Eigenschaft propertieseine Map zuweisen:

def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63]

def person = Person.get(someId)
person.properties = bindingMap

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63


Mit Unterklassen

Der Binder kann mit Hilfe von verschachtelten Maps ein vollständiges Diagramm von Objekten füllen.

// Klassen
class Person {
    String firstName
    String lastName
    Integer age
    Address homeAddress
}

class Address {
    String county
    String country
}

// Bindung
def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63, homeAddress: [county: 'Surrey', country: 'England'] ]

def person = new Person(bindingMap)

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63
assert person.homeAddress.county == 'Surrey'
assert person.homeAddress.country == 'England'

Mit Arrays

Auch Arrays sind möglich:

Der Binder kann auch Collections und Maps füllen und aktualisieren. Der folgende Code zeigt ein einfaches Beispiel für das Auffüllen einer Liste von Objekten in einer Domänenklasse: (Dieser Code würde auf dieselbe Weise funktionieren, wenn albums ein Array statt einer List wäre.)

// Klassen
class Band {
    String name
    static hasMany = [albums: Album] // One-To-Many Relation von Band-zu-Album (Für Datenbank-Relation wichtige Angabe!)
    List albums 
}

class Album {
    String title
    Integer numberOfTracks
}

// Bindung
def bindingMap = [name: 'Genesis',
                  'albums[0]': [title: 'Foxtrot', numberOfTracks: 6],
                  'albums[1]': [title: 'Nursery Cryme', numberOfTracks: 7]]

def band = new Band(bindingMap)

assert band.name == 'Genesis'
assert band.albums.size() == 2
assert band.albums[0].title == 'Foxtrot'
assert band.albums[0].numberOfTracks == 6
assert band.albums[1].title == 'Nursery Cryme'
assert band.albums[1].numberOfTracks == 7

Beachte:

Die Struktur einer Map die an ein Set gebunden wird ist dieselbe wie die einer Map die an eine Liste gebunden wird. Aber da ein Set ungeordnet ist, müssen die Indexe der Elemente in der Map nicht unbedingt mit denen des Sets übereinstimmen.

Wenn im obigen Codebeispiel albums ein Set statt einer List wäre, könnte die BindungsMap genau gleich aussehen - aber es könnte auch geschehen dass "Foxtrot" an zweiter Stelle im Set erscheint.

Wenn man existierende Elemente in einem Set mithilfe iner Map-Datenbindung realisieren möchte, müssen die Indexe wie im folgenden Beispiel an den derzeitigen Stand des Set's angepasst werden:

/*
 * The value of the indexes 0 and 1 in albums[0] and albums[1] are arbitrary
 * values that can be anything as long as they are unique within the Map.
 * They do not correspond to the order of elements in albums because albums
 * is a Set.
 */
def bindingMap = ['albums[0]': [id: 9, title: 'The Lamb Lies Down On Broadway']
                  'albums[1]': [id: 4, title: 'Selling England By The Pound']]

def band = Band.get(someBandId)

/*
 * This will find the Album in albums that has an id of 9 and will set its title
 * to 'The Lamb Lies Down On Broadway' and will find the Album in albums that has
 * an id of 4 and set its title to 'Selling England By The Pound'.  In both
 * cases if the Album cannot be found in albums then the album will be retrieved
 * from the database by id, the Album will be added to albums and will be updated
 * with the values described above.  If a Album with the specified id cannot be
 * found in the database, then a binding error will be created and associated
 * with the band object.  More on binding errors later.
 */
band.properties = bindingMap


Mit Maps

// Klassen
class Album {
    String title
    static hasMany = [players: Player] // One-To-Many Relation von Album-zu-Player (Für Datenbank-Relation wichtige Angabe!)
    Map players 
}

class Player {
    String name
}

// Bindung
def bindingMap = [title: 'The Lamb Lies Down On Broadway',
                  'players[guitar]': [name: 'Steve Hackett'],
                  'players[vocals]': [name: 'Peter Gabriel'],
                  'players[keyboards]': [name: 'Tony Banks']]

def album = new Album(bindingMap)

assert album.title == 'The Lamb Lies Down On Broadway'
assert album.players.size() == 3
assert album.players.guitar.name == 'Steve Hackett'
assert album.players.vocals.name == 'Peter Gabriel'
assert album.players.keyboards.name == 'Tony Banks'


Für weitere Informationen konsultiert man am besten die Offizielle Dokumentationsseite zu Data Binding in Grails. (Sehr komplexes Thema)


Datei-Uploads

Grails unterstützt Datei-Uploads über die MultipartHttpServletRequest-Schnittstelle von Spring. Der erste Schritt für das Hochladen von Dateien ist die Erstellung eines multipart-Formulars wie dieses:

Upload Form: <br />
<g:uploadForm action="upload">
   <input type="file" name="myFile" />
   <input type="submit" />
</g:uploadForm>

Das uploadForm-Tag fügt bequem das enctype="multipart/form-data"-Attribut zum Standard <g:form>-Tag hinzu.

Es gibt dann eine Reihe von Möglichkeiten, den Datei-Upload zu handhaben. Eine besteht darin, direkt mit der Spring MultipartFile-Instanz zu arbeiten:


Es gibt dann eine Reihe von Möglichkeiten, den Datei-Upload zu handhaben. Eine besteht darin, direkt mit der Spring MultipartFile-Instanz zu arbeiten:

  • Dies ist praktisch, um Übertragungen an andere Ziele durchzuführen und die Datei direkt zu manipulieren, da man mit der MultipartFile-Schnittstelle einen InputStream usw. erhalten kann.
def upload() {
    def f = request.getFile('myFile')
    if (f.empty) {
        flash.message = 'file cannot be empty'
        render(view: 'uploadForm')
        return
    }

    f.transferTo(new File('/some/local/dir/myfile.txt'))
    response.sendError(200, 'Done')
}


Datei-Upload mithilfe von Data-Binding

Datei-Uploads können auch mit Datenbindung durchgeführt werden. Betrachte z.B. diese Domänenklasse:

// Domänenklasse
class Image {
    byte[] myFile

    static constraints = {
        // Limit upload file size to 2MB
        // Wichtig: Würde diese Eigenschaft nicht gesetzt werden, könnte die Datenbank-Spalte zu klein sein. (MySQL rechnet mit einer Standard Blob-Größe von 255-bytes für byte-Arrays)
        myFile maxSize: 1024 * 1024 * 2
    }
}

// Datenbindung
def img = new Image(params)


Upload-Größen-Limit von Grails

Grails Standard-Wert für die maximale Dateigröße liegt bei 128KB. Wenn dieses Limit überschritten wird taucht folgender fehler auf:

org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException

Dieses Limit kann man in seiner application.yml folgendermaßen anpassen:

grails:
    controllers:
        upload:
            maxFileSize: 2000000 # Die maximal zulässige Größe für hochgeladene Dateien.
            maxRequestSize: 2100000 # Die maximal zulässige Größe für Mehrteil-/Formulardatenanfragen.

Diese Begrenzungen existieren um Denial-of-Service Attacken entgegen-zu-wirken und die Perfomanz der Applikation zu steigern. Siehe auch OWASP recommendations - Unrestricted File Upload


URL-Mappings

In der Dokumentation wird immer von der standardmäßigen URL-Mapping-Konfiguration ausgegangen, die wie folgt aussieht:/controller/action/id.
Diese Konvention ist jedoch nicht fest mit Grails verdrahtet und wird in der Tat durch eine URL-Mappings-Klasse gesteuert, die sich unter grails-app/controllers/mypackage/UrlMappings.groovy befindet. Diese Klasse hat eine einzelne Eigenschaft Namens mappings in der alle Regeln definiert werden.


Beispiel-Mapping (URI /product auf die Aktion list() vom ProductController leiten):

"/product"(controller: "product", action: "list")

Alternative-Syntax (Closure/Block-Code):

"/product" {
    controller = "product"
    action = "list"
}

Beispiel-Mapping (URI /product auf die Standard-Aktion vom ProductController leiten):

"/product"(controller: "product")


Nested URL-Mapping

Wenn man Zuordnungen hat, die alle unter einen bestimmten Pfad fallen, kann man diese Zuordnungen mit der group-Methode gruppieren:

group "/product", {
    "/apple"(controller:"product", id:"apple")
    "/htc"(controller:"product", id:"htc")
}
// Nested Grouping
group "/store", {
    group "/product", {
        "/$id"(controller:"product")
    }
}


Weiterleitung mithilfe von URL-Mappings

Seit Grails 2.3 ist es möglich, URL-Mappings zu definieren, die eine Weiterleitung angeben. Wenn eine URL-Zuordnung eine Weiterleitung spezifiziert, wird jedes Mal, wenn diese Zuordnung mit einer eingehenden Anfrage übereinstimmt, eine Weiterleitung mit den von der Zuordnung gelieferten Informationen initiiert.

Wenn eine URL-Zuordnung eine Weiterleitung angibt, muss die Zuordnung entweder einen String liefern, der eine URI darstellt, zu der umgeleitet werden soll, oder eine Zuordnung, die das Ziel der Umleitung darstellt. Diese Map ist genau wie die Map strukturiert, die als Argument an die redirect-Methode in einem Controller übergeben werden kann.

"/viewBooks"(redirect: '/books/list')
"/viewAuthors"(redirect: [controller: 'author', action: 'list'])
"/viewPublishers"(redirect: [controller: 'publisher', action: 'list', permanent: true])

Anforderungsparameter, die Teil der ursprünglichen Anforderung waren, werden standardmäßig nicht in die Weiterleitung einbezogen. Um sie aufzunehmen, ist es notwendig, den Parameter keepParamsWhenRedirect: true hinzuzufügen.

"/viewBooks"(redirect: [uri: '/books/list', keepParamsWhenRedirect: true])
"/viewAuthors"(redirect: [controller: 'author', action: 'list', keepParamsWhenRedirect: true])
"/viewPublishers"(redirect: [controller: 'publisher', action: 'list', permanent: true, keepParamsWhenRedirect: true])

Integrierte Variablen im Mapping

Im vorigen Abschnitt wurde gezeigt, wie einfache URLs mit konkreten "Token" abgebildet werden können.

  • Bei der URL-Abbildung sind "Token" die Zeichenfolge zwischen jedem Schrägstrich, '/'.
  • Ein konkreter Token ist ein Token, das gut definiert ist, wie z.B. /product.

In vielen Fällen weiß man jedoch erst zur Laufzeit, welchen Wert ein bestimmtes Token haben wird. In diesem Fall kann man zum Beispiel variable Platzhalter innerhalb der URL verwenden:

static mappings = {
  "/product/$id"(controller: "product")
}

In diesem Fall wird Grails durch Einbetten der $id-Variablen als zweites Token das zweite Token automatisch in einen Parameter (verfügbar über das params-Scope-Objekt) namens id abbilden.

class ProductController {
     def index() { render params.id }
}

Natürlich kann man auch mehrere Variablen verwenden, wie bei diesem Beispiel eines Blogs:

"/$blog/$year/$month/$day/$id"(controller: "blog", action: "show")

Optionale Variablen im Mappings

Indem man ein Fragezeichen am ende des Variabl-Namens anhängt schlägt das URL-Mapping auch zu, wenn der Nutzer diese nicht angegeben hat. Beispiel-Mapping:

"/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")

Das obige Beispiel-Mapping schlägt bei allen folgenden URLs zu:

/graemerocher/2007/01/10/my_funky_blog_entry
/graemerocher/2007/01/10
/graemerocher/2007/01
/graemerocher/2007
/graemerocher

Optionale Datei-Erweiterungen in Mappings

Wenn man die Datei-Erweiterung auch ganz einfach in einer params-Scope Variable haben möchte, kann man dies durch das anhängen von der Optionalen Variable (.$format)?:

// Mapping
"/$controller/$action?/$id?(.$format)?"()

// Controller
def index() {
    render "extension is ${response.format}"
}

Selbst eingepflanzte Mapping-Variablen

Man kann auch selber mithilfe von URL-Mapping Variablen in das params-Scope reinschreiben.

"/holiday/win" {
     id = "Marrakech"
     year = 2007
}

Dynamisch aufgelöste Mapping-Variablen

Die oben-genannten, selbst setzbaren statischen Variablen sind sehr nützlich.
Aber manchmal soll sich der Wert der Variable je nach Anfrage anders verhalten können. Dies kann man erzielen, indem man einen eigenen Block zur Variable hinzufügt.

"/holiday/win" {
     id = { params.id }
     isEligible = { session.user != null } // must be logged in
}

Im obrigen Fall wird der Code innerhalb der Blöcke erst dann evaluiert, wenn das URL-Matching zugeschlagen wurde, und kann dadurch "mit Logik versehen werden".


URL-Mapping Constraints (Einschränkungen des Typens einer Variable)

Nehmen wir folgendes Beispiel:

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

Dieses Mapping funktioniert für:

/graemerocher/2007/01/10/my_funky_blog_entry

Aber auch für Werte, die wir vllt. nicht vorgesehen haben:

/graemerocher/not_a_year/not_a_month/not_a_day/my_funky_blog_entry

Dies wäre sehr problematisch und man müsste eine (komplexe) eigene Logik reincoden, damit bestimmte Variablen nur einen bestimmten Wert haben dürfen.
Zum Glück bieten uns URL-Mappings einen einfachen Weg für diese Einschränkung/Validation:

"/$blog/$year?/$month?/$day?/$id?" {
     controller = "blog"
     action = "show"
     constraints {
          year(matches:/\\\d{4}/)
          month(matches:/\\\d{2}/)
          day(matches:/\\\d{2}/)
     }
}


Error-Code Mapping

Mit Grails kann man HTTP-Antwortcodes auch Controllern, Aktionen oder Ansichten zuordnen. Hierzu verwendet man einfach einen Methodennamen, der zu dem Antwort-Code passt, an dem man interessiert ist:

"403"(controller: "errors", action: "forbidden")
"404"(controller: "errors", action: "notFound")
"500"(controller: "errors", action: "serverError")

Man kann auch auftauchende Exceptions auf eine eigene Seite mappen:

"500"(controller: "errors", action: "illegalArgument", exception: IllegalArgumentException)


Nur bestimmte HTTP-Methoden Mappen

URL-Mappings können auch so konfiguriert werden, dass sie basierend auf der HTTP-Methode (GET, POST, PUT oder DELETE) abgebildet werden. Dies ist sehr nützlich für RESTful-APIs und zur Einschränkung von Mappings auf der Basis der HTTP-Methode.

static mappings = {
   "/product/$id"(controller:"product", action: "update", method: "PUT")
}


Wildcards im Mapping

Grails' URL-Mappings-Mechanismus unterstützt auch Wildcard-Mappings.

Dieses Mapping stimmt mit allen Pfaden wie /image/logo.jpg überein.

static mappings = {
    "/images/*.jpg"(controller: "image")
    // Natürlich kann man den gleichen Effekt mit einer Variablen erreichen:
    "/images/$name.jpg"(controller: "image")
}

Man kann jedoch auch doppelte Platzhalter verwenden, um mehr als eine Ebene darunter abzugleichen:

static mappings = {
    // will match /image/logo.jpg as well as /image/other/logo.jpg 
    "/images/**.jpg"(controller: "image")
}

Noch besser: Durch die Kombination dieser 2 Prinzipien kann man eine "doppelte Platzhalter-Variable" verwenden:

// Mapping
static mappings = {
    // will match /image/logo.jpg and /image/other/logo.jpg
    "/images/$name**.jpg"(controller: "image")
}

// Controller
def name = params.name
println name // prints "logo" or "other/logo"


RESTful-Mapping

Seit Grails 2.3 ist es möglich, RESTful-URL-Mappings zu erstellen, die nach Konvention auf Controller abbilden. Die Syntax dafür lautet wie folgt:

"/books"(resources:'book')

Man definiert einen Basis-URI und den Namen des Controllers, der zugeordnet werden soll, mit dem resources-Parameter. Das obige Mapping führt zu den folgenden URLs:

HTTP Methode URI Grails Aktion
GET /books index
GET /books/create create
POST /books save
GET /books/${id} show
GET /books/${id}/edit edit
PUT /books/${id} update
DELETE /books/${id} delete

Wenn man sich nicht sicher ist, welche Zuordnung für einen Fall erzeugt wird, führt man einfach den Befehl $ grails url-mappings-report aus.

Wenn man eine der genannten URL-Mappings ein- oder ausschließen möchten, kann man dies mit dem includes- oder excludes-Parameter tun, der den Namen der Grails-Aktion zum Ein- oder Ausschließen akzeptiert:

"/books"(resources:'book', excludes:['delete', 'update'])
// or
"/books"(resources:'book', includes:['index', 'show'])

Explizites REST-Mapping

(Ab Grails 3.1) Wenn man es vorzieht, sich bei der Definition seines Mappings nicht auf ein Ressourcen-Mapping zu verlassen, können Sie jedem URL-Mapping den Namen der HTTP-Methode (in Kleinbuchstaben) voranstellen, um die HTTP-Methode anzugeben, auf die es sich bezieht. Die folgende URL-Abbildung:

"/books"(resources:'book')

ist gleich wie

get "/books"(controller:"book", action:"index")
get "/books/create"(controller:"book", action:"create")
post "/books"(controller:"book", action:"save")
get "/books/$id"(controller:"book", action:"show")
get "/books/$id/edit"(controller:"book", action:"edit")
put "/books/$id"(controller:"book", action:"update")
delete "/books/$id"(controller:"book", action:"delete")

Single Resources

Eine Single Ressource ist eine Ressource, für die es nur eine (möglicherweise pro Benutzer) im System gibt. Man kann eine einzelne Ressource mit dem single Parameter anlegen (im Gegensatz zu resources):

"/book"(single:'book')

Daraus ergeben sich die folgenden URL-Mappings: (Der Hauptunterschied besteht darin, dass die Id nicht in der URL-Zuordnung enthalten ist.)

HTTP Method URI Grails Aktion
GET /book/create create
POST /book save
GET /book show
GET /book/edit edit
PUT /book update
DELETE /book delete

Nested URL-Resource-Mapping

Man kann Ressourcen-Zuordnungen verschachteln, um untergeordnete Ressourcen zu erzeugen. Zum Beispiel resultiert die folgende Definition dazu,..:

"/books"(resources:'book') {
  "/authors"(resources:"author")
}

...dass die folgende URL verfügbar ist:

HTTP Method URL Grails Action
GET /books/${bookId}/authors index
GET /books/${bookId}/authors/create create
POST /books/${bookId}/authors save
GET /books/${bookId}/authors/${id} show
GET /books/${bookId}/authors/edit/${id} edit
PUT /books/${bookId}/authors/${id} update
DELETE /books/${bookId}/authors/${id} delete

Linking to REST-Mappings from GSP

Man kann auf jedes URL-Mapping verlinken, das mit dem von Grails bereitgestellten g:link-Tag erstellt wurde, indem man einfach auf den Controller und die Aktion verweist, auf die verlinkt werden soll:

<g:link controller="book" action="index">My Link</g:link>

Als Annehmlichkeit kann man auch eine Domäneninstanz an das Ressourcenattribut des Link-Tags übergeben:

<!-- Hier wird automatisch der richtige Link erzeugt (in diesem Fall "/books/1" für eine ID von "1"). -->
<g:link resource="${book}">My Link</g:link>


Im Falle der verschachtelten Ressourcen ist ein wenig anders, da sie in der Regel zwei Identifikatoren erfordern (die ID der Ressource und diejenige, in der sie verschachtelt ist). Zum Beispiel das Mapping:

"/books"(resources:'book') {
  "/authors"(resources:"author")
}

Wenn man einen Link zur Show-Aktion des Author-Controllers herstellen wollen würde, würde man es so schreiben müssen:

<!-- Results in /books/1/authors/2 -->
<g:link controller="author" action="show" method="GET" params="[bookId:1]" id="2">The Author</g:link>

Um dies jedoch prägnanter zu machen, gibt es ein resource-Attribut zum Link-Tag, das stattdessen verwendet werden kann:

  • Das resource-Attribut akzeptiert einen durch einen Schrägstrich getrennten Pfad zur Ressource (in diesem Fall "book/author"). Die Attribute des Tags können zur Angabe des notwendigen bookId-Parameters verwendet werden.
<!-- Results in /books/1/authors/2 -->
<g:link resource="book/author" action="show" bookId="1" id="2">My Link</g:link>


CORS

Spring Boot bietet CORS-Unterstützung out-of-the-box, aber es ist schwierig, es in einer Grails-Anwendung zu konfigurieren, da URLMapping anstelle von Annotationing zur Definition von URLs verwendet werden.

Beginnend mit Grails 3.2.1 wurde eine Möglichkeit zur Konfiguration von CORS hinzugefügt, die in einer Grails-Anwendung sinnvoll ist.

Einmal aktiviert, ist die Standardeinstellung "weit offen".

# application.yml
grails:
    cors:
        enabled: true

Das ergibt eine Zuordnung zu allen urls (/**) mit:

allowedOrigins ['*']
allowedMethods ['*']
allowedHeaders ['*']
exposedHeaders null
maxAge 1800
allowCredentials true

Einige dieser Einstellungen kommen direkt von Spring Boot und können sich in zukünftigen Versionen ändern. Siehe Spring CORS Configuration Documentation

Alle diese Einstellungen können leicht außer Kraft gesetzt werden.

# application.yml
grails:
    cors:
        enabled: true
        allowedOrigins:
            - http://localhost:5000


Man kann auch Einstellungen für mehrere URLs definieren:

  • Beachte: Die Angabe von mindestens einem Mapping deaktiviert die Erstellung des globalen Mappings (/**). Wenn man diese Einstellung beibehalten möchten, sollte man diese zusammen mit den anderen Mappings explizit angeben
# application.yml
grails:
    cors:
        enabled: true
        allowedHeaders:
            - Content-Type
        mappings:
            /api/**:
                allowedOrigins:
                    - http://localhost:5000
                # Other configurations not specified default to the global config

Die obigen Einstellungen führen zu einer einzigen Mapping von /api/** mit den folgenden Einstellungen:

allowedOrigins ['http://localhost:5000']
allowedMethods ['*']
allowedHeaders ['Content-Type']
exposedHeaders null
maxAge 1800
allowCredentials true

Wenn man keine der Standardeinstellungen außer Kraft setzen will, sondern nur URLs angeben möchten, kann man dies wie in diesem Beispiel tun:

# application.yml
grails:
    cors:
        enabled: true
        mappings:
            /api/**: inherit


Interceptor

Abfänger vs Filters

Das neue Abfang-Konzept in Grails 3.0 ist in vielerlei Hinsicht dem Filter-Konzept überlegen, vor allem können die Inteceptoren die CompileStatic-Annotation von Groovy verwenden, um die Leistung zu optimieren (was oft kritisch ist, da die Inteceptoren bei jeder Anfrage ausgeführt werden können).

Filters werden aus Gründen der Abwärtskompatibilität weiterhin unterstützt, gelten aber als veraltet.

Request-Matching von Interceptoren

Die Konvention von Grails sieht standardmäßig vor, dass ein Interceptor X alle Anfragen vom Controller mit gleichen Namen X abfängt.

Mithilfe der Interceptor-API können jedoch auch eigene Matching-Regeln festgelegt werden. Hierzu gibt es die Methoden match und matchAll, die beide eine Instanz von Matcher zurückgeben, mit dem man den Interceptor konfigurieren kann.


Zum Beispiel wird der folgende Interceptor sich mit allen Anfragen beschäftigen, die nicht an den login-Controller gerichtet sind:

class AuthInterceptor {
  AuthInterceptor() {
    matchAll()
    .excludes(controller:"login")
  }

  boolean before() {
    // perform authentication
  }
}


Man kann den Abgleich auch mit einem benannten Argument durchführen:

class LoggingInterceptor {
  LoggingInterceptor() {
    match(controller:"book", action:"show") // using strings
    match(controller: ~/(author|publisher)/) // using regex
  }

  boolean before() {
    ...
  }
}

Man kann eine beliebige Anzahl von Matchern verwenden. Diese werden in der Reihenfolge ausgeführt, in der sie definiert wurden.

Zum Beispiel wird der obige Interceptor für alle der folgenden Punkte passen:

  • wenn die show-Aktion vom BookController aufgerufen wird
  • wenn (jegliche Aktion vom) AuthorController oder PublisherController aufgerufen wird


Alle genannten Argumente mit Ausnahme von uri akzeptieren entweder einen String- oder einen Regex-Ausdruck. Das uri-Argument unterstützt einen String-Pfad, der mit dem AntPathMatcher von Spring kompatibel ist. Die möglichen benannten Argumente sind:

  • namespace - Der Namensraum des Controller
  • controller - Der Name des Controllers
  • action - Der Name der Aktion
  • method - Die HTTP-Methode
  • uri - Die URI der Anfrage. Wenn dieses Argument genutzt wird, werden alle anderen Argumente ignoriert und nur dieses Argument genutzt!


Reihenfolge von Interceptoren bestimmen

Interceptoren können durch Definition der order-Eigenschaft geordnet werden, die eine numerische Priorität definiert.

Der Standardwert der order-Eigenschaft ist 0. Die Ausführungsreihenfolge des Interceptors wird bestimmt, indem die Order-Eigenschaft in aufsteigender Richtung sortiert und der niedrigste numerisch geordnete Interceptor zuerst ausgeführt wird.


Die Werte HIGHEST_PRECEDENCE und LOWEST_PRECEDENCE können verwendet werden, um Filter zu definieren, die zuerst bzw. zuletzt ausgeführt werden sollen.

Zu beachten gilt: Wenn man einen Interceptor schreibt der von anderen benutzt werden soll, ist es besser, die HIGHEST_PRECEDENCE und LOWEST_PRECEDENCE zu inkrementieren oder zu dekrementieren, damit andere Interceptoren vor oder nach seinem Interceptor eingefügt werden können:

int order = HIGHEST_PRECEDENCE + 50
// or
int order = LOWEST_PRECEDENCE - 50


Um die berechnete Reihenfolge der Interceptoren herauszufinden, kann logback.groovy ein Debug-Logger hinzufügt werden:

logger 'grails.artefact.Interceptor', DEBUG, ['STDOUT'], false


Die order-Eigenschaft eines Interceptors kann innerhalb grails-app/conf/application.yml auch überschrieben werden:

Man kann die Standardreihenfolge eines Interceptors überschreiben, indem man die Konfiguration der bean-Überschreibung in grails-app/conf/application.yml verwendet:

beans:
  authInterceptor:
    order: 50


Aufbau von Interceptoren

Mit dem Befehl $ grails create-interceptor [Name] kann das Grundgerüst für einen Interceptor in grails-app/controllers erstellt werden.

Jeder Interceptor erbt nach Konvention vom Interceptor-Trait.


Beispiel: ($ grails create-interceptor Test, Kommentare von mir eingefügt)

class TestInterceptor {

    /**
     * Executed before a matched action
     *
     * @return Whether the action should continue and execute
     */
    boolean before() { true }

    /**
     * Executed after the action executes but prior to view rendering
     * The after method can also modify the view or model using the view and model properties respectively.
     *
     * @return True if view rendering should continue, false otherwise
     */
    boolean after() { 
        /* Example of altering the 'model' and 'view':
        model.foo = "bar" // add a new model attribute called 'foo'
        view = 'alternate' // render a different view called 'alternate' 
        */
        true
    }
    
    /**
     * Executed after view rendering completes
     * If an exception occurs, the exception is available using the 'throwable' property of the Interceptor trait. 
     */
    void afterView() {
        //no-op
    }
}


Inhaltsverhandlung (Content Negotiation)

Grails hat eine Unterstützung für Inhaltsverhandlungen eingebaut, bei denen entweder

  • der HTTP-Accept-Header,
  • ein expliziter Anforderungsparameter namens format oder
  • die Erweiterung einer zugeordneten URI verwendet wird.


Mime-Types konfigurieren

Bevor man sich mit Inhaltsverhandlungen befassen kann, muss man Grails mitteilen, welche Inhaltstypen man unterstützen möchten. Standardmäßig wird Grails mit einer Reihe verschiedener Inhaltstypen innerhalb von grails-app/conf/application.yml unter Verwendung der Einstellung grails.mime.types konfiguriert:

grails:
    mime:
        types:
            all: '*/*'
            atom: application/atom+xml
            css: text/css
            csv: text/csv
            form: application/x-www-form-urlencoded
            html:
              - text/html
              - application/xhtml+xml
            js: text/javascript
            json:
              - application/json
              - text/json
            multipartForm: multipart/form-data
            rss: application/rss+xml
            text: text/plain
            hal:
              - application/hal+json
              - application/hal+xml
            xml:
              - text/xml
              - application/xml

Das obige Konfigurationsbit erlaubt es Grails, das Format einer Anfrage, die entweder die Medientypen text/xml oder application/xml enthält, als einfach xml zu erkennen. Man kann auch eigenen Typen hinzufügen, indem man einfach neue Einträge in die grails.mime.types-Map einfügt. Der erste Eintrag ist das Standardformat.


Inhaltsverhandlung unter Verwendung des format Anfrageparameters

Angenommen, eine Controller-Aktion kann eine Ressource in einer Vielzahl von Formaten zurückgeben: HTML, XML und JSON. Welches Format wird der Client erhalten?

  • Der einfachste und zuverlässigste Weg für den Client, dies zu kontrollieren, ist über einen URL-Parameter namens format:
http://my.domain.org/books?format=xml


Man könnte diesen Parameter auch in der URL-Mappings-Definition hart-definieren...

"/book/list"(controller:"book", action:"list") {
    format = "xml"
}

.., aber man kann auch die Controller-spezifische Methode withFormat() verwenden:

import grails.converters.JSON
import grails.converters.XML

class BookController {

    def list() {
        def books = Book.list()

        withFormat {
            html bookList: books
            json { render books as JSON }
            xml { render books as XML }
            '*' { render books as JSON }
        }
    }
}

In diesem Beispiel führt Grails nur den Block innerhalb withFormat() aus, der dem angeforderten Inhaltstyp entspricht. Wenn das bevorzugte Format also html ist, wird Grails nur den html()-Aufruf ausführen.

Jeder 'Block' kann entweder ein Map-Modell für die entsprechende Ansicht (wie wir es für 'html' im obigen Beispiel tun) oder eine Closure sein. Die Closure kann jeden Standard-Aktionscode enthalten, z.B. kann er ein Modell zurückgeben oder Inhalte direkt rendern.

Wenn kein Format explizit übereinstimmt, kann ein * (Wildcard)-Block verwendet werden, um alle anderen Formate zu behandeln.


Es gibt ein spezielles Format, all, das anders behandelt wird als die expliziten Formate. Wenn all angegeben wird (normalerweise geschieht dies über den Accept-Header - siehe unten), dann wird der erste Block von withFormat() ausgeführt, wenn kein * (Platzhalter)-Block vorhanden ist.

Inhaltsverhandlung unter Verwendung des Accept-Headers

Jede eingehende HTTP-Anfrage hat einen speziellen Accept-Header, der definiert, welche Medientypen (oder Mime-Typen) ein Client "akzeptieren" kann. In älteren Browsern ist dies typischerweise der Fall:

*/*

was einfach alles bedeutet. Neuere Browser senden jedoch interessantere Werte wie diesen, der von Firefox gesendet wird:

text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, \
    text/plain;q=0.8, image/png, */*;q=0.5

Dieser spezielle Accept-Header ist nicht hilfreich, da er anzeigt, dass XML das bevorzugte Antwortformat ist, während der Benutzer eigentlich HTML erwartet. Aus diesem Grund ignoriert Grails den Accept-Header bei Browsern standardmäßig. Nicht-Browser-Clients sind jedoch in der Regel spezifischer in ihren Anforderungen und können Accept-Header wie z.B:

application/json

Wie erwähnt, ist die Standardkonfiguration in Grails, den Accept-Header für Browser zu ignorieren. Dies wird durch die Konfigurationseinstellung grails.mime.disable.accept.header.userAgents erreicht, die so konfiguriert ist, dass sie die wichtigsten Rendering-Engines erkennt und deren ACCEPT-Header ignoriert. Dadurch kann die Inhaltsaushandlung von Grails auch für Nicht-Browser-Clients weiterhin funktionieren:

grails:
    mime:
        disable:
            accept:
                header:
                    userAgents:
                        - Gecko
                        - WebKit
                        - Presto
                        - Trident

Anmerkung:

Wenn der "accept"-Header verwendet wird, aber keine registrierten Inhaltstypen enthält, geht Grails davon aus, dass ein defekter Browser die Anfrage stellt und stellt das HTML-Format ein - beachten Sie, dass sich dies von der Funktionsweise der anderen Inhaltsaushandlungsmodi unterscheidet, da diese das "all"-Format aktivieren würden! Ein Accept-Header mit dem Wert */\* resultiert in den Wert all für die format-Eigenschaft.


Inhaltsverhandlung unter Verwendung einer Erweiterung in der URI

Dank der Standard URL-Mapping-Definition..

"/$controller/$action?/$id?(.$format)?"{

..kann das format auch durch das anhängen der gewünschten Erweiterung am Ende der URI erfolgen:

/book/list.xml


Anfrage-Format vs Rückgabe-Format

Ab Grails 2.0 gibt es einen getrennten Begriff des request-Formats und des response-Formats.

  • Das request-Format wird durch den CONTENT_TYPE-Header diktiert und wird normalerweise verwendet, um festzustellen, ob die eingehende Anforderung in XML oder JSON geparst werden kann.
  • Das response-Format verwendet die Dateierweiterung, den Formatparameter oder den ACCEPT-Header, um zu versuchen, dem Client eine geeignete Antwort zu liefern. (Wie bereits mehrmals beschrieben)


Das bei Controllern verfügbare withFormat befasst sich speziell mit dem response-Format. Wenn man eine Logik hinzufügen möchte, die sich mit dem request-Format befasst, kann man dies mit einer separaten withFormat-Methode tun, die im request-Objekt verfügbar ist:

request.withFormat {
    xml {
        // read XML
    }
    json {
        // read JSON
    }
}

Inhaltsverhandlung Testen

Um die Inhaltsaushandlung in einem Unit- oder Integrationstest zu testen (siehe Abschnitt Testen), kann man entweder

  • die Kopfzeilen der eingehenden Anfragen manipulieren:
void testJavascriptOutput() {
    def controller = new TestController()
    controller.request.addHeader "Accept",
              "text/javascript, text/html, application/xml, text/xml, */*"

    controller.testAction()
    assertEquals "alert('hello')", controller.response.contentAsString
}
  • oder den format-Parameter der Anfrage setzen, um einen ähnlichen Effekt auszulösen:
void testJavascriptOutput() {
    def controller = new TestController()
    controller.params.format = 'js'

    controller.testAction()
    assertEquals "alert('hello')", controller.response.contentAsString
}