Groovy als Web-Backend

Aus Jonas Notizen Webseite
Zur Navigation springen Zur Suche springen
Grails Logo

Grails ist ein freies Webframework für die Programmiersprache Groovy. Grails bietet Konzepte wie Scaffolding, automatische Validatoren und Internationalisierung. Grails ist an Ruby on Rails angelehnt und baut auf mehreren etablierten Frameworks wie Spring, Hibernate und SiteMesh auf und verbindet diese mit der Skriptsprache Groovy.


Inhaltsverzeichnis

Einführung

Quellen


Über Grails

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.



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

Es gibt auch noch andere Implementation wie "GORM for MongoDB".

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 obere 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. Dies 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 verdienen die Einstellungen grails.gorm.default.mapping und grails.gorm.default.constraints. Diese definieren die Standard-ORM-Zuordnungen/DSL's und die von jeder Entität verwendeten Standard-Validierungseinschränkungen.


Ändern der Standard-Datenbankzuordnung

Möglicherweise hat man den 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-Validierungseinschränkungen

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

Man kann die Standardbeschränkungen ü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 muss 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.

Betrachte man 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

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 Viele-zu-Eins ohne Kaskadierung

In diesem Fall haben wir eine unidirektionale Viele-zu-Eins-Beziehung von Face-to-Nose.

Beispiel einer Unidirektionalen Viele-zu-Eins 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 {}
Bemerke: Der Fremdschlüssel liegt beim Besitzer (vgl. Eins-zu-Eins mithilfe von hasOne)
Grails-gorm-unidirectional many to one-db schema view.png
Beispiel-Code der Unidirektionalen Viele-zu-Eins 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 myFace = new Face(nose: myNose).save() // Creating a new and valid Face-Instance and saving it
myFace.setNose(new Nose()) // Updating/Editing it
myFace.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 Viele-Zu-Eins mithilfe von belongsTo

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

Beispiel einer Bidirektionalen Viele-zu-Eins 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 Viele-zu-Eins ohne Kaskadierung!
Grails-gorm-unidirectional many to one-db schema view.png
Bemerke: Die Struktur bleibt gleich wie beim unidirektionalen Viele-zu-Eins ohne Kaskadierung!

In diesem Fall verwenden wir die Einstellung belongsTo, um zu sagen, dass Nose zum Face "gehört":

belongsTo definiert eine "Zugehörigkeit zu" -Beziehung, in der die von belongsTo angegebene Klasse das Eigentum an der Beziehung übernimmt. Dadurch wird gesteuert, wie die Aktionen des Speicherns und Löschen kaskadieren. Das genaue Verhalten hängt von der Art der Beziehung ab:

  • Viele-zu-Eins / Eins-zu-Eins: Die Aktionen Speichern und Löschen kaskadieren vom Eigentümer zum Abhängigen (die Klasse mit belongsTo).
  • Eins-zu-viele: Die Aktion des Speichern kaskadiert immer von der "Eins" Seite zur "Viele" Seite. Wenn die "Viele" Seite auch ein belongsTo besitzt, kaskadiert auch das Löschen in diese Richtung gelöscht.
  • Viele-zu-Viele: Nur die Aktion des Speichern kaskadiert vom Eigentümer zum Abhängigen, das Löschen kaskadiert hingegen nie.
Beispiel-Code der Bidirektionalen Viele-zu-Eins 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
Jedoch:
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)


Unidirektionales Viele-Zu-Eins mithilfe von belongsTo

Eine Sache, die Menschen oft verwirrt, ist, dass belongsTo zwei verschiedene Syntaxen unterstützt.

Die oben verwendete Syntax definiert nicht nur eine einfache Kaskadierung zwischen zwei Klassen, sondern fügt auch eine entsprechende Rückrefrenz zum Modell hinzu, wodurch die Beziehung automatisch in eine bidirektionale Beziehung umgewandelt wird.

Um einfach nur das Kaskadierungsverhalten zu definieren (ohne Rückrefrenz auf den "Eigentümer") kann man folgende Syntax benutzen:

Beispiel einer Unidirektionalen Viele-zu-Eins 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
7 }
Bemerke: Die Struktur bleibt gleich wie beim bidirektionalen Many-to-One!
Grails-gorm-unidirectional many to one-db schema view.png
Bemerke: Die Struktur bleibt gleich wie beim bidirektionalen Many-to-One!


Unidirektionales Eins-zu-Eins mithilfe von belongsTo

Beim ersten Beispiel der "Viele-zu-Eins" hat man sich vielleicht gefragt, warum diese Assoziation eine Viele-zu-Eins und keine Eins-zu-Eins ist. Der Grund ist, dass es möglich ist, das man mehrere Instanzen von Face dieselbe Instanz von Nose zuordnen kann. Wenn man diese Zuordnung als echte Eins-zu-Eins-Zuordnung definieren möchten, ist eine unique Einschränkung erforderlich:

Beispiel einer Unidirektionalen Eins-zu-Eins 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 }
3 
4 class Nose {
5     static belongsTo = [face: Face]
6     static constraints = {
7         face unique:true
8     }
9 }


Bidirektionales Eins-zu-Eins mithilfe von hasOne

hasOne definiert eine bidirektionale Eins-zu-Eins-Zuordnung zwischen zwei Klassen, in denen sich der Fremdschlüssel im Kind befindet.

Beispiel einer Bidirektionalen Eins-zu-Eins 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 Kindes-Tabelle, 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
    }
}


Kontrollieren der Enden von Assoziationen

Gelegentlich kann es vorkommen, dass man sich mit Domänen-klassen konfrontiert sieht, die mehrere Eigenschaften desselben Typs haben. Sie können sogar selbst-referenziell sein, d.h. die Assoziationseigenschaft hat denselben Typ wie die Domänen-klasse, in der sie sich befindet. Solche Situationen können Probleme verursachen, weil GORM den Typ der Assoziation möglicherweise falsch errät. Betrachten Sie diese einfache Klasse

Beispiel einer Domänen-klasse die mehrere Eigenschaften des gleichen Typen trägt, die sogar noch vom selben Typen wie die Domänen-klasse selbst sind (selbst-referenziell)
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
 1 class Person {
 2     String name
 3     Person parent
 4 
 5     // Bildet die rechts zu sehende, selbst-referenzielle Eins-zu-Eins Assoziation
 6     static belongsTo = [ supervisor: Person ]
 7 
 8     static constraints = { 
 9         supervisor nullable: true 
10         parent nullable: true
11     }
12 }

Für GORM sind die Eigenschaften parent und supervisor zwei Richtungen derselben Zuordnung. Wenn man also die parent Eigenschaft für eine Personen-Instanz festlegt, legt GORM automatisch die supervisor -Eigenschaft für die andere Personen-Instanz fest. Dies mag ggf. das sein was man will, aber wenn man sich die Klasse ansieht haben wir tatsächlich zwei unidirektionale Beziehungen.

Um GORM zur richtigen Zuordnung zu führen kann man über die Eigenschaft mappedBy feststellen, dass eine bestimmte Zuordnung unidirektional ist:

Mit der statischen Eigenschaft mappedBy können Sie steuern, ob eine Zuordnung als unidirektional oder bidirektional zugeordnet wird und welche Eigenschaften bei bidirektionalen Zuordnungen die umgekehrte Richtung bilden.

Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
 1 class Person {
 2     String name
 3     Person parent
 4 
 5     static belongsTo = [ supervisor: Person ]
 6     
 7     // Die Eigenschaft "none" wird als umgekehrte Richtung der Zuordnung (oder als "Rückverweis") behandelt.
 8     static mappedBy = [ supervisor: "none", parent: "none" ]
 9 
10     static constraints = { 
11         supervisor nullable: true 
12         parent nullable: true
13     }
14 }
Bemerke: Die Struktur bleibt gleich!
Bemerke: Die Struktur bleibt gleich!

Man kann "none" auch durch einen beliebigen Eigenschaftsnamen der Zielklasse ersetzen. Und dies funktioniert natürlich auch für normale Domänenklassen, nicht nur für selbst-referenzielle. Die Eigenschaft mappedBy ist auch nicht auf viele-zu-eins- und eins-zu-eins-Zuordnungen beschränkt - Sie funktioniert auch für Eins-zu-viele- und viele-zu-viele-Zuordnungen


Kollektionen einer bidirektionalen Eins-zu-Viele-Klasse leeren/ersetzen

Betrachte man die folgende GORM-Entitäten:

Beispiel einer Bidirektionalen Eins-zu-Viele Beziehung von Book zu Review.
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Book {
2     String name
3     static hasMany = [reviews: Review]
4 }
5 class Review {
6     String author
7     String quote
8     static belongsTo = [book: Book]
9 }

Mit dieser Struktur kann man wie folgt einen Beispiel-Datensatz aus einem Buch mit 2 Reviews anlegen:

1 new Book(name: 'Daemon')
2     .addToReviews(new Review(quote: 'Daemon does for surfing the Web what Jaws did for swimming in the ocean.', author: 'Chicago Sun-Times'))
3     .addToReviews(new Review(quote: 'Daemon is wet-yourself scary, tech-savvy, mind-blowing!', author: 'Paste Magazine'))
4     .save()

Einfach, simpel und logisch. Was jetzt aber, wenn man alle reviews löschen (und von der Assoziationen entfernen) oder durch eine neue Liste ersetzen möchte? Mit der jetzigen ORM-Definition würde man z.B. die folgenden Hilfsfunktionen basteln:

 1 Book replaceReviews(Serializable idParam, List<Review> newReviews) {
 2     Book book = Book.where { id == idParam }.join('reviews').get()
 3     clearReviews(book)
 4     newReviews.each { book.addToReviews(it) }
 5     book.save()
 6 }
 7 
 8 void clearReviews(Book book) {
 9     List<Serializable> ids = []
10     book.reviews.collect().each {
11         book.removeFromReviews(it)
12         ids << it.id
13     }
14     Review.executeUpdate("delete Review r where r.id in :ids", [ids: ids])
15 }

Alternativ könnte man auch mithilfe der mapping-Eigenschaft cascade das Verhalten des Kaskadierens einer Lösch/Speichern-Aktion ändern:

Zur Verfügung stehen die Werte all, merge, save-update, delete, lock, refresh, evict (See GORM-Operation Session#evict(Object object)), replicate und all-delete-orphan (Nur gültig für Eins-zu-Viele Assoziationen). Man kann auch mehrere Werte angeben indem man diese durch ein Kommata trennt.

Standardmäßig konfiguriert GORM eine Kaskadenrichtlinie von

  • "all" für den Fall, dass eine Entität "zu einer anderen gehört" (belongsTo),
  • "save-update" für Szenarien ohne existierende "gehört zu" (belongsTo)-Anweisung
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
 1 class Book {
 2     String name
 3     static hasMany = [reviews: Review]
 4     static mappping = {
 5         reviews cascade: 'all-delete-orphan'
 6     }
 7 }
 8 class Review {
 9     String author
10     String quote
11     static belongsTo = [book: Book]
12 }

Das Kaskadenverhalten all-delete-orphan sorgt dafür, dass jede verwaiste Review gelöscht wird. Das Aufrufen von .clear() reicht daher aus, um die vorherigen Reviews des Books zu entfernen.

1 Book replaceReviews(Serializable idParam, List<Review> newReviews) {
2     Book book = Book.where { id == idParam }.join('reviews').get()
3     book.reviews.clear()
4     newReviews.each { book.addToReviews(it) }
5     book.save()
6 }


Mehr zur "Eins-zu-Viele"-Assoziation mit hasMany

Eine Eins-zu-Viele-Beziehung liegt vor, wenn eine Klasse, z.B. Review, viele Instanzen einer anderen Klasse hat, z.B. Book. Mit GORM definiert man eine solche Beziehung mit der Eigenschaft hasMany:

Beispiel einer Unidirektionalen Eins-zu-Viele Beziehung von Book zu Author.
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Book {
2     String name
3     static hasMany = [reviews: Review]
4 }
5 class Review {
6     String author
7     String quote
8 }
Bemerke: GORM ordnet diese Art von Beziehung standardmäßig einer Join-Tabelle zu. (book_review)

GORM fügt automatisch eine Eigenschaft vom Typ java.util.Set in die Domänenklasse ein, basierend auf der Einstellung hasMany. Dies kann verwendet werden, um die Sammlung zu durchlaufen:

def a = Author.get(1)

for (book in a.books) {
    println book.title
}

Das Standard-Kaskadenverhalten einer solchen Assoziation besteht darin, "Speichern" und "Aktualisieren" zu kaskadieren, jedoch nicht "Löschen". Es sei denn, es wird auch eine Zugehörigkeit/Abhängigkeit (belongsTo) definiert.


Mehrere Eigenschaften des gleichen Types auf der Vielen-Seite

Wenn man zwei Eigenschaften desselben Typs auf der "Vielen"-Seite eines Eins-zu-Viele hat, muss man "mappedBy" verwenden, um anzugeben, welche Sammlung zugeordnet werden soll:

Beispiel einer Unidirektionalen Eins-zu-Viele Beziehung von Airport zu Flight, bei dem die Klasse Flight zwei Spalten mit dem gleichen Typen von Airport besitzt
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Airport {
2     static hasMany = [flights: Flight]
3     static mappedBy = [flights: "departureAirport"]
4 }
5 class Flight {
6     Airport departureAirport
7     Airport destinationAirport
8 }
Bemerke: Die Eigenschaften flights werden von GORM nicht als Datenbankspalte gemappt sondern sind nur logische Konstrukte die durch mappedBy eingestellt/definiert wurden.


Dies gilt auch, wenn man mehrere Sammlungen hat, die auf der "vielen" Seite unterschiedlichen Eigenschaften zugeordnet sind:

Beispiel einer Unidirektionalen Eins-zu-Viele Beziehung von Airport zu Flight, bei dem die Klasse Airport zwei Kollektionen des Typen Flights besitzt und die Klasse Flight zwei Spalten mit dem gleichen Typen von Airport besitzt
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Airport {
2     static hasMany = [outboundFlights: Flight, inboundFlights: Flight]
3     static mappedBy = [outboundFlights: "departureAirport",
4                        inboundFlights: "destinationAirport"]
5 }
6 class Flight {
7     Airport departureAirport
8     Airport destinationAirport
9 }
Bemerke: Die Eigenschaften outboundFlights und inboundFlights werden von GORM nicht als Datenbankspalte gemappt sondern sind nur logische Konstrukte die durch mappedBy eingestellt/definiert wurden. Daher sieht auch die Schema-Ansicht gleich wie beim oberen Beispiel aus.


(Bidirektionales) Viele-zu-Viele

GORM unterstützt viele-zu-viele-Beziehungen, indem man auf beiden Seiten der Beziehung ein hasMany definiert und auf der besitzenden Seite der Beziehung ein "Gehört zu" definiert:

Beispiel einer Bidirektionalen Viele-zu-Viele Beziehung von Author zu Book, bei dem die Klasse Author die "Besitzt-habende"-Seite ist
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Book {
2     static belongsTo = Author
3     static hasMany = [authors:Author]
4     String title
5 }
6 class Author {
7     static hasMany = [books:Book]
8     String name
9 }
Bemerke: GORM ordnet viele-zu-viele mithilfe einer Verknüpfungstabelle (Join-Table) auf Datenbankebene zu.

Die besitzende Seite der Beziehung, in diesem Fall "Autor", übernimmt die Verantwortung für das Fortbestehen (Persisting) der Beziehung und ist die einzige Seite, die kaskadieren kann.

Dies ist das erwartete Verhalten, da in GORM genau wie in "Hibernate" nur eine Seite der vielen-zu-vielen die Verantwortung für die Verwaltung der Beziehung übernehmen kann.

Beispiel-Code der Bidirektionalen Viele-zu-Viele Beziehung von Author zu Book, inklusive die daraus resultierende Datenbankansicht (IntelliJ).
Beispiel-Code Resultat
// Die `save`-Aktion der Besitz-Habenden-Seite (Author) kaskadiert, so dass auch die Book's in der Datenbank gespeichert werden/verbleiben.
new Author(name:"Stephen King")
        .addToBooks(new Book(title:"The Stand"))
        .addToBooks(new Book(title:"The Shining"))
        .save()
// Dies speichert nur das Book, und nicht dessen Author'en!!
new Book(title:"Groovy in Action")
        .addToAuthors(new Author(name:"Dierk Koenig"))
        .addToAuthors(new Author(name:"Guillaume Laforge"))
        .save()


Grundlegende Kollektions-Typen + Einstellen der Join-Table

Neben den Zuordnungen zwischen verschiedenen Domänenklassen unterstützt GORM auch die Zuordnung grundlegenden/primitiven Sammlungstypen. Die folgende Klasse erstellt beispielsweise eine Zuordnung von Spitznamen, bei der es sich um eine Reihe von String-Instanzen handelt:

Beispiel einer Unidirektionalen Eins-zu-Viele Beziehung von Person zu String
Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
1 class Person {
2     static hasMany = [nicknames: String]
3     
4     
5     // NUR ZU SCHAUZWECKEN ::: BITTE NICHT VERWENDEN!
6     String[] strings
7 }
Bemerke: GORM ordnet diese eins-zu-viele mithilfe einer Verknüpfungstabelle (Join-Table) auf Datenbankebene zu.
Beispiel-Code einer Unidirektionalen Eins-zu-Viele Beziehung von Person zu String, inklusive die daraus resultierende Datenbankansicht (IntelliJ).
Beispiel-Code Resultat
new Person(
    nicknames: ["Gandalf", "Josef"],
    strings: ["Lorem", "Ipsum"]
).save()
new Person(
    nicknames: ["Gandalf", "Josef"],
    strings: ["Lorem", "Ipsum"]
).save()

Mit der Eigenschaft joinTable kann man die verschiedenen Aspekte der Join-Tabelle ändern:

joinTable passt die Verknüpfungstabelle an, die für unidirektionale Eins-zu-Viele-, Viele-zu-Viele- und primitive Auflistungstypen verwendet wird.

Beispiel einer Unidirektionalen Eins-zu-Viele Beziehung von Person zu String mit veränderten joinTable Einstellungen
Beispiel-Code Resultat
class Person {
    static hasMany = [nicknames: String]

    static mapping = {
       nicknames joinTable: [name: 'bunch_o_nicknames',
                           key: 'person_id',
                           column: 'nickname',
                           type: "text"]
    }
}


Domain-Modelling in GORM: Komposition

Neben Assoziationen unterstützt GORM das Konzept der Komposition/Zusammensetzung. In diesem Fall kann anstelle der Abbildung von Klassen auf separate Tabellen eine Klasse in die aktuelle Tabelle "eingebettet" werden. Zum Beispiel:

Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
 1 // grails-app/domain/Person.groovy
 2 class Person {
 3     Address homeAddress
 4     Address workAddress
 5     static embedded = ['homeAddress', 'workAddress']
 6 }
 7 
 8 // grails-app/domain/Address.groovy
 9 class Address {
10     String number
11     String code
12 }

TIPP: Wenn man die Klasse Address in einer separaten Groovy-Datei im Verzeichnis grails-app/domain definiert, erhält man auch eine Tabelle address (wie im oberen Beispiel). Wenn dies nicht geschehen soll, kann man mithilfe der Funktion von Groovy mehrere Klassen pro Datei definieren und die Klasse Address unterhalb der Klasse Person in die Datei grails-app/domain/Person.groovy aufnehmen.

Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

IntelliJ's Domänenklassen-Abhängigkeiten-Ansicht
 1 // grails-app/domain/Person.groovy
 2 class Person {
 3     Address homeAddress
 4     Address workAddress
 5     static embedded = ['homeAddress', 'workAddress']
 6     
 7     class Address {
 8         String number
 9         String code
10     }
11 }
Bei der Assoziations-Ansicht hat sich natürlich nichts geändert.. Das Groovy-Objekt Address, das ja in diesem Szenario nur zum halten/gruppieren von Daten zuständig ist (DRY), wird hierdurch einfach nicht als eigene Tabelle abgebildet!


Domain-Modelling in GORM: Vererbung

Tabelle pro VererbungshierarchieQuelle: Wikimedia
Tabelle pro UnterklasseQuelle: Wikimedia
Tabelle pro konkrete KlasseQuelle: Wikimedia

GORM unterstützt die Vererbung sowohl von abstrakten Basisklassen als auch von konkreten persistenten GORM-Entitäten.


Abbildungsverfahren von Vererbungshierarchien

Quelle der Beschreibungen: Wikipedia

Es gibt im Wesentlichen drei verschiedene Verfahren, um Vererbungshierarchien auf Datenbanktabellen abzubilden.

Tabelle pro Vererbungshierarchie
(auch Single Table, einzelne Tabelle) Bei diesem Verfahren werden alle Attribute der Basisklasse und aller davon abgeleiteten Klassen in einer gemeinsamen Tabelle gespeichert. Zusätzlich wird ein sogenannter „Diskriminator“ in einer weiteren Spalte abgelegt, der festlegt, welcher Klasse das in dieser Zeile gespeicherte Objekt angehört. Attribute von abgeleiteten Klassen dürfen bei diesem Ansatz aber in den meisten Fällen nicht mit einem NOT-NULL-Constraint versehen werden. Außerdem können Beschränkungen der Anzahl erlaubter Spalten pro Tabelle diesen Ansatz bei großen Klassen bzw. Klassenhierarchien vereiteln.
Tabelle pro Unterklasse
(auch Joined oder Class Table) Bei diesem Verfahren wird eine Tabelle für die Basisklasse angelegt und für jede davon abgeleitete Unterklasse eine weitere Tabelle. Ein Diskriminator wird nicht benötigt, weil die Klasse eines Objekts durch eine 1-zu-1-Beziehung zwischen dem Eintrag in der Tabelle der Basisklasse und einem Eintrag in einer der Tabellen der abgeleiteten Klassen festgelegt ist.
Tabelle pro konkrete Klasse
(auch Table per Class oder Concrete Table) Hier werden die Attribute der abstrakten Basisklasse in die Tabellen für die konkreten Unterklassen mit aufgenommen. Die Tabelle für die Basisklasse entfällt. Der Nachteil dieses Ansatzes besteht darin, dass es nicht möglich ist, mit einer Abfrage Instanzen verschiedener Klassen zu ermitteln.


Die gewünschte Vererbungshierarchie kann mithilfe von ORM-DSL-Mapping wie folgt festgelegt werden:

class Payment {
    int amount
    static mapping = {
        tablePerHierarchy false
        // ODER
        tablePerConcreteClass true
    }
}

class CreditCardPayment extends Payment {
    String cardNumber
}

Polymorph-ische Abfragen

class Content {
     String author
}
class BlogEntry extends Content {
    URL url
}
class Book extends Content {
    String ISBN
}
class PodCast extends Content {
    byte[] audioStream
}

Das Ergebnis der Vererbung ist, dass man die Möglichkeit hat, polymorph abzufragen. Wenn man beispielsweise die Methode list() für die Superklasse Content verwendet, werden auch alle Unterklassen von Content zurückgegeben:

def content = Content.list() // list all blog entries, books and podcasts
content = Content.findAllByAuthor('Joe Bloggs') // find all by author

def podCasts = PodCast.list() // list only podcasts


Domain-Modelling in GORM: Sets, Lists und Maps

Wenn man eine Viele-Beziehung in GORM definiert, handelt es sich standardmäßig um eine java.util.Set.

SortedSet

Sets garantieren Einzigartigkeit, aber keine Reihenfolge, was möglicherweise nicht das ist, was man will. Um eine benutzerdefinierte Ordnung zu erhalten, konfigurieren Sie das Set als SortedSet:

Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

class Author {
    SortedSet books
    static hasMany = [books: Book]
}

In diesem Fall wird eine Implementierung von java.util.SortedSet für die injezierte Variable verwendet. Dies bedeutet, dass man java.lang.Comparable in seiner Book-Klasse implementieren müsste:

class Book implements Comparable {
    String title
    Date releaseDate = new Date()

    @Override
    int compareTo(obj) {
        releaseDate.compareTo(obj.releaseDate)
    }
}


List

Um Objekte in der Reihenfolge zu halten, in der sie hinzugefügt wurden, und um sie wie ein Array nach Index zu referenzieren, kann man den Sammlungstyp als List definieren:

Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

class Author {
    List books
    static hasMany = [books: Book]
}

Auf Datenbankebene funktioniert dies so, dass Hibernate eine Spalte books_idx erstellt, in der der Index der Elemente in der Sammlung gespeichert wird, um diese Reihenfolge auf Datenbankebene beizubehalten. (Siehe Bild)

Bei Verwendung einer Liste müssen Elemente zur Sammlung hinzugefügt werden, bevor sie gespeichert werden. Andernfalls löst Hibernate eine Ausnahme aus (org.hibernate.HibernateException: Nullindexspalte für die Sammlung):

// Wirft den genannten Fehler!
def book = new Book(title: 'The Shining')
book.save()
author.addToBooks(book)

// Richtiger Weg
def book = new Book(title: 'Misery')
author.addToBooks(book)
author.save()


Hibernate Bags (Collection)

Wenn Ordnung und Eindeutigkeit keine Rolle spielen (oder wenn man diese explizit verwaltet), kann man den Typ Bag von Hibernate verwenden, um zugeordnete Sammlungen darzustellen.

Die einzige dafür erforderliche Änderung besteht darin, den Sammlungstyp als Collection zu definieren

class Author {
   Collection books
   static hasMany = [books: Book]
}

Da Eindeutigkeit und Reihenfolge nicht von Hibernate verwaltet werden, löst das Hinzufügen oder Entfernen von Sammlungen, die als Bag zugeordnet sind, nicht das Laden aller vorhandenen Instanzen aus der Datenbank aus. Daher ist dieser Ansatz leistungsfähiger und erfordert weniger Speicher als die Verwendung eines Sets oder eine Liste.


Maps von Objekten

Wenn man eine einfache Zuordnung von Zeichenfolge / Wert-Paaren wünscht, kann GORM dies wie folgt zuordnen:

Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

Resultat des Beispiel-Codes
class Author {
    Map books // map of ISBN:book names
}

def a = new Author()
a.books = ['1590597583':"My Book"]
a.save()

In oberen Fall müssen der Schlüssel und der Wert der Map ein String sein.

Wenn man eine Map mit Objekten möchte, kann man dies wie folgt tun:

Domänenklasse IntelliJ's Datenbank-Schema-Ansicht dieser Domänenklasse

(dataSource: SQL)

Resultat des Beispiel-Codes
class Book {
    Map authors
    static hasMany = [authors: Author]
}

def a = new Author(name:"Stephen King")

def book = new Book()
book.authors = [stephen:a]
book.save()

Die statische hasMany-Eigenschaft definiert den Typ der Elemente in der Map. Die Schlüssel für die Map müssen Zeichenfolgen sein.


Hinweis zu Sammlungen und dessen Leistung

Der Java Set-Typ erlaubt keine Duplikate. Um die Eindeutigkeit beim Hinzufügen eines Eintrags zu einer festgelegten Zuordnung sicherzustellen, muss Hibernate die gesamten Zuordnungen aus der Datenbank laden. Wenn man eine große Anzahl von Einträgen in der Zuordnung hat, kann dies in Bezug auf die Leistung kostspielig sein.

Das gleiche Verhalten ist für den Typ List erforderlich, da Hibernate die gesamte Zuordnung laden muss, um die Reihenfolge aufrechtzuerhalten. Wenn man eine große Anzahl von Datensätzen in der Zuordnung erwartet, sollte man die Zuordnung daher bidirektional machen, damit die Verknüpfung auf der umgekehrten Seite erstellt werden kann.

def book = new Book(title:"New Grails Book")
def author = Author.get(1)
book.author = author
book.save()

Im oberen Beispiel wird der Zuordnungs-Link vom untergeordneten Element (Buch) erstellt. Daher ist es nicht erforderlich, die Sammlung direkt zu bearbeiten, was zu weniger Abfragen und effizienterem Code führt. Wenn man bei einem Autor mit einer großen Anzahl zugeordneter Buchinstanzen Code wie den folgenden schreiben, wirkt sich dies auf die Leistung aus:

def book = new Book(title:"New Grails Book")
def author = Author.get(1)
author.addToBooks(book)
author.save()


Persistenz Grundlagen

Persistenz ist in der Informatik der Begriff, der die Fähigkeit bezeichnet, Daten (oder Objekte) oder logische Verbindungen über lange Zeit (insbesondere über einen Programmabbruch hinaus) bereitzuhalten.

Da ein Programm jederzeit unvorhergesehen unterbrochen werden kann, bedeutet persistente Datenhaltung insbesondere, dass jede Zustandsänderung der Daten sofort auf dem nichtflüchtigen Medium gespeichert werden muss.

„Persistent“ wird als ein im Kontext wohldefinierter Fachbegriff für „nicht unkontrolliert veränderlich“ verwendet.

Eine wichtige Sache, an die man sich bei GORM erinnern sollte, ist, dass GORM unter der Oberfläche Hibernate für die Persistenz verwendet. Wenn man mit ActiveRecord oder iBatis/MyBatis gearbeitet hat, fühlt sich das "Sitzungs"-Modell von Hibernate möglicherweise etwas seltsam an.

Wenn man Grails verwendet, bindet Grails automatisch eine Hibernate-Sitzung an die aktuell ausgeführte Anforderung. Auf diese Weise kann man die Methoden save und delete sowie andere GORM-Methoden transparent verwenden.

Wenn man Grails nicht verwendet, muss man sicherstellen, dass eine Sitzung an die aktuelle Anforderung gebunden ist. Eine Möglichkeit, dies zu erreichen, ist die withNewSession(Closure)-Methode:

Book.withNewSession {
        // your logic here
}

Eine weitere Option besteht darin, eine Transaktion mit der withTransaction(Closure)-Methode zu binden:

Book.withTransaction {
        // your logic here
}


Transaktions-Write-Behind

Eine nützliche Funktion von Hibernate über direkte JDBC-Aufrufe und sogar andere Frameworks ist, dass beim Aufrufen von save() oder delete() zu diesem Zeitpunkt nicht unbedingt SQL-Vorgänge ausgeführt werden. Hibernate stapelt SQL-Anweisungen und führt sie so spät wie möglich aus, häufig am Ende der Anforderung, wenn die Sitzung geleert (flush) und geschlossen wird.Wenn man Grails verwendet, wird dies normalerweise automatisch für einen erledigt. Wenn man GORM außerhalb von Grails verwendet, muss man die Sitzung möglicherweise am Ende des Vorgangs manuell leeren.


Hibernate speichert Datenbankaktualisierungen nach Möglichkeit zwischen, wobei die Änderungen nur dann tatsächlich übertragen werden, wenn bekannt ist, dass ein Flush erforderlich ist, oder wenn ein Flush programmgesteuert ausgelöst wird. Ein häufiger Fall, in dem Hibernate zwischengespeicherte Aktualisierungen löscht, ist das Ausführen von Abfragen, da die zwischengespeicherten Informationen möglicherweise in den Abfrageergebnissen enthalten sind. Solange man jedoch konfliktfreie Speicherungen, Aktualisierungen und Löschvorgänge durchführt, werden diese gestapelt, bis die Sitzung gelöscht wird. Dies kann eine erhebliche Leistungssteigerung für Anwendungen sein, die viele Datenbankschreibvorgänge ausführen.


Zu beachten gilt, dass das Flushing nicht mit dem Commiten einer Transaktion identisch ist. Wenn die Aktionen im Kontext einer Transaktion ausgeführt werden, führt das Flushing SQL-Aktualisierungen aus, aber die Datenbank speichert die Änderungen in ihrer Transaktionswarteschlange und schließt die Aktualisierungen erst ab, wenn die Transaktion festgeschrieben wird.

Speichern und Aktualisieren von Objekten

Ein Beispiel für die Verwendung der save()-Methode wäre:

def p = Person.get(1)
p.save()

Die eigentliche Speicherung wird nicht sofort in die Datenbank übertragen, sondern beim nächsten Flush. Es gibt jedoch Fälle, in denen man steuern möchten, wann diese Anweisungen ausgeführt werden (oder in der Hibernate-Terminologie, wenn die Sitzung "geleert" wird). Dazu kann man das Argument flush für die save()-Methode verwenden:

def p = Person.get(1)
p.save(flush: true)

Zu Beachten gilt, dass in diesem Fall alle ausstehenden SQL-Anweisungen einschließlich vorheriger Speicherungen, Löschungen usw. mit der Datenbank synchronisiert werden. Auf diese Weise kann man auch Ausnahmen abfangen, was in der Regel in Szenarien mit optimistischem Sperren hilfreich ist:

def p = Person.get(1)
try {
    p.save(flush: true)
}
catch (org.springframework.dao.DataIntegrityViolationException e) {
    // deal with exception
}

Außerdem gilt zu beachten dass GORM eine Domain-Instanz jedes Mal überprüft, wenn man sie speichern will. Wenn diese Überprüfung fehlschlägt, wird die Domäneninstanz nicht in der Datenbank gespeichert. Standardmäßig gibt save() in diesem Fall einfach null zurück. Wenn Sie jedoch eine Ausnahme auslösen möchten, können Sie das Argument failOnError verwenden:

def p = Person.get(1)
try {
    p.save(failOnError: true)
}
catch (ValidationException e) {
    // deal with exception
}


Löschen von Objekten

Ein Beispiel für die Verwendung der delete()-Methode wäre:

def p = Person.get(1)
p.delete()

Wie beim Speichern verwendet Hibernate das Transaktions-Write-Behind-Konzept, um das Löschen durchzuführen. Um das Löschen an Ort und Stelle durchzuführen, kann man das Argument flush verwenden:

def p = Person.get(1)
p.delete(flush: true)

Mit dem flush-Argument kann man alle Fehler abfangen, die beim Löschen auftreten. Ein häufiger Fehler, der auftreten kann, ist, wenn Sie eine Datenbankeinschränkung verletzt, obwohl dies normalerweise auf einen Programmier- oder Schemafehler zurückzuführen ist. Das folgende Beispiel zeigt, wie eine DataIntegrityViolationException abgefangen wird, die ausgelöst wird, wenn man die Datenbankeinschränkungen verletzt:

import org.springframework.dao.*

def p = Person.get(1)

try {
    p.delete(flush: true)
}
catch (DataIntegrityViolationException e) {
    // handle the error
}

Um einen Stapel-Löschvorgang durchzuführen, gibt es verschiedene Möglichkeiten, dies zu erreichen. Eine Möglichkeit besteht darin, eine Where-Abfrage zu verwenden:

Person.where {
        name == "Fred"
}.deleteAll()


Eager und Lazy Fetching

Zuordnungen in GORM sind standardmäßig faul. Dies lässt sich am besten anhand eines Beispiels erklären:

class Airport {
    String name
    static hasMany = [flights: Flight]
}

class Flight {
    String number
    Location destination
    static belongsTo = [airport: Airport]
}

class Location {
    String city
    String country
}

Angesichts der oben genannten Domänenklassen und des folgenden Codes:

def airport = Airport.findByName("Gatwick")
for (flight in airport.flights) {
    println flight.destination.city
}

GORM führt einzelne SQL-Abfrage aus, um...

  • die "Flughafen"-Instanz abzurufen,
  • eine andere, um ihre Flüge abzurufen,
  • und dann eine zusätzliche Abfrage für jede Iteration über die Flugzuordnung, um das aktuelle Flugziel abzurufen.

Mit anderen Worten: Man führt N+1-Anfragen aus! (wenn Sie die ursprüngliche ausschließen, um den Flughafen zu erhalten).


Eager Fetching konfigurieren

class Airport {
    String name
    static hasMany = [flights: Flight]
    static mapping = {
        flights lazy: false
    }
}

Für weitere Informationen zu Eager Fetching (dessen Nachteile und Prinzipien) sowie zum Batch-Fetching Siehe die Offizielle Dokumentation


Checken auf Modifikation

Sobald man eine persistente Domänenklasseninstanz geladen und möglicherweise geändert hat, ist es nicht einfach, die ursprünglichen Werte abzurufen. Wenn man versucht, die Instanz mit get(id) neu zu laden, gibt Hibernate die aktuell geänderte Instanz aus dem Sitzungscache zurück.

Das neuladen mit einer neuen Abfrage würde einen Flush auslösen, der Probleme verursachen kann, wenn seine Daten noch nicht zum Flush bereit sind. Daher bietet GORM einige Methoden zum Abrufen der ursprünglichen Werte, die Hibernate beim Laden der Instanz zwischenspeichert (die für die fehlerhafte Überprüfung verwendet wird)

  • isDirty(): Checkt ob überhaupt irgend-ein Feld verändert wurde (Funktioniert mit allen Persistenten Eigenschaften und Assoziationen, nur Collection-Assoziationen (Bags) funktionieren in der jetzigen Version noch nicht)
  • isDirty(fieldName): Checkt ob ein bestimmtes Feld verändert wurde
  • getDirtyPropertyNames(): Gibt die Namen der Felder zurück die verändert wurden
  • getPersistentValue(fieldName): Gibt den ursprünglichen / derzeitig persistenten Wert eines Felds zurück


Abfragen erstellen (Querying)

GORM unterstützt eine Reihe leistungsstarker Abfragemöglichkeiten, von dynamischen Findern, über Kriteriensuche bis hin zu eigens definierbaren HQL-Anfragen (Hibernate Object Oriented Query Language). Abhängig von der Komplexität der Abfrage hat man die folgenden Optionen in der Reihenfolge der Flexibilität und Leistung:

  • Dynamische Finder
  • "Wo/Bei Dem"-Abfragen
  • Kriterien Abfragen
  • Hibernate Query Language (HQL)

Darüber hinaus führt die Fähigkeit von Groovy, Sammlungen mit GPath und Methoden wie sort, findAll usw. in Kombination mit GORM zu bearbeiten, zu einer leistungsstarken Kombination.


Instanzen auflisten

Die list()-Methode gibt (ohne Argumente) alle Domäneninstanzen einer Klasse zurück:

def books = Book.list()

Die list(params)-Methode unterstützt Argumente zur Paginierung..

def books = Book.list(offset:10, max:20)

..und zum simplen sortieren. (Das argument sort trägt den Namen des Feldes der Domänenklasse nach der man sortieren will, und das order-Argument akzeptiert die Werte asc (ascending, aufsteigend) und desc (descenting, absteigend))

def books = Book.list(sort:"title", order:"asc")


Instanz über ID ausfindig machen

Die zweite Grundform eines Abrufs ist mithilfe die Datenbankkennung über der Methode get(id):

def book = Book.get(23)

Mit der Methode getAll(ids) kann man auch gleich mehrere Instanzen nach der ID ausfindig machen.

def books = Book.getAll(23, 93, 81)


Dynamische Finder

GORM unterstützt das Konzept der dynamischen Finder. Dynamische Finder sieht aus wie ein statischer Methodenaufruf, aber die Method selbst sind auf Codeebene in keiner Form vorhanden.

Stattdessen wird eine Methode mithilfe von Codesynthese zur Laufzeit auf magische Weise generiert, basierend auf den Eigenschaften einer bestimmten Klasse. Nehmen wir zum Beispiel die Klasse Book:

class Book {
    String title
    Date releaseDate
    Author author
}
class Author {
    String name
}

Die Klasse Book hat Eigenschaften wie title, releaseDate und author. Diese können mithilfe der dynamischen findBy* und findAllBy*-Methoden in form von Methodenausdrücken genutzt werden:

def book = Book.findByTitle("The Stand")

book = Book.findByTitleLike("Harry Pot%")

book = Book.findByReleaseDateBetween(firstDate, secondDate)

book = Book.findByReleaseDateGreaterThan(someDate)

book = Book.findByTitleLikeOrReleaseDateLessThan("%Something%", someDate)


Methodenausdrücke

Ein Methodenausdruck in GORM besteht aus dem Präfix wie findBy*, gefolgt von einem Ausdruck, der eine oder mehrere Eigenschaften kombiniert. Die Grundform ist:

Book.findBy(<<Property>><<Comparator>><<Boolean Operator>>)?<<Property>><<Comparator>>

Die mit einem "?" Token sind optional. Jeder Komparator ändert die Art der Abfrage. Beispielsweise:

def book = Book.findByTitle("The Stand")

book =  Book.findByTitleLike("Harry Pot%")

Im obigen Beispiel entspricht die erste Abfrage der (exakten) Gleichheits-Abfrage, während die letztere aufgrund des Like-Komparators einem SQL-Like-Ausdruck entspricht.


Die verfügbaren Komperatoren (Comperators) sind:

  • InList - In der Liste der angegebenen Werte
  • LessThan - weniger als der gegebener Wert
  • LessThanEquals - kleiner oder gleich als der gegeben Wert
  • GreaterThan - größer als der gegebene Wert
  • GreaterThanEquals - Größer als oder gleich dem gegeben Wert
  • Like - Entspricht einem SQL-Like-Ausdruck
  • Ilike - Wie Like, außer dass Groß- und Kleinschreibung nicht berücksichtigt wird
  • NotEqual - Negiert die Gleichheitsbedingung
  • InRange - Zwischen den from und to Werten einer Groovy-Range
  • Rlike - Führt eine Regexp-LIKE in MySQL oder Oracle aus, anderenfalls wird auf Like zurückgefallen
  • Between - Zwischen zwei Werten (erfordert zwei Argumente)
  • IsNotNull - Kein Nullwert (nimmt kein Argument an)
  • IsNull - Ist ein Nullwert (nimmt kein Argument an)

Beachte: Die letzten drei Komperatoren fordern im Vergleich zu den anderen Komperatoren eine unterschiedliche Anzahl von Methodenargumenten an, wie das folgende Beispiel demonstrieren sollte:

def now = new Date()
def lastWeek = now - 7
def book = Book.findByReleaseDateBetween(lastWeek, now)

books = Book.findAllByReleaseDateIsNull()
books = Book.findAllByReleaseDateIsNotNull()


Boolesche Logik

Methodenausdrücke können auch einen booleschen Operator verwenden, um zwei oder mehr Kriterien zu kombinieren:

def books = Book.findAllByTitleLikeAndReleaseDateGreaterThan("%Java%", new Date() - 30)

In diesem Fall verwenden wir And in der Mitte der Abfrage, um sicherzustellen, dass beide Bedingungen erfüllt sind. Man kann jedoch auch Or verwenden:

def books = Book.findAllByTitleLikeOrReleaseDateGreaterThan("%Java%", new Date() - 30)

Man kann so viele Kriterien kombinieren wie man möchten, aber alle müssen mit And oder Or kombiniert werden. Wenn man And und Or kombinieren möchte oder wenn die Anzahl der Kriterien einen sehr langen Methodennamen ergibt, wäre es gut stattdessen einfach in eine Kriterien- oder HQL-Abfrage zu verwenden (siehe später).


Assoziationen Abfragen

def author = Author.findByName("Stephen King")

def books = author ? Book.findAllByAuthor(author) : []

In diesem Fall verwenden wir die Author-Instanz wenn sie nicht null ist um alle Book-Instanzen für den angegebenen Author abzurufen.


Pagination und Sortierung der Ergebnisse

Wie bei der list()-Methode funktionieren hier auch die genannten Argumente zur Pagination und Sortierung der Ergebnisse. Diese werden als finales Argument in einer Map angegeben.

def books = Book.findAllByTitleLike("Harry Pot%",
               [max: 3, offset: 2, sort: "title", order: "desc"])


"Where" Abfragen

Die where()-Methode baut auf der Unterstützung von Detached Criteria auf, indem sie eine erweiterte DSL-Abfrage zur Kompilierungszeit für allgemeine Abfragen bereitstellt. Die where-Methode ist flexibler als dynamische Finder, weniger ausführlich als Kriterien und bietet dennoch einen leistungsstarken Mechanismus zum Erstellen von Abfragen.

Grundlegende Abfragen

Die where() Methode akzeptiert eine Closure, die den regulären Collection-Methoden von Groovy sehr ähnlich sieht. Die Closure sollte die logischen Kriterien im regulären Groovy-Syntax definieren:

def query = Person.where {
   firstName == "Bart"
}
Person bart = query.find()

Das zurückgegebene Objekt ist eine DetachedCriteria-Instanz. Dies bedeutet, dass es keiner bestimmten Datenbankverbindung oder Sitzung zugeordnet ist. Dies bedeutet, dass man die where-Methode verwenden könnte, um allgemeine Abfragen auf Klassenebene zu definieren:

import grails.gorm.*

class Person {
    static DetachedCriteria<Person> simpsons = where {
         lastName == "Simpson"
    }
    ...
}
...
Person.simpsons.each { Person p ->
    println p.firstname
}

Die Ausführung von where-Abfragen ist verzögert und erfolgt nur bei Verwendung der DetachedCriteria-Instanz. Wenn man eine Abfrage im where-Stil sofort ausführen möchten, gibt es Variationen von findAll und find-Methoden, um dies zu erreichen:

def results = Person.findAll {
     lastName == "Simpson"
}
def results = Person.findAll(sort:"firstName") {
     lastName == "Simpson"
}
Person p = Person.find { firstName == "Bart" }

Jeder Groovy-Operator kann einer regulären Kriterien-methode zugeordnet werden. Die folgende Tabelle enthält eine Zuordnung der Groovy-Operatoren zu den Methoden:

Operator Abgebildet als Kriterien Methode Beschreibung
== eq Gleich wie
!= ne Nicht gleich wie
> gt Größer als
< lt Kleiner als
>= ge Größer als oder gleich wie
<= le Kleiner als oder gleich wie
in inList In der gegebenen Liste enthalten
==~ like Ähnlich wie dem gegeben String
=~ ilike like, aber ohne Acht auf Groß-und-Kleinschreibung

Es ist möglich, reguläre Groovy-Vergleichsoperatoren und -Logik zu verwenden, um komplexe Abfragen zu formulieren:

def query = Person.where {
    (lastName != "Simpson" && firstName != "Fred") || (firstName == "Bart" && age > 9)
}
def results = query.list(sort:"firstName")

Die Groovy-Regex-Matching-Operatoren werden like und ilike-Abfragen zugeordnet, es sei denn, der Ausdruck auf der rechten Seite ist ein Pattern-Objekt. In diesem Fall wird sie einer rlike Abfrage zugeordnet:

def query = Person.where {
     firstName ==~ ~/B.+/
}

Der von den dynamischen Findern bekannte between-Ausdruck lässt sich in einer where-Abfrage mit dem Schlüsselwort in durchsetzen:

def query = Person.where {
     age in 18..65
}

Die von den dynamischen Findern bekannte isNull und isNotNull Abfrage kann durch einen simplen Vergleich mit null realisiert werden

def query = Person.where {
     middleName == null
}


Komposition von Abfragen

Da der Rückgabewert der where-Methode eine DetachedCriteria-Instanz ist, kann man aus der ursprünglichen Abfrage neue Abfragen erstellen (aneinanderreihen):

DetachedCriteria<Person> query = Person.where {
     lastName == "Simpson"
}
DetachedCriteria<Person> bartQuery = query.where {
     firstName == "Bart"
}
Person p = bartQuery.find()

Beachten: Eine als Variable definierte Closure kann nur dann an die where-Methode übergeben können, wenn er explizit in eine DetachedCriteria-Instanz umgewandelt wurde. Mit anderen Worten, Folgendes führt zu einem Fehler:

def callable = {
    lastName == "Simpson"
}
def query = Person.where(callable)

Das Obige muss wie folgt geschrieben werden:

import grails.gorm.DetachedCriteria

def callable = {
    lastName == "Simpson"
} as DetachedCriteria<Person>
def query = Person.where(callable)

Wie man sieht, wird die Closure-Definition (unter Verwendung des Schlüsselworts as) in eine DetachedCriteria-Instanz umgewandelt (ge-castet), die auf die Person-Klasse abzielt.


Konjunktion, Disjunktion und Negation

Wie bereits erwähnt, kann man reguläre logische Groovy-Operatoren (|| und &&) zu Konjunktionen und Disjunktionen kombinieren:

def query = Person.where {
    (lastName != "Simpson" && firstName != "Fred") || (firstName == "Bart" && age > 9)
}

Man kann logische Komparationen wie gewohnt mit einem ! negieren:

def query = Person.where {
    firstName == "Fred" && !(lastName == 'Simpson')
}


Eigenschaftenvergleichsabfragen

Wenn man einen Eigenschaftsnamen sowohl auf der linken als auch auf der rechten Seite eines Vergleichsausdrucks verwendet, werden automatisch die entsprechenden Eigenschaftsvergleichskriterien verwendet:

def query = Person.where {
   firstName == lastName
}

In der folgenden Tabelle wird beschrieben, wie jeder Vergleichsoperator den Eigenschaftenvergleichsmethoden der einzelnen Kriterien zugeordnet wird:

Operator Abgebildet als Kriterien Methode Beschreibung
== eqProperty Gleich wie
!= neProperty Nicht gleich wie
> gtProperty Größer als
< ltProperty Kleiner als
>= geProperty Größer als oder gleich wie
<= leProperty Kleiner als oder gleich wie


Assoziationen

Zuordnungen können mithilfe des Punktoperators abgefragt werden, um den Eigenschaftsnamen der abzufragenden Zuordnung anzugeben:

def query = Pet.where {
    owner.firstName == "Joe" || owner.firstName == "Fred"
}

Man kann mehrere Kriterien in einem Closure-Methodenaufruf gruppieren, wobei der Name der Methode mit dem Zuordnungsnamen übereinstimmt:

def query = Person.where {
    pets { name == "Jack" || name == "Joe" }
}

Diese Technik kann mit anderen Kriterien der obersten Ebene kombiniert werden:

def query = Person.where {
     pets { name == "Jack" } || firstName == "Ed"
}

Bei Assoziationen mit Sammlungen können Abfragen auf die Größe der Sammlung angewendet werden:

def query = Person.where {
       pets.size() == 2
}

Die folgende Tabelle zeigt, welcher Operator für jeden size()-Vergleich auf welche Kriterienmethode abgebildet wird:

Operator Abgebildet als Kriterien Methode Beschreibung
== sizeEq Die Größe der Sammlung ist gleich wie
!= sizeNe Die Größe der Sammlung ist nicht gleich wie
> sizeGt Die Größe der Sammlung ist größer als
< sizeLt Die Größe der Sammlung ist kleiner als
>= sizeGe Die Größe der Sammlung ist größer als oder gleich wie
<= sizeLe Die Größe der Sammlung ist kleiner als oder gleich wie


Aliase und Sortierung

Wenn man eine Abfrage für eine Assoziation definiert wird automatisch ein Alias für die Abfrage generiert. Zum Beispiel die folgende Abfrage:

def query = Pet.where {
    owner.firstName == "Fred"
}

Generiert einen Alias für die owner-Assoziation, z.B. owner_alias_0. Diese generierten Aliase sind in den meisten Fällen in Ordnung, aber nicht nützlich, wenn man die Ergebnisse später sortieren oder mit einer Projektion versehen möchten. Die folgende Abfrage schlägt beispielsweise fehl:

// fails because a dynamic alias is used
Pet.where {
    owner.firstName == "Fred"
}.list(sort:"owner.lastName")

Wenn man die Ergebnisse sortieren möchte, sollte ein expliziter Alias verwendet werden, der durch einfaches Deklarieren einer Variablen in der where-Abfrage definiert werden kann:

def query = Pet.where {
    // Define an alias called o1
    def o1 = owner 
    // Use the alias in the query itself
    o1.firstName == "Fred" 
// Use the alias to sort the results
}.list(sort:'o1.lastName')

Durch Zuweisen des Namens einer Zuordnung zu einer lokalen Variablen wird diese automatisch zu einem Alias, der innerhalb der Abfrage selbst und auch zum Sortieren oder Projizieren der Ergebnisse verwendet werden kann.


Unterabfragen

Es ist möglich, Unterabfragen innerhalb von Abfragen auszuführen. Um beispielsweise alle Personen zu finden, die älter als das Durchschnittsalter sind, kann die folgende Abfrage verwendet werden:

final query = Person.where {
  age > avg(age)
}


Methode Beschreibung
avg Der Durchschnitt aller Werte
sum Die Summe aller Werte
max Der maximale Wert
min Der minimale Wert
count Die Anzahl an Werten
property Gibt eine Eigenschaft der resultierenden Entitäten zurück

Man kann zusätzliche Kriterien auf jede Unterabfrage anwenden, indem man die of-Methode verwendet und eine Closure übergibt, der die Kriterien enthält:

def query = Person.where {
  age > avg(age).of { lastName == "Simpson" } && firstName == "Homer"
}

Da die property-Unterabfrage mehrere Ergebnisse zurückgibt, vergleicht das verwendete Kriterium alle Ergebnisse. Die folgende Abfrage findet beispielsweise alle Personen, die jünger als Personen mit dem Nachnamen "Simpson" sind:

Person.where {
    age < property(age).of { lastName == "Simpson" }
}


Erweiterte Unterabfragen in GORM

Die Unterstützung für Unterabfragen wurde erweitert. Man kann jetzt mit verschachtelten Unterabfragen rumwerken:

def results = Person.where {
    firstName in where { age < 18 }.firstName
}.list()

Kriterien und where Abfragen können nahtlos gemischt werden:

def results = Person.withCriteria {
    notIn "firstName", Person.where { age < 18 }.firstName
}

Unterabfragen können mit Projektionen verwendet werden:

def results = Person.where {
    age > where { age > 18 }.avg('age')
}

Korrelierte Abfragen, die zwei Domänenklassen umfassen, können verwendet werden:

def employees = Employee.where {
    region.continent in ['APAC', "EMEA"]
}.id()
def results = Sale.where {
    employee in employees && total > 100000
}.employee.list()

Unterstützung für Aliase (Querverweise) mithilfe einfacher Variablendeklarationen wurde hinzugefügt:

def query = Employee.where {
    def em1 = Employee
    exists Sale.where {
        def s1 = Sale
        def em2 = employee
        return em2.id == em1.id
    }.id()
}
def results = query.list()


Andere Funktionen

Im Rahmen einer Abfrage stehen einem verschiedene Funktionen zur Verfügung. Diese sind in der folgenden Tabelle zusammengefasst:

Method Description
second Die Sekunde einer Date-Eigenschaft
minute Die Minute einer Date-Eigenschaft
hour Die Stunde einer Date-Eigenschaft
day Der Tag einer Date-Eigenschaft
month Den Monat einer Date-Eigenschaft
year Der Jahr einer Date-Eigenschaft
lower Konvertiert eine String-Eigenschaft in kleinbuchstaben
upper Konvertiert eine String-Eigenschaft in Großbuchstaben
length Die Länge einer String-Eigenschaft
trim Trimmt eine String-Eigenschaft

HINWEIS: Derzeit können Funktionen nur auf Eigenschaften oder Zuordnungen von Domänenklassen angewendet werden. Man kann beispielsweise keine Funktion für ein Ergebnis einer Unterabfrage verwenden.

Die folgende Abfrage kann beispielsweise verwendet werden, um alle 2011 geborenen Pets zu finden:

def query = Pet.where {
    year(birthDate) == 2011
}

Man kann Funktionen auch auf Assoziationen anwenden:

def query = Person.where {
    year(pets.birthDate) == 2009
}


Batch-Updates und Löschungen

Da jeder where-Methodenaufruf eine DetachedCriteria-Instanz zurückgibt kann man where-Abfragen verwenden, um Stapelvorgänge wie Stapelaktualisierungen und -löschungen auszuführen. Mit der folgenden Abfrage werden beispielsweise alle Personen mit dem Nachnamen "Simpson" aktualisiert, um den Nachnamen "Bloggs" zu erhalten:

DetachedCriteria<Person> query = Person.where {
    lastName == 'Simpson'
}
int total = query.updateAll(lastName:"Bloggs")

HINWEIS: Join-Abfragen (Abfragen, die Zuordnungen abfragen) sind nicht zulässig sind. Um die abgefragten/gefunden Datensätze stapelweisen zu Löschen kann man die Methode deleteAll verwenden:

DetachedCriteria<Person> query = Person.where {
    lastName == 'Simpson'
}
int total = query.deleteAll()


(Angehängte/Attached) Kriterien

Kriterien sind eine erweiterte/komplexere Methode, bei der mithilfe eines Groovy-Builders potenziell komplexe Abfragen erstellt werden. Kriterien-Abfragen zu nutzen ist ein viel besserer Ansatz als das Erstellen von Query-Strings mit einem StringBuilder.

Kriterien können entweder mit den Methoden createCriteria() oder withCriteria(Closure) verwendet werden.

Der Builder verwendet die Kriterien-API von Hibernate. Die Knoten in diesem Builder ordnen die statischen Methoden zu, die in der Restrictions-Klasse der Hibernate Criteria-API gefunden werden kann. Beispielsweise:

def c = Account.createCriteria()
def results = c {
    between("balance", 500, 1000)
    eq("branch", "London")
    or {
        like("holderFirstName", "Fred%")
        like("holderFirstName", "Barney%")
    }
    maxResults(10)
    order("holderLastName", "desc")
}

Mit diesem Kriterium werden bis zu 10 Account-Objekte in einer Liste ausgewählt, die den folgenden Kriterien entsprechen:

  • balance liegt zwischen 500 und 1000
  • branche ist London
  • holderFirstName beginnt mit Fred oder Barney

Die Ergebnisse werden in absteigender Reihenfolge nach holderLastName sortiert.

Wenn keine Datensätze mit den oben genannten Kriterien gefunden werden, wird eine leere Liste zurückgegeben.

Konjunktionen und Disjunktionen

Wie im vorherigen Beispiel gezeigt können Kriterien in einem logischen ODER mit einem or {}-Block gruppiert werden:

or {
    between("balance", 500, 1000)
    eq("branch", "London")
}

Dies funktioniert auf die gleiche Angehensweiße mit einem logischen UND:

and {
    between("balance", 500, 1000)
    eq("branch", "London")
}

Auch Negierungen können mithilfe einer logischen NOT-Anweisung durchgesetzt werden:

not {
    between("balance", 500, 1000)
    eq("branch", "London")
}

Alle Bedingungen der obersten Ebene sind implizit mit einem logischen UND verbunden.


Assoziationen

Assoziationen können abgefragt werden, indem ein Knoten vorhanden ist, der dem Eigenschaftsnamen entspricht. Angenommen, die Account-Klasse hatte viele Transaction-Objekte:

class Account {
    //...
    static hasMany = [transactions: Transaction]
    //...
}

Wir können diese Zuordnung abfragen, indem wir den Eigenschaftsnamen transactions als Builder-Knoten verwenden:

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
    transactions {
        between('date', now - 10, now)
    }
}

Mit dem obigen Code werden alle Account-Instanzen gefunden, die in den letzten 10 Tagen Transaktionen ausgeführt haben. Man kann solche Assoziations-Abfragen auch in logischen Blöcken verschachteln:

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
    or {
        between('created', now - 10, now)
        transactions {
            between('date', now - 10, now)
        }
    }
}

In diesem Beispiel finden wir alle Konten, die entweder Transaktionen in den letzten 10 Tagen ausgeführt haben oder in den letzten 10 Tagen kürzlich erstellt wurden.


Projections

Projektionen können verwendet werden, um die Ergebnisse anzupassen. Hierzu definiert man einen Knoten namens projections im Baum des Kriterienerstellers. Innerhalb des Projektionsknotens gibt es äquivalente Methoden zu den Methoden die man in der Klasse Projections findet:

def c = Account.createCriteria()

def numberOfBranches = c.get {
    projections {
        countDistinct('branch')
    }
}

Wenn in der Projektion mehrere Felder angegeben sind, wird eine Werteliste zurückgegeben. Andernfalls wird ein einzelner Wert zurückgegeben.


Projection-Ergebnisse transformieren

Wenn der von der Kriterienmethode zurückgegebene Rohwert oder das einfache Objektarray nicht den Anforderungen entspricht, kann das Ergebnis mit einem ResultTransformer transformiert werden. Angenommen, man möchte die Kriterienergebnisse in eine Karte umwandeln, damit wir die Werte einfach nach Schlüssel referenzieren können:

def c = Account.createCriteria()

def accountsOverview = c.get {
    // ALIAS_TO_ENTITY_MAP: Each row of results is a Map from alias to entity instance
    resultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP)
    projections {
        sum('balance', 'allBalances')
        countDistinct('holderLastName', 'lastNames')
    }
}

// accountsOverview.allBalances
// accountsOverview.lastNames

Beachte: Wir haben jeder Projektion einen Alias als zusätzlichen Parameter hinzugefügt, der als Schlüssel verwendet werden soll. Damit dies funktioniert, müssen für alle Projektionen Aliase definiert sein, da sonst der entsprechende Karteneintrag nicht erstellt wird. Wir können das Ergebnis auch über die Transformers.aliasToBean()-Methode in ein Objekt unserer Wahl umwandeln. In diesem Fall verwandeln wir es in ein AccountOverview-Objekt:

class AccountsOverview {
    Number allBalances
    Number lastNames
}
def c = Account.createCriteria()

def accountsOverview = c.get {
    // aliasToBean: Creates a resulttransformer that will inject aliased values into instances of Class via property methods or fields.
    resultTransformer(Transformers.aliasToBean(AccountsOverview))
    projections {
        sum('balance', 'allBalances')
        countDistinct('holderLastName', 'lastNames')
    }
}

// accountsOverview instanceof AccountsOverview

Jeder Alias muss eine entsprechende Eigenschaft oder einen expliziten Setter für die Bean haben, andernfalls wird eine Ausnahme ausgelöst.


SQL-Projektionen

Die Kriterien-DSL bietet Zugriff auf die SQL-Projektions-API von Hibernate.

Das erste Argument für die sqlProjection-Methode ist das SQL-Fragment, das die Projektionen definiert.

Das zweite Argument ist eine Liste von Zeichenfolgen, die die Aliasnamen der Spalten darstellen, die den in SQL ausgedrückten projizierten Werten entsprechen.

Das dritte Argument ist eine Liste von org.hibernate.type.Type-Instanzen, die den in SQL ausgedrückten projizierten Werten entsprechen. Die API unterstützt alle org.hibernate.type.Type-Objekte, aber Konstanten wie INTEGER, LONG, FLOAT usw. werden vom DSL bereitgestellt, die allen in org.hibernate.type.StandardBasicTypes definierten Typen entsprechen.


Betrachte man die folgenden Domänenklasse..

// Box is a domain class...
class Box {
    int width
    int height
}

..und führt die folgende Projektionsanweisung..

// Use SQL projections to retrieve the perimeter and area of all of the Box instances...
def c = Box.createCriteria()

def results = c.list {
    projections {
      sqlProjection '(2 * (width + height)) as perimeter, (width * height) as area', ['perimeter', 'area'], [INTEGER, INTEGER]
    }
}

...mit diesem Datensatz aus..

width height
2 7
2 8
2 9
4 9

..bekommt man folgendes Ergebniss:

[[18, 14], [20, 16], [22, 18], [26, 36]]


Wenn nur 1 Wert projiziert wird, müssen der Alias und der Typ nicht in eine Liste aufgenommen werden:

def results = c.list {
    projections {
      sqlProjection 'sum(width * height) as totalArea', 'totalArea', INTEGER
    }
}

Diese Abfrage würde ein einzelnes Ergebnis mit dem Wert 84 als Gesamtfläche aller Box-Instanzen zurückgeben. DSL unterstützt gruppierte Projektionen mit der Methode sqlGroupProjection.

def results = c.list {
    projections {
        sqlGroupProjection 'width, sum(height) as combinedHeightsForThisWidth', 'width', ['width', 'combinedHeightsForThisWidth'], [INTEGER, INTEGER]
    }
}

Das erste Argument für die Methode sqlGroupProjection ist das SQL-SELECT-Fragment, das die Projektionen definiert.

Das zweite Argument repräsentiert die SQL GROUP BY-Klausel, die Teil der Abfrage sein sollte. Diese Zeichenfolge kann ein einzelner Spaltenname oder eine durch Kommas getrennte Liste von Spaltennamen sein.

Das dritte Argument ist eine Liste von Zeichenfolgen, die Spaltenaliasnamen darstellen, die den in SQL ausgedrückten projizierten Werten entsprechen.

Das vierte Argument ist eine Liste von org.hibernate.type.Type-Instanzen, die den in SQL ausgedrückten projizierten Werten entsprechen.

Die obige Abfrage projiziert die kombinierten Höhen von Feldern, die nach Breite gruppiert sind, und liefert Ergebnisse, die wie folgt aussehen:

[[2, 24], [4, 9]]

Jede der inneren Listen enthält 2 Werte. Der erste Wert ist eine Breite und der zweite Wert ist die Summe der Höhen aller Boxen, die diese Breite haben.


SQL-Restriktionen benutzen

Man kann auch auf die von Hibernate bereitgestellten SQL-Restriktions-Funktion zugreifen.

def c = Person.createCriteria()

def peopleWithShortFirstNames = c.list {
    sqlRestriction "char_length(first_name) <= 4"
}

SQL-Einschränkungen können parametrisiert werden, um SQL-Injection-Schwachstellen im Zusammenhang mit dynamischen Einschränkungen zu beheben.

def c = Person.createCriteria()

def peopleWithShortFirstNames = c.list {
    sqlRestriction "char_length(first_name) < ? AND char_length(first_name) > ?", [maxValue, minValue]
}

Beachte: Der Parameter ist SQL. Das im Beispiel referenzierte Attribut first_name bezieht sich auf das Persistenzmodell und nicht auf das Objektmodell wie in HQL-Abfragen. Die Person-Eigenschaft mit dem Namen firstName wird der Spalte first_name in der Datenbank zugeordnet, und man muss in der Zeichenfolge sqlRestriction darauf verweisen.

Das hier verwendete SQL ist nicht unbedingt datenbankübergreifend portierbar.

Skrollbare Ergebnisse

Man kann die ScrollableResults-Funktion von Hibernate verwenden, indem man die scroll-Methode aufruft:

def results = crit.scroll {
    maxResults(10)
}
def f = results.first()
def l = results.last()
def n = results.next()
def p = results.previous()

def future = results.scroll(10)
def accountNumber = results.getLong('number')

Um die Dokumentation zu quotieren:

Ein Ergebnisiterator, mit dem Sie sich in beliebigen Schritten innerhalb der Ergebnisse bewegen können. Das Query / ScrollableResults-Muster ist dem JDBC PreparedStatement / ResultSet-Muster sehr ähnlich, und die Semantik der Methoden dieser Schnittstelle ähnelt den ähnlich benannten Methoden in ResultSet.

Im Gegensatz zu JDBC sind die Ergebnisspalten von Null an nummeriert.


Festlegen von Eigenschaften in der Criteria-Instanz

Wenn ein Knoten in der Builder-Struktur nicht mit einem bestimmten Kriterium übereinstimmt, wird versucht, eine Eigenschaft für das Criteria-Objekt selbst festzulegen. Dies ermöglicht den vollständigen Zugriff auf alle Eigenschaften in dieser Klasse. In diesem Beispiel werden setMaxResults, setFirstResult und setFetchMode für die Criteria-Instanz aufgerufen:

import org.hibernate.FetchMode as FM
...
def results = c.list {
    maxResults(10)
    firstResult(50)
    fetchMode("aRelationship", FM.JOIN)
}


Abfragen mit Eager-Fetching durchführen

Die join-Methode weist die Kriterien-API an, einen JOIN zu verwenden, um die benannten Zuordnungen zu den Task-Instanzen abzurufen.

def criteria = Task.createCriteria()
def tasks = criteria.list{
    eq "assignee.id", task.assignee.id
    join 'assignee'
    join 'project'
    order 'priority', 'asc'
}
Nicht empfohlen für Eins-zu-Viele

Es ist wahrscheinlich am besten, dies nicht für Eins-zu-Viele-Assoziationen zu verwenden, da man höchstwahrscheinlich doppelte Ergebnisse erzielen wird. Verwende stattdessen den select Fetch-Modus:

import org.hibernate.FetchMode as FM
...
def results = Airport.withCriteria {
    eq "region", "EMEA"
    fetchMode "flights", FM.SELECT
}

Obwohl dieser Ansatz eine zweite Abfrage auslöst, um die flights-Assoziationen zu erhalten, erhält man zuverlässige Ergebnisse - auch mit der Option maxResults. fetchMode und join sind allgemeine Einstellungen der Abfrage und können nur auf der obersten Ebene angegeben werden, d. h. Sie können nicht in Projektionen oder Zuordnungsbeschränkungen verwendet werden.

Implizites Eager-Fetching

Ein wichtiger Punkt, den man berücksichtigen sollte, ist, dass Assoziationen automatisch geladen werden, wenn man Assoziationen in die Abfrageeinschränkungen afunimmt. Zum Beispiel in dieser Abfrage..

def results = Airport.withCriteria {
    eq "region", "EMEA"
    flights {
        like "number", "BA%"
    }
}

..würde die flights-Sammlung eifrig über einen JOIN geladen, obwohl der Abrufmodus nicht explizit festgelegt wurde.


Methoden-Referenz

Wenn man den Builder ohne Methodennamen aufgerufen wird..

c { /* */ }

.. ist die Standard-Methode alle Ergebnisse zurückzugeben - was das obere Beispiel äquivalent zu dem folgenden Beispiel macht:

c.list { /* */ }
Methode Beschreibung
list Dies ist die Standardmethode. Es werden alle übereinstimmenden Zeilen zurückgegeben.
get Gibt eine eindeutige Ergebnismenge zurück, d. H. Nur eine Zeile. Die Kriterien müssen so formuliert sein, dass nur eine Zeile abgefragt wird. Diese Methode ist nicht mit einer Beschränkung auf die erste Zeile zu verwechseln.
scroll Gibt eine scrollbare Ergebnismenge zurück.
listDistinct Wenn Unterabfragen oder Zuordnungen verwendet werden, kann es sein, dass in der Ergebnismenge mehrere Male dieselbe Zeile angezeigt wird. Dies ermöglicht das Auflisten nur bestimmter Entitäten und entspricht DISTINCT_ROOT_ENTITY der CriteriaSpecification-Klasse.
count Gibt die Anzahl der übereinstimmenden Zeilen zurück.

Kriterien kombinieren

Man kann mehrere (angehängte) Kriterien-Closures wie folgt definieren:

def emeaCriteria = {
    eq "region", "EMEA"
}

def results = Airport.withCriteria {
    emeaCriteria.delegate = delegate
    emeaCriteria()
    flights {
        like "number", "BA%"
    }
}

Diese Technik erfordert, dass sich jedes Kriterium auf dieselbe Domänenklasse bezieht. Ein flexiblerer Ansatz ist die Verwendung von getrennten Kriterien, wie im folgenden Abschnitt beschrieben wird:

getrennte Kriterien

Getrennte Kriterien sind Kriterienabfragen, die keiner bestimmten Datenbanksitzung / -verbindung zugeordnet sind. Abgelöste Kriterienabfragen werden seit Grails 2.0 unterstützt und haben viele Verwendungsmöglichkeiten, einschließlich der Möglichkeit, allgemeine Abfragen für wiederverwendbare Kriterien zu erstellen, Unterabfragen auszuführen und Stapelaktualisierungen / -löschungen auszuführen.

getrennte Kriterien definieren

Der primäre Einstiegspunkt für die Verwendung der getrennten Kriterien ist die DetachedCriteria-Klasse, die eine Domänenklasse als einziges Argument für ihren Konstruktor akzeptiert:

import grails.gorm.*
...
def criteria = new DetachedCriteria(Person)

Sobald man einen Verweis auf eine getrennte Kriterieninstanz erhalten hat, kann man where oder Kriterien -Abfragen ausführen, um die entsprechende Abfrage aufzubauen. Um eine normale Kriterienabfrage zu erstellen, kann man die Methode build verwenden:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}

Die Methoden in der DetachedCriteria-Instanz mutieren das ursprüngliche Objekt nicht, sondern geben stattdessen eine neue Abfrage zurück. Mit anderen Worten: Man muss den Rückgabewert der build -Methode verwenden, um das mutierte Kriterienobjekt zu erhalten:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
def bartQuery = criteria.build {
    eq 'firstName', 'Bart'
}


getrennte Kriterien ausführen

Im Gegensatz zu regulären Kriterien sind getrennte Kriterien insofern faul, als zum Zeitpunkt der Definition keine Abfrage ausgeführt wird. Sobald eine Abfrage mit getrennten Kriterien erstellt wurde, gibt es eine Reihe nützlicher Abfragemethoden, die in der folgenden Tabelle zusammengefasst sind:

Method Description
list Listet alle übereinstimmenden Entitäten auf
get | find Gibt ein einzelnes übereinstimmendes Ergebnis zurück
count Zählt alle übereinstimmenden Datensätze
exists Gibt true zurück, wenn übereinstimmende Datensätze vorhanden sind
deleteAll Löscht alle übereinstimmenden Datensätze
updateAll(Map) Aktualisiert alle übereinstimmenden Datensätze mit den angegebenen Eigenschaften

Im folgenden Code werden beispielsweise die ersten 4 übereinstimmenden Datensätze aufgelistet, die nach der Eigenschaft firstName sortiert werden:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
def results = criteria.list(max:4, sort:"firstName")

Man kann der list-Methode auch zusätzliche Kriterien übergeben:

def results = criteria.list(max:4, sort:"firstName") {
    gt 'age', 30
}


Die DetachedCriteria-Klasse selbst implementiert auch die Iterable-Schnittstelle, was bedeutet, dass sie wie eine Liste behandelt werden kann.

In diesem Fall wird die Abfrage nur ausgeführt, wenn die einzelnen Methoden aufgerufen werden. Gleiches gilt für alle anderen Iterationsmethoden der Groovy-Sammlung.

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
criteria.each {
    println it.firstName
}

Man kann auch dynamische Finder auf DetachedCriteria-Objekten ausführen, genau wie bei Domänenklassen:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
def bart = criteria.findByFirstName("Bart")

getrennte Kriterien für Unterabfragen benutzen

Im Rahmen einer regulären/angehängten Kriterienabfrage kann man ein DetachedCriteria verwenden, um eine Unterabfrage auszuführen. Wenn man beispielsweise alle Personen finden möchten, die älter als das Durchschnittsalter sind, kann man dies zum Beispiel so lösen:

def results = Person.withCriteria {
     gt "age", new DetachedCriteria(Person).build {
         projections {
             avg "age"
         }
     }
     order "firstName"
 }

In diesem Fall ist die Unterabfrageklasse dieselbe wie die ursprüngliche Kriterienabfrageklasse (d. H. Person), und daher kann die Abfrage verkürzt werden auf:

def results = Person.withCriteria {
    gt "age", {
        projections {
            avg "age"
        }
    }
    order "firstName"
}

Wenn sich die Unterabfrageklasse von der ursprünglichen Kriterienabfrage unterscheidet, müsste man die ursprüngliche Syntax verwenden.

Im vorherigen Beispiel stellte die Projektion sicher, dass nur ein einziges Ergebnis zurückgegeben wurde (das Durchschnittsalter). Wenn seine Unterabfrage mehrere Ergebnisse zurückgibt, müssen verschiedene Kriterienmethoden verwendet werden, um das Ergebnis zu vergleichen. Um beispielsweise alle Personen zu finden, die älter als 18 bis 65 Jahre sind, kann eine gtAll-Abfrage verwendet werden:

def results = Person.withCriteria {
    gtAll "age", {
        projections {
            property "age"
        }
        between 'age', 18, 65
    }

    order "firstName"
}

In der folgenden Tabelle sind die Kriterienmethoden für die Bearbeitung von Unterabfragen zusammengefasst, die mehrere Ergebnisse zurückgeben:

Methode Beschreibung
gtAll größer als alle Unterabfrageergebnisse
geAll größer oder gleich allen Unterabfrageergebnissen
ltAll weniger als alle Unterabfrageergebnisse
leAll kleiner oder gleich allen Unterabfrageergebnissen
eqAll gleich allen Unterabfrageergebnissen
neAll nicht gleich allen Unterabfrageergebnissen


Hibernate Query Language (HQL)

Siehe GORM-Dokumentation für einen Überblick über HQL sowie die Hibernate-Dokumentation für eine ausführliche Version


TODO

#8 Advanced GORM Features | Überfliegen und schauen was wichtig sein könnt

#9 Programmatic Transactions | ?

#10 Data Services

#11 Mutliple DataSources | auslassen und "Siehe Dokumentation"

#12 Multi-Tenancy | ???

#13 Constraints

#14 Testing

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
}