Grails-Tutorial


Autorin: Dr. Silvia Rothen, rothen ecotronics, Bern, Schweiz
Letzte Überarbeitung: 13.05.18


Grails ist ein javabasiertes Framework zur Erstellung von Webapplikationen mit Datenbankanbindung. Das vorliegende Tutorial konzentriert sich vor allem auf die Einbindung bestehender ERP-Datenbanken im Enterprise-Bereich. Dabei habe ich vor allem Aufgabenstellungen evaluiert, die mir bis jetzt beim Entwickeln von Webapplikationen mit ADF Business Components und ADF Faces begegnet sind.

Mein anfänglicher Enthusiasmus ist einer gewissen Skepsis gewichen. Wie ich Grails in Bezug auf die Anbindung von existierenden Oracle-Datenbanken einschätze, lesen Sie in "Grails and real databases - a bumpy road".

Achtung: Dieses Tutorial entstand über mehrere Jahre. Begonnen habe ich ungefähr mit der Version 0.5. Nicht alles stimmt noch für die aktuelle Version. Letzte Änderungen stammen vom Februar 2012 und beziehen sich auf die Grails-Version 2.0.0.


Inhaltsverzeichnis


Was ist Grails

Zurück zum Inhaltsverzeichnis


Schneller Einstieg

Bezieht sich auf Grails-Version 1.0.3 unter Windows.

  1. Falls nicht bereits vorhanden, ein Java JDK installieren, mindestens 1.4, aber für Annotations und ähnliches möglichst Version 1. 5 oder 1.6.
    Achtung: es muss ein JDK vorhanden sein, JRE reicht nicht. Das kein JDK vorhanden ist, merkt man u.a. daran, dass im Verzeichnis \bin\ die Datei native2ascii.exe nicht vorhanden ist
  2. Grails von grails.org herunterladen und und in Verzeichnis entzippen, z.B
    C:\Programme\programming\grails-1.0.3
    Neu gibt es für Windows auch einen Installer, der ein Untermenü ins Startmenü einbindet, aber die Umgebungsvariablen muss man nach wie vor manuell setzen.
  3. In Umgebungsvariablen (Arbeitsplatz - Systemeigenschaften - Erweitert) neue Systemvariable erstellen
    set GRAILS_HOME=C:\Programme\programming\grails-1.0.3
  4. Systemvariable für JAVA_HOME erstellen oder anpassen, falls bereits vorhanden.
    set JAVA_HOME=C:\Programme\programming\javaclasses\jdk1.6.0_05
    Unter Windows lassen sich die Systemvariablen auf der Kommandozeile z.B. mit echo %JAVA_HOME% überprüfen
  5. In Umgebungsvariablen (Arbeitsplatz - Systemeigenschaften - Erweitert) Path ergänzen oder falls bereits vorhanden abändern mit
    %JAVA_HOME%\bin;%GRAILS_HOME%\bin
  6. Von jetzt an passiert alles auf der Kommandozeile:
    In Verzeichnis gehen, wo neue Grails-Applikation erzeugt werden soll, z.B.
    cd C:\dateienmitback\programming\grails\
  7. Applikation anlegen mit
    grails create-app
    Namen für Applikation angeben, z.B. mybookmarks 
  8. Ins Verzeichnis der Applikation wechseln
    cd mybookmarks
  9. Ein neues Objekt erzeugen mit
    grails create-domain-class
    Objektnamen eingeben, z.B. user
    Kann in Kleinschrift eingegeben werden, Klasse erhält automatisch den Namen "User".
  10. Klasse User suchen und öffnen, z.B.
    C:\dateienmitback\programming\grails\mybookmarks\grails-app\domain\User.groovy
  11. Klasse mit ein paar Feldern ergänzen
    class User {
      static hasMany = [bookmarks:Bookmark]
      String loginname
      String email
    }
  12. DB-Mapping erzeugen mit grails generate-all
    Wenn nach der Domain-Klasse gefragt wird, den Namen eingeben, aber mit Grossbuchstaben am Anfang, also User.
  13. Applikation starten mit
    grails run-app
    Wenn Port 8080 bereits belegt ist, weil man z.B. Oracle XE installiert hat:
    grails -Dserver.port=9090 run-app 
  14. Wenn Meldung "Server running".. erscheint, Applikation laden, z.B. mit
    http://localhost:9090/mybookmarks/

Versionswechsel

Versionswechsel von Grails führen dazu, dass man auch seine Projekte mit grails upgrade updatet. Je nach Version sind weitere Änderungen nötig.

JQuery und der Wechsel zu Version 2.0.0

Ab Grails-Version 2 ist JQuery automatisch die Standard-JavaScript-Library. Damit erfolgt die Einbindung anders als früher. Anstelle von

<g:javascript library="jquery" />

kommen die folgenden zwei Zeilen in den Header einer Seite, beziehungsweise in die Template-Seite, wenn die Einbindung für alle Seiten gelten soll:

<r:layoutResources/>
<r:require modules="jquery-ui, blueprint"/>

Version 2.0.0 und H2

Neuerdings wird die H2-DB standardmässig eingebunden. Dazu gehört auch eine Konsole, die man während der Ausführung des Projektes mit projekt/dbconsole aufrufen kann, also z.B. mit http://localhost:9090/mybatis/dbconsole/

Allerdings gibt es bei der Konsole auch einen Stolperstein: Die vorgeschlagene URL entspricht nicht unbedingt der im Projekt verwendeten URL. In diesem Feld muss unbedingt dasselbe eingetragen werden wie in der Konfiguration der Datasource:

Insbesondere wenn man (so wie ich) übersieht, dass am einen Ort "file" und am andern "mem" steht, dann wird man sich wundern, wo die Daten eigentlich geblieben sind.

Version 2.0.0 und Oracle

Und gleich noch ein Stolperstein: Wer Grails mit Java 6 und Oracle einsetzt, muss auch den entsprechenden Oracle Treiber ersetzen, d.h. statt ojdbc14.jar kommt jetzt ojdbc6.jar ins lib-Verzeichnis. Den Driver kann man unter http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-10201-088211.html herunterladen. Ausserdem schadet es nicht, grails clean abzusetzen, bevor man das Projekt nach dem Update startet.

Wechsel von Version 1.0.3 zu 1.0.4

Für den Upgrade unbedingt zuerst für jedes Projekt die folgenden zwei Befehle auf der Kommandozeile absetzen:

Wechsel von Version 0.5.6 zu 0.6

Folgende Anpassungen sind generell und in den Projekten nötig:Ein paar weitere Änderungen, die mir aufgefallen sind:

Zurück zum Inhaltsverzeichnis


Ausführen einer bestehenden Applikation

  1. Ins Stammverzeichnis der Applikation wechseln
  2. grails -Dserver.port=9090 run-app aufrufen 
  3. Wenn Meldung "Server running".. erscheint, Applikation laden, z.B. mit
    http://localhost:9090/mybookmarks/user/list
Achtung: für die Migration älterer Versionen auf die Version 0.5.6 gibts es ein Migrationsskript. Allerdings ist es mir nur teilweise gelungen, eine Applikation damit so zu migrieren, dass sie nachher wieder lief (ich hab auch nicht wahnsinnig lange probiert.

Zurück zum Inhaltsverzeichnis


Grails und Datenbankanbindung

Achtung: Defaultmässig ist Grails für die Verwendung einer RAM-Datenbank konfiguriert. Sobald man die Applikation stoppt, sind alle Daten weg!

Datenquellen

Eine Standardinstallation von Grails ist bereits für die Verwendung einer In-Memory-Datenbank (HSQLDB) eingerichtet. Mit den Datasource-Dateien im Verzeichnis grails-app/conf/ lässt sich Grails für die Verwendung beliebiger Datenbanken konfigurieren. Vorgesehen sind in Version 0.5.6 drei Datasource-Dateien für Entwicklungs-, Test- und produktive Umgebung:

Ab Version 0.6 sind diese 3 Datenquellen in einer einzigen Datei DatSource.groovy in den folgenden 3 Closures abgelegt:

Die Auswahl der jeweiligen Umgebung erfolgt über den Aufruf grails run-app. Ohne weitere Angaben wird die Entwicklungsumgebung gestartet. Soll stattdessen die produktive oder die Testumgebung gestartet werden, dann wird prod oder dev in den Befehl eingefügt, also z.B. grails prod run-app

Zurück zum Inhaltsverzeichnis

Grails an Oracle XE anbinden

Soll statt der In-Memory-Datenbank (HSQLDB) eine richtige Datenbank verwendet werden, dann muss Grails zuerst dafür konfiguriert werden. Hier eine kurze Beschreibung für die Verwendung von Oracle XE. Eine ausführliche Beschreibung findet sich unter:

http://www.oracle.com/technology/pub/articles/grall-grails.html

Oracle Konfiguration auf der angegebenen Webseite funktioniert mit zwei Anpassungen

Die wichtigen Punkte für die Konfiguration von Grails mit Oracle:

Konfiguration in Version 0.6
dataSource {
  pooled = false
  driverClassName = "org.hsqldb.jdbcDriver"
  username = "sa"
  password = ""
}

// environment specific settings
environments {
  development {
    dataSource {
      url = "jdbc:oracle:thin:@localhost:1521:XE"
      driverClassName = "oracle.jdbc.OracleDriver"
      username = "meinname"
      password = "xxxxxx"
    }
  }
....
Hier noch die Konfiguration von Version 0.5.6
class DevelopmentDataSource {

  boolean pooling = true
  String dbCreate = "update" 
  String url = "jdbc:oracle:thin:@localhost:1521:XE"

  String driverClassName = "oracle.jdbc.OracleDriver"

  String username = "meinname"
  String password = "xxxxxx"
}

Achtung: 

Mit dbCreate lässt sich einstellen, ob die Applikation selbst Tabellen anlegen, aktualisieren und verändern darf, d.h. ob sie das Datenbankschema ändern darf.  Die drei Optionen sind

Wenn Sie mit bestehenden Tabellen arbeiten (die Grails-Literatur spricht mit einer gewissen Überheblichkeit von Legacy-Databases) oder ihr Datenbankschema nur manuell ändern möchten, dann löschen Sie unbedingt die folgende Zeile:

String dbCreate = "update" 

Quelle für diesen Tipp:
Jason Rudolph, InfoQ, Getting Started with Grails

Die Tabellen werden übrigens erst beim Befehl run-app angelegt oder geändert, nicht bereits beim Scaffolding mit generate-all. 

Zurück zum Inhaltsverzeichnis

1:1-, 1:n-, m:n-Beziehungen

Für die Persistenzschicht sind in Grails die Domain-Klassen zuständig. Verschiedene Arten von Beziehungen werden dort folgendermassen umgesetzt.

Pseudo-1:1-Beziehung in Version 1.0.3

Das vorliegende Beispiel lehnt sich an das Face-Nose-Beispiel aus Kapitel 5 der Reference Documentation an, baut dieses aber in den folgenden Punkten aus, so dass es den Anforderungen einer bestehenden Datenbank entspricht:

Die in der Dokumentation vorgeschlagene Lösung kann unter diesen Voraussetzungen nicht verwendet werden: Damit das Löschen und Speichern kaskadiert, muss die Mastertabelle eine Beziehung zur Detailtabelle aufweisen, was auf Datenbankebene als Fremdschlüssel in Master- und Detailtabelle realisiert wird. Da eine Nase zu einem Gesicht gehört, wird Face als die Master- und Nose als die Detailtabelle betrachtet.

Hier der Vorschlag aus der Dokumentation:

class Face {
  Nose nose
}


class Nose {
  static belongsTo = [face:Face]
}

Statt der vorgeschlagenen Variante benutze ich eine 1:n Beziehung, die über eine Constraint unique auf dem Fremdschlüssel in der Detailtabelle in eine 1:1-Beziehung umgewandelt wird.

class Face {
  static hasMany = [noses: Nose]
}

class Nose {
  static belongsTo = [face:Face]
  static mapping = {
    columns {
      face column:"FACENR"
    }
  }
  static constraints = {
    face unique:true
  }
}

Mit static mapping wird zudem auf einen Fremdschlüssel gemapped, der nicht face_id, sondern FACENR heisst.

Einfache 1:n Beziehung

Das Beispiel bezieht sich auf Version 1.0.3. Das folgende Beispiel zeigt, wie eine 1:n-Beziehung zwischen zwei Domain-Klassen (d.h. Tabellen) erstellt werden kann. Dies funktioniert auch in Oracle, solange der User in der Datasource auch die notwendigen Grants hat, um Tabellen zu erstellen und abzuändern. In diesem Beispiel gehe ich von Grails-konformen Tabellen aus, d.h. die Namenskonventionen werden eingehalten, die Schlüssel kommen von Grails, nicht aus einer Sequence etc. Weiter unten finden sich Hinweise, wie man mit bestehenden, nicht grailskonformen Datenbanken und vorhandenen Daten arbeitet.

Das Beispiel geht (wie die Grails Reference Documentation) von den folgenden zwei Domain-Klassen aus:

Die Mastertabelle sieht folgendermassen aus:

class Author {
  static hasMany = [bookList : Book]
  String name
  String toString() {
    return "${id} ${name}"
  }
}

Die Detailklasse dagegen benötigt zwei Sachen: erstens ein Feld vom Typ der Masterklasse und zweitens den Vermerk static belongsTo...

class Book {
  static belongsTo = [Author]
  String title
  Author author
  String toString() {
    return "${id} ${title}"
  }
}

Die Methode toString() dient übrigens dazu, dass DropDown-Felder gleich von Anfang an einen brauchbaren Text anzeigen. Die Zeile mit belongsTo ist nur notwendig, wenn beim Löschen von Datensätzen in der Tabelle Author auch eine Löschweitergabe an Book erfolgen soll. Für die Datenbanktabellen bedeutet dies, dass ein Delete in der Master-Tabelle Author ein cascading Delete in der Detailtabelle Book auslöst.

Der Befehle "static belongsTo" darf in einer Klasse nur einmal vorkommen. Hat die Klasse Fremdschlüssel von mehreren anderen Klassen, dann lautet die Syntax folgendermassen:

static belongsTo = [User, Category, Keyword]

Um diese 1:n-Beziehung mit Daten zu füllen, benützt man addTo....

Author authorInstance = new Author(name: "Silvia Rothen")
Book bookInstance = new Book(title: "Kohlendioxid und Energie")
authorInstance.addToBookList(bookInstance)

Achtung, hier hat das API seit Version 0.5 geändert, der Aufruf
authorInstance.addBook(bookInstance) ist deprecated! In älteren Grails-Büchern und Artikeln findet man manchmal noch die veraltete Schreibweise.

m:n-Beziehung

Bei der m:n-Beziehung läuft es ähnlich wie im Fall der 1:n-Beziehung. Es kommt einfach noch ein "static hasMany" dazu. Die Zwischentabelle, mit der eine m:n-Beziehung datenbankseitig realisiert wird, tritt in Grails nicht als Domain-Klasse in Erscheinung. Nur bei einer nichtgrailskonformen Namensgebung tritt sie im static mapping der beiden anderen Tabellen in Erscheinung. Wie bei der 1:n-Beziehung muss aber genau eine Klasse mit belongsTo als untergeordnete Klasse gekennzeichnet werden. Wird ein Master-Objekt gelöscht, dann werden alle zugehörigen Detail-Objekte mitgelöscht!

Es gibt die folgenden 2 Tabellen, zwischen denen eine Many-to-Many-Relation besteht:

Die Zwischentabelle heisst GRAILS-konform AUTHOR_BOOK. Sie hat einen zusammengesetzten Primärschlüssel, der aus den folgenden 2 Felder besteht:

Nun kann das Mapping direkt in den Domain-Dateien erstellt werden. Die entsprechenden Einträge in den Domain-Klassen sehen folgendermassen aus.

Author.groovy:

class Author {
  static mapping = {
    table 'AUTHOR'
    bookList column:'BOOK_ID', joinTable:'AUTHOR_BOOK'
    columns {
      id (column:'ID')
      version (column:'VERSION')
      name (column:'NAME')
    }
  }
  java.lang.String name
  static hasMany = [ bookList : Book ]
  
  static constraints = {
    id(nullable: false, size: 0..19)
    version(nullable: false, size: 0..19)
    name(size: 1..1020, blank: false)
    bookList()
  }
}

Book.groovy

class Book {
  static mapping = {
    table 'BOOK'
    authorList column:'AUTHOR_ID', joinTable:'AUTHOR_BOOK'
    columns {
      id (column:'ID')
      version column:'VERSION'
      title column:'TITLE'
    }
  }
  java.lang.String title
  static hasMany = [ authorList : Author ]
  static belongsTo = [Author]

  static constraints = {
    id(nullable: false, size: 0..19, type:java.math.BigDecimal)
    version(nullable: false, size: 0..19, type:java.math.BigDecimal)
    title(size: 1..1020, blank: false)
    authorList()
  }
}

Für die Hilfstabelle AUTHOR_BOOK ist keine Domainklasse nötig.

Beim Scaffolding gegen Oracle XE wird eine Zwischentabelle generiert, z.B. mit dem Tabellennamen BOOK_KEYWORD, wobei die zweite Namenskomponente jene Klasse mit dem belongsTo ist. Diese Zwischentabelle enthält erwartungsgemäss nichts anderes als die zwei Fremdschlüssel zu den anderen Tabellen.

Achtung:

Das Scaffolding funktioniert für die Views bei einer m:n-Beziehung nicht (zumindest nicht in Version 1.0.3). Die entsprechenden Seiten müssen manuell angepasst werden.

Zurück zum Inhaltsverzeichnis

Mapping bestehender Datenbanken mit ORM DSL ab Version 1

Der Text in diesem Abschnitt bezieht sich auf Grails ab Version 1.0.4

HQL mit executeQuery

In gewissen Situationen kommt man an native SQL nicht vorbei. Eine Möglichkeit, die dem schon relativ nahe kommt, ist die Verwendung der Methode executeQuery mit HQL in einer Domain-Klasse. Allerdings gibt es ein paar Stolpersteine:

Schauen wir uns zuerst die Domain-Klasse an:

class Book2ExecuteQuery {
  String title
  String description

  static mapping = {
    table 'BOOK2'
  }

  static constraints = {
    title size: 1..255, blank: false
    description nullable: true, size: 0..255
  }
}

Speziell ist daran nur das Mapping der Tabelle. Beim Kontroller wird es viel interessanter. Die Methode executeQuery dürfte im Normalfall v.a. in der Closure list zum Zuge kommen, da man dort am ehesten die angezeigten Datensätze beschränken möchte. Damit ist die vorgestellte Technik eine Alternative zur WHERE-Clause, die sich ja bisher nur mit Annotations realisieren lässt. 

class Book2ExecuteQueryController {
  ...
  def list = {
    if(!params.max) params.max = "5"
    if(!params.offset) params.offset = "0"
    if(!params.sort) params.sort = "id"
    if(!params.order) params.order = "desc"
    def results
    results = Book2ExecuteQuery.executeQuery(
      """SELECT new map(b.id as id, b.version as version,
      b.title as title, b.description as description)

      FROM Book2ExecuteQuery b
      WHERE b.id >= :minId
      ORDER BY b.${params.sort} ${params.order}
      """, [minId: 200L], [max: params.max.toInteger()
      offset: params.offset.toInteger()])

    return [book2ExecuteQueryInstanceList: results]
  }
}

Die vorliegende Lösung unterstützt sowohl die Sortierung über die Spaltenköpfe wie das Paging. Man beachte auch das params.max.toInteger(), das nötig ist, weil Parameter normalerweise Strings zurückliefern.

Native SQL mit createSQLQuery

Eine zweite Möglichkeit, native SQL für ein Domain-Objekt zu verwenden, ist die Methode createSQLQuery. Diese Methode gehört nicht zur Domainklasse, sondern zur Hibernate-Session. Mit der Methode addEntity kann das Resultat der Query aber auf eine Domain-Objekt gemapped werden.

Das Beispiel zeigt einige interessante Punkte:

Fangen wir mit der Domainklasse an. Hier sind eigentlich nur die zwei SQL Statements sowie das "cache 'read-only'", mit dem die Klasse nur lesbar gemacht wird, interessant. Das static mapping "mutable false", das eigentlich der Hibernate-Syntax entsprechen würde, hat laut einer Nabble-Diskussion gar nie funktioniert, wurde aber in Version 1.0.4 aufgrund eines Bugs nicht abgefangen. Ab Version 1.1. funktioniert es definitiv nicht mehr.

Im SQL sollten Sie ein paar Punkte beachten: 

class EmployeeCreateSqlQuery {
  static final SQL_LIST = """
    SELECT
      LEVEL,
      emp.employee_id as id,
      man.last_name || ' ' || man.first_name AS manager,
      emp.last_name || ' ' || emp.first_name AS employee,
      dep.department_name as departmentname
    FROM employees emp,
      employees man,
      departments dep
    WHERE LEVEL = :pLevel
      AND emp.manager_id = man.employee_id (+)
      AND emp.department_id = dep.department_id
    START WITH emp.manager_id IS NULL
    CONNECT BY PRIOR emp.employee_id = emp.manager_id
  """

  static final SQL_GET = """
    SELECT
      LEVEL, 
      emp.employee_id as id,
      man.last_name || ' ' || man.first_name AS manager,
      emp.last_name || ' ' || emp.first_name AS employee,
      dep.department_name as departmentname
    FROM employees emp,
      employees man,
      departments dep
    WHERE emp.manager_id = man.employee_id (+)
      AND emp.department_id = dep.department_id
      AND emp.employee_id = :pId
    START WITH emp.manager_id IS NULL
    CONNECT BY PRIOR emp.employee_id = emp.manager_id
  """
  

  static mapping = {
    table 'EMPLOYEES'
    version false
    cache 'read-only'
  }

  Byte           level
  String         manager
  String         employee
  String         departmentname

  static constraints = {
    id              (nullable: false, size: 1..6)
    level           (nullable: false, inList:[1,2,3,4])
    manager         (nullable: true, size: 0..46)
    employee        (nullable: true, size: 0..46)
    departmentname  (nullable: true, size: 1..30)
  }
}

Wenn wir die Klasse soweit definiert haben, dann lässt sich das Gerüst für Controller und View mit "grails generate-all" erstellen. Da wir eine Domain-Klasse haben, die nicht aktualisierbar ist, können wir im Controller alle Closures ausser jenen für list und show löschen. Auch bei den Views bleiben nur list.gsp und show.gsp übrig.

Beim Controller muss man als erstes die Session Factory einfügen. Für die Liste braucht es zwei SQL-Statements, nämlich neben jenem für die Liste selbst noch eines, welches das Total für das Paging berechnet.

class EmployeeCreateSqlQueryController {

  org.hibernate.SessionFactory sessionFactory

  def index = {redirect(action: list, params: params)}

  def list = {
    if (!params.max) params.max = 10
    if (!params.offset) params.offset = 0
    if (!params.level) params.level = 3
    if (!params.sort) params.sort = "id"
    if (!params.order) params.order = "ASC"

    String sqlList = EmployeeCreateSqlQuery.SQL_LIST + 
      " ORDER BY ${params.sort} ${params.order}"

    String sqlTotal = """
      SELECT COUNT(*) total 
      FROM (${EmployeeCreateSqlQuery.SQL_LIST})"""

    def query = sessionFactory.currentSession
      .createSQLQuery(sqlList)

      .addEntity("employeeCreateSqlQuery", 
         EmployeeCreateSqlQuery.class)

    query.setParameter("pLevel", params.level)
    query.setFirstResult(params.offset.toInteger())
    query.setMaxResults(params.max.toInteger())
    def list = query.list()

    def queryCount = sessionFactory.currentSession
      .createSQLQuery(sqlTotal)
    queryCount.setParameter("pLevel", params.level)
    def listCount = queryCount.list()

    return [employeeCreateSqlQueryInstanceList: list,
      total: listCount[0]]
  }

  def show = {
    def query = sessionFactory.currentSession
      .createSQLQuery(EmployeeCreateSqlQuery.SQL_GET)
      .addEntity("employeeCreateSqlQuery",
        EmployeeCreateSqlQuery.class);
    query.setParameter("pId", params.id)
    def employeeCreateSqlQueryInstance = query &&
      query.list().size() == 1 ? query.list()[0] : null;

    if (!employeeCreateSqlQueryInstance) {
      flash.message = """EmployeeCreateSqlQuery not found
        with id ${params.id}"""
      redirect(action: list)
    }
    else {return [employeeCreateSqlQueryInstance:
      employeeCreateSqlQueryInstance]}
  }
}

Die show.gsp-Seite wird so verwendet, wie sie generiert wurde, die list.gsp-Seite erfährt dagegen ein paar minimale Änderungen, damit das Paging und das Sortieren via Spaltenköpfe funktioniert.

<html>
  <head>
    <meta http-equiv="Content-Type" 
     content="text/html; charset=UTF-8"/>
    <meta name="layout" content="main"/>
    <title>Employee List</title>
  </head>
  <body>
    <div class="nav">
      <span class="menuButton">
        <a class="home" href="${createLinkTo(dir: '')}">Home</a>
      </span>
    </div>
    <div class="body">
      <h1>EmployeeCreateSqlQuery List</h1>
      <g:if test="${flash.message}">
        <div class="message">${flash.message}</div>
      </g:if>
      <p>Level:
      <g:link action="list" params="[level:1]">1</g:link>
      <g:link action="list" params="[level:2]">2</g:link>
      <g:link action="list" params="[level:3]">3</g:link>
      <g:link action="list" params="[level:4]">4</g:link>
      </p>
      <div class="list">
        <table>
          <thead>
            <tr>
              <g:sortableColumn property="id" title="Id" 
                params="${[level: params.level]}"/>
              <g:sortableColumn property="level" title="Level" 
                params="${[level: params.level]}"/>
              <g:sortableColumn property="manager" title="Manager" 
                params="${[level: params.level]}"/>
              <g:sortableColumn property="employee" title="Employee" 
                params="${[level: params.level]}"/>
              <g:sortableColumn property="departmentname" 
                title="Department Name" 
                params="${[level: params.level]}"/>
            </tr>
          </thead>
          <tbody>
            <g:each in="${employeeCreateSqlQueryInstanceList}" 
              status="i" var="employeeCreateSqlQueryInstance">

              <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
                <td>
                  <g:link action="show"
                    id="${employeeCreateSqlQueryInstance.id}">
                    ${fieldValue(bean: employeeCreateSqlQueryInstance,
                      field: 'id')}
                  </g:link>
                </td>
                <td>
                  ${fieldValue(bean: employeeCreateSqlQueryInstance,
                    field: 'level')}
                </td>
                <td>
                  ${fieldValue(bean: employeeCreateSqlQueryInstance,
                    field: 'manager')}
                </td>
                <td>
                  ${fieldValue(bean: employeeCreateSqlQueryInstance,
                    field: 'employee')}
                </td>
                <td>
                  ${fieldValue(bean: employeeCreateSqlQueryInstance,
                    field: 'departmentname')}
                </td>
              </tr>
            </g:each>
          </tbody>
        </table>
      </div>
      <div class="paginateButtons">
        <g:paginate total="${total}" params="${params}"/>
      </div>
    </div>
  </body>
</html>

Zugewiesener Primärschlüssel

Die folgende Methode bezieht sich auf Version 1.3.5 oder später. Dies ist wichtig, da es in Vorversionen zum Teil tückische Bugs gab. Das Ziel ist es, einem Primärschlüssel manuell Werte zuzuweisen. Der Primärschlüssel entspricht dabei abgesehen von der manuellen Zuweisung den Konventionen, d.h. er ist numerisch und heisst id. Der Trick ist, dass man im Mapping für id eine Property generator:"assigned" definiert. Die Domain-Klasse sieht so aus:

class Genre {
  String title

  static mapping = {
    id(generator:'assigned')
  }
}

Wichtig ist, dass generator:'assigned' innerhalb von mapping steht und nicht etwa innerhalb von constraints. Dass ich dieses kleine Detail übersehen habe, hat mich ziemlich viel Zeit gekostet!

Einen Bug in Bezug auf zugewiesene Primärschlüssel gibt es in Grails in Version 1.3.5 immer noch: der Wert für id wird nicht automatisch aus den Parametern geholt, sondern man muss ihn explizit zuweisen. Für die closure save() muss also zwingend die fett markierte Zeile im folgenden enthalten:

def save = {

  def genreInstance = new Genre(params)

  //explizite Zuweisung nötig wegen Bug, zumindest bis Version 1.3.5
  genreInstance.id = params.id.toInteger()

  if (genreInstance.save(flush: true)) {
    flash.message = "${message(code: 'default.created.message',
      args: [message(code: 'genre.label', default: 'Genre'),
        genreInstance.id])}"
    redirect(action: "show", id: genreInstance.id)
  } else {
    render(view: "create", model: [genreInstance: genreInstance])
  }
}

Dass es sich hierbei um einen Bug handelt sieht man auf dieser Seite.

Offene Punkte in ORM DSL unter Grails 1.0.3

Für die folgenden Probleme habe ich in ORM DSL noch keine Lösung gefunden (aber teilweise mit Hibernate Annotations):

Zurück zum Inhaltsverzeichnis

Mapping bestehender Oracle-Datenbanken mit Annotations

Zur Zeit (d.h. November 2008) und mit der Grails-Version 1.0.4 ist das Mapping mit Annotations vermutlich der beste Kompromiss für Applikationen mit existierenden Datenbanken: Die Version mit Annotations ist besser in Grails-Domain-Klassen integriert als ein Mapping mit Hibernate-XML-Dateien. Gleichzeitig kann man aber viele anspruchsvolle Mapping-Aufgaben erledigen, die mit ORM DSL noch nicht möglich sind, z.B. Einbezug von Tabellenspalten, die read-only sind oder Beschränkung auf Zeilen anhand einer WHERE-Clause.

Registrierung in hibernate.cfg.xml

Damit die annotierten Klassen verwendet werden können, müssen Sie in grails-app/conf/hibernate/hibernate.cfg.xml registriert werden:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    ...  
    <mapping class="AthleteAnnotated" />
  </session-factory>
</hibernate-configuration>

Einträge in Datasource.groovy

Damit mit annotierten Domain-Klassen gearbeitet werden kann, braucht es die folgenden zwei Einträge in DataSource.groovy:

Import-Statement

Soll das Mapping in den Domain-Klassen mit Annotations erfolgen, dann muss unbedingt das folgende Import-Statement am Anfang der Klasse stehen:

import javax.persistence.*

Leider wird dies bei ganz vielen Beispielen im Web unterschlagen!

Mit Oracle-Sequence generierter Primärschlüssel 

Der folgende Code aus einer Domainklasse zeigt

  1. Primärschlüssel, der nicht der Namenskonvention entspricht
  2. aus Oracle-Sequence generierten Primärschlüssel
    1. sequenceName ist der Name der Sequence in der DB
    2. allocationSize=1 bewirkt, dass jeder Wert von der Datenbank geholt wird. Das verhindert Sprünge in der Nummerierung bei Neustart der Applikation, kostet aber Performance, weil jeder Wert von der DB geholt wird.

import javax.persistence.*

@Entity
@Table(name="ATHLETE")
@SequenceGenerator(name="athlete_seq", 
  sequenceName="ATHLETE_SEQ", 
  allocationSize=1)
class AthleteAnnotated implements Serializable {
  @Id
  @GeneratedValue (strategy=GenerationType.SEQUENCE,
    generator="athlete_seq")
  @Column (name="ATHLETEID")
  Long id
  ....
}

Optimistic Locking mit Feld Version

Wenn für eine Tabelle optimistic locking zugelassen sein soll, dann benötigt sie ein Feld für die Version (im Normalfall ein numerisches Feld). Dieses Feld muss auch entsprechend annotiert werden, nämlich mit

class AthleteAnnotated implements Serializable {
  ...
  @Version
  Long version
  ...
}

Read-Only-Tabelle

Soll eine Klasse nur lesbar, aber nicht änderbar sein, dann lässt sich dies in der annotierten Entity mit dem Attribut mutable festlegen:

import javax.persistence.*

@Entity (mutable=false)
@Table(name="emp")
class Employee implements Serializable {
...

Read-only Spalte in aktualisierbarer Tabelle

Im konkreten Fall ging es um eine Tabelle, die ein mit SYSDATE abgefülltes Einfügedatum enthielt. Die Tabelle ist aktualisierbar, dieses Feld dagegen soll zwar angezeigt werden, aber den ursprünglichen Wert behalten. Die ORM-DSL-Lösung scheitert hier beim Insert: der einfache Trick, für das Feld kein Eingabefeld im Formular anzuzeigen, bringt nichts, da das Insert-Statement gnadenlos null in das Feld schreibt. Mit Annotations klappt es dagegen, weil das Feld in Insert- und Update-Statements ausgenommen wird:

@Column(name="INSERTDATE",
  insertable=false, updatable=false)
Date insertdate

Where-Clause für Subset einer Tabelle

Arbeitet man mit einer existierenden Datenbank (in meinem Fall mit einem ausgewachsenen ERP), dann hat man das Problem, dass u.U. Tausende von Datensätzen existieren, aber für die Webanwendung nur ein paar Dutzend davon relevant sind. In diesem Fall möchte man die Datenmenge gleich von Anfang an mit einer Where-Clause beschränken. Mit ORM DSL geht dies zumindest in Grails 1.0.3 noch nicht, aber mit Annotations kriegt man es hin:

import javax.persistence.*
import org.hibernate.annotations.Where

@Entity
@Table(name="ATHLETE")
@Where(clause="""ATHLETEID
  > 6""")
class AthleteAnnotated implements Serializable {
  ...
}

Und das schönste daran: Es klappt sogar mit den dreifachen Anführungszeichen, so dass man richtig umfangreiche WHERE-Clauses schreiben kann.

Achtung: Für diesen Fall benötigt man schon zwei Import-Statements.

Mit Annotation Datentyp CLOB erzwingen

Um für eine Spalte den Oracle-Datentyp CLOB zu erzwingen, versieht man die Spalte mit den folgenden Annotationen:

@Column(name="text", nullable=true, columnDefinition="clob")
String text

Es empfiehlt sich übrigens, die Spalte nullable zu machen, denn bei mehr als 4000 Zeichen lässt sich der Feldinhalt nicht mehr direkt mit einem INSERT einfügen. Es kommt dann zu dieser "Exception:  java.sql.SQLException: ORA-01704: Zeichenfolge zu lang".

Offene Fragen

Folgende Fragen konnte ich bis jetzt noch nicht beantworten:

Zurück zum Inhaltsverzeichnis

Mapping bestehender Oracle-Datenbanktabellen mit XML

Soll Grails an eine bestehende Datenbank angebunden werden, die nicht den Grails-Konventionen entspricht, dann muss ein Mapping gemacht werden. Mit ORM DSL lässt sich noch nicht ganz alles realisieren. Und manchmal ist es auch von Vorteil, das Mapping nicht im Code, sondern in externen XML-Dateien zu haben. In diesen Fällen kommt ein Mapping mit Hibernate-XML-Dateien zum Zuge. Vor der Grails-Version 1 war dies ohnehin die einzige Variante, nicht-grails-konforme Datenbanken einzubinden.

Wenn man für existierende Datenbanken diese Mapping-Dateien automatisch erzeugen möchte, dann greift man zu den "Hibernate Tools for Eclipse and Ant", mit diesen Tools lässt sich Reverse Engineering von bestehenden Tabellen erzeugen. Für Grails erstellt man normalerweise pro Tabelle oder Abfrage eine Hibernate-Mapping-Datei. Die Hibernate-Mapping-Dateien (*.hbm.xml) kommen ins Verzeichnis \hibernate. Die XML-Datei ist ein Ersatz für eine Domain-Klasse, sie kann vom zugehörigen Controller direkt verwendet werden.

Registrierung in hibernate.cfg.xml

Die Mapping-Dateien werden ebenso wie annotierte Domain-Klassen in die Konfigurations-Datei eingetragen, aber nicht als Klassen sondern als Ressourcen. Die Konfigurationsdatei hibernate.cfg.xml muss auch bei Verwendung einer externen Datenbank keine Einträge für Treiber haben, dies übernehmen nach wie vor die Datasource-Dateien.

So sieht die Konfigurationsdatei hibernate.cfg.xml aus, wenn nicht grailskonforme Tabellen eingebunden werden:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration
  PUBLIC 
  "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
      <mapping resource="Country.hbm.xml"/>
    </session-factory>
</hibernate-configuration>

Jede nicht konforme Tabelle erhält eine eigene XML-Mapping-Datei (siehe unten), deren Dateiname in die Konfigurationsdatei als resource eingetragen wird.

Das Hibernate-Mapping dient übrigens nicht nur dazu, bestehende Datenbank-Tabellen zu mappen, sondern kann auch beim Generieren von Tables dafür eingesetzt werden, Datentypen und Feldlängen präzise festzulegen, statt dies Grails zu überlassen.

Mapping des Primärschlüssels

Hier das XML-Mapping für eine Tabelle, bei welcher der Name des Schlüsselfeldes nicht mit der Grailskonvention, d.h. "id", übereinstimmt.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC 
  "-//Hibernate/Hibernate Mapping DTD//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="Country" table="hibernate">
    <id name="id" column="mykey" unsaved-value="null">
      <generator class="native"></generator>
    </id>
    <version name="version" column="version"
      type="java.lang.Long"/>
    <property name="countryname" column="mytext" />
  </class>
</hibernate-mapping>

Das optionale Untertag <generator bestimmt darüber, wie der Schlüssel generiert wird. Die folgenden Werte für class lassen sich im Zusammenhang mit Oracle-Datenbanken nutzen:

Eine vollständige Liste für das class-Property von generator findet sich auf www.roseindia.net.

Das nächste Beispiel ist eine Tabelle, die einen nichtnumerischen Primary Key aufweist, dessen Spaltenname in der DB ausserdem nicht id heisst. In der Domainklasse kommt das Property id gar nicht vor. Das XML-Mapping sieht folgendermassen aus:

...
  <class name="Country" table="hibernate">
    <id name="id" unsaved-value="null">
      <column name="country_id" 
        sql-type="char(2)"
not-null="true">
      </column>
      <generator class="assigned"></generator>
    </id>
    ...
  </class>
</hibernate-mapping>

Interessant ist auch, dass ein Formularfeld für id mit "generator assigned" nicht automatisch in das Datenbankfeld gespeichert wird, sondern dass es noch eine zusätzliche explizite Zuweisung braucht:

def save = { 
  def country = new Country() 
  flash.message = "Id in Formular: ${params.id}"
  country.properties = params 
  country.id = params.id   
  if(country.save()) {...

Jetzt noch ein Beispiel, wie man Grails zwingt, für den Primärschlüssel eine bestimmte Oracle-Sequence zu verwenden:

  <class name="Author" table="author">
    <id name="id" column="id"
      unsaved-value="null">
      <generator class="native">
        <param name="sequence">AUTHOR_SEQ</param>
      </generator>
    </id>
   ...

Read-Only-Tabelle

Soll eine Klasse nur lesbar, aber nicht änderbar sein, dann lässt sich dies in XML mit dem Attribut mutable der Klasse festlegen:

<class name="Employee" table="emp" mutable="false">

Mapping auf Oracle-BLOB

Auch das Mapping auf einen Oracle-BLOB habe ich bis jetzt nur mit Hibernate hingekriegt.

Felddefinition in Domain-Klasse:

byte[] imgflag

Constraints sind in der Domain-Klasse nicht notwendig.

XML-Mapping für das BLOB-Property

...
  <class name="Country" table="hibernate">
    ...
    <property name="imgflag">
      <column name="bildfahne" sql-type="blob">
      </column>
    </property>
  </class>
</hibernate-mapping>

Zurück zum Inhaltsverzeichnis

Stored Procedure einbinden

Im folgenden zeige ich ein Beispiel, wie eine Stored Procedure aus einer Oracle-Datenbank eingebunden werden kann. Das Besondere daran ist, dass nicht einfach einzelne Werte zurückgeliefert werden, sondern ein ganzes Dataset. Die beschriebene Lösung ist nicht sehr groovy-like, aber leider die einzige, die ich bis jetzt zum Laufen gebracht habe. Die Stored Procedure wird als Dienst angeboten. Damit setzt sich die Lösung aus folgenden Elementen zusammen:

Stored Procedure

Das einzig Spezielle an der Stored Procedure ist der Out-Parameter vom Typ sys_refcursor:

TEST.findAthletesLike(
  res OUT sys_refcursor, str IN string)
AS
BEGIN
  OPEN res FOR
    SELECT
      ath.athleteid,
      ath.firstname,
      ath.lastname,
      ath.dateofbirth,
      ath.stoppedtime,
      ath.points
  FROM athlete ath
  WHERE ath.firstname LIKE str || '%'
    OR ath.lastname LIKE str || '%';
END findAthletesLike;
/

Methode in einem Service

Der Service verwendet CallableStatement und castet dieses auf OracleCallableStatement, um auf den Cursor zugreifen zu können:

import groovy.sql.Sql
import java.sql.*;

class OrmService {
  ...
  def getFromSqlStoredProc(String str) {
    Sql sql = new Sql(dataSource)
    CallableStatement stmt =     
       dataSource.connection.prepareCall(
       "BEGIN findAthletesLike(?,?); END;")
    stmt.setString(2, str)
    stmt.registerOutParameter(1, OracleTypes.CURSOR)
    stmt.execute()
    ResultSet rs =   
      ((oracle.jdbc.driver.OracleCallableStatement) stmt)
      .getCursor(1)
    return rs
  }
}

Closure in einem Controller

Die Closure im Controller ist verglichen mit dem Service absolut unspektakulär:

class OrmController {
  OrmService ormService
  ...
  def listSqlCreateSQLQueryWithStoredProc = {
    String str = "G"
    if (str) {
      def data = ormService.getFromSqlStoredProc(str)
      return ['data': data]
    }
  }
}

GSP-Seite

Da wir ein ResultSet zurückerhalten, sieht die Ausgabe etwas anders aus, als gewohnt:

<html>
  <body>
    <div class="body">
    <p>Flash ${flash.message}</p>
    <table >
      <%
        while (data.next()) {
          println(data.getString("firstname") +
          " " + data.getString("lastname") + "<br />" )
        }
      %>
    </table>
    </div>
  </body>
</html>

Zurück zum Inhaltsverzeichnis

Standardmappings von Grails- auf Oracle-Datentypen

Die folgende Tabelle enthält das Mapping der Datentypen zwischen Grails und Oracle XE (bezieht sich auf Grails 0.5.6 und Oracle XE):

Grails-Datentyp Oracle-XE-Datentyp
Boolean Number(1)
byte/Byte Number(3)
int/Integer Number(10)
long/Long Number(19)
float/Float Float
double/Double Float
String Varchar2(255)
byte[] mit maxSize:10000 Long Raw
Date Timestamp(6)

Hier ein paar Tricks, wie man Grails in den Domainklassen ohne Hibernate dazu bringt, in einer Oracle-Datenbank andere Datentypen zu mappen. Die Auswahl richtet sich vor allem an Datentypen aus, die bei uns in der Firma konkret im Einsatz sind. Mappings, die sich nur mit Hibernate realisieren lassen wie BLOBs oder Primary Key umbenennen, finden sich weiter oben.

Varchar2 > 255

String langtxt
..
static def constraints = {
  langtxt(maxSize:1000)
}

Varchar2(1) als Pseudo-Boolean mit J/N

String mybool
static constraints = {
  mybool(size:1..1, inList:["J", "N"])
}

Selfjoin (Autojoin)

In Oracle arbeitet man oft mit rekursiven Beziehungen, weil der SQL-Dialekt dies gut unterstützt. Damit lassen sich Baumstrukturen beliebiger Tiefe in einer einzigen Tabelle abbilden. Ein solcher Selfjoin lässt sich in Grails auch ohne Hibernate erzeugen oder abbilden.

class Keyword {
  static hasMany = [keywords:Keyword]
  Keyword parent
  ...
  static constraints = {
    parent(nullable:true) // nötig für den Root
  }
}

Zurück zum Inhaltsverzeichnis

DB-Views in Grails

Bei der Arbeit mit existierenden Datenbanken gibt es Aufgabenstellungen, für die Grails auch in der Version 1.0.3 nur schlecht gerüstet ist. Eine davon ist die Arbeit mit Views. Domains sind defaultmässig mit einzelnen Tabellen verbunden und gehen davon aus, dass auf diesen Tabellen auch Update-, Insert- und Delete-Statements abgesetzt werden können. Aus den folgenden Gründen arbeitet man bei bestehenden Datenbanken oft mit Views:

In Grails habe ich bis anhin auf folgende Workarounds zurückgegriffen, um mit komplexen Queries zu arbeiten:

Zurück zum Inhaltsverzeichnis

Allgemeine Tipps und Tricks mit Datenbanken

Die folgenden Informationen gelten für Datenbanken generell.

Mindestanforderungen für bestehende Datenbanken (version)

Mit den verschiedenen Bordmitteln von Grails kann man inzwischen auch für bereits existierende Datenbanken recht gut Grails-Applikationen erstellen. Allerdings gibt es eine Mindestanforderung:

Für Tabellen, die nur lesbar sein müssen, gilt diese Einschränkung nicht. Hier ist es möglich, in ORM DSL version false zu setzen, also z.B.

class Athleteview {
  static mapping = {
     table 'ATHLETEVIEW'
     version false
     ...
  }
  ...
}

Einfüge- und Aktualisierungsdatum

Werden in einer Domain die folgenden zwei Felder erstellt, dann verwaltet Grails völlig selbständig Einfüge- und Aktualisierungsdatum eines Datensatzes:

Mit folgendem Code lassen sich diese zwei Felder auf bestehende Datenfelder mappen:

class MyTable {
  ...
  Date dateCreated
  Date lastUpdated

  static mapping = {
    table 'DBTABLE'
    columns {
      ...
    dateCreated column:'INSERTDAT'
    lastUpdated column:'MUTDAT'
  }
}

Achtung:

Mit dieser Technik verwaltet Grails die Felder, es sollte deshalb in der Datenbank für diese Felder keine Insert- oder Update-Trigger geben.

Transient-Felder

Felder, die nicht auf ein Feld der Datenbanktabelle gemapped werden, lassen sich mit folgendem Code kennzeichnen:

class Basket {
  ...
  BigDecimal price
  Integer amount
  BigDecimal getTotal() {
    if (price && amount) {
      return price * amount
    }
  }
  static transients = ['total']

}

Zurück zum Inhaltsverzeichnis


IDE für Grails einrichten

Die ideale Entwicklungsumgebung für Grails suche ich noch, im Moment probiere ich sowohl mit dem JDeveloper wie mit Eclipse und IntelliJ herum, aber so richtig überzeugt mich noch nichts. Vergessliche Leute wie ich haben sich an die Annehmlichkeiten der Code Completion gewöhnt und rümpfen über das Programmieren im Text-Editor die Nase. Groovy lässt sich problemlos in den JDeveloper integrieren, aber bei den GSP-Seiten hapert es noch. 

IntelliJ

Ich habe mir sogar IntelliJ zugelegt, da es im Moment bezüglich Grails-Unterstützung, z.B. Code completion, einhellig als die beste IDE bezeichnet wird. In dieser Hinsicht habe ich sehr gute Erfahrungen gemacht. Allerdings lässt sich mit einem Notebook mit 1GB RAM und einer ziemlich vollen Festplatte kaum mit IntelliJ arbeiten: Der Start meines Grails Workspace benötigt ca. 10 Minuten und es kommt immer wieder zu Hängern, v.a. wenn ich noch andere Java Applikationen starte. Gelegentlich schnappt sich IntelliJ auch mehr als 90% des CPUs und dann legt es den ganzen Rechner lahm!

Achtung: Falls IntelliJ zum Debuggen von Grails-Applikationen verwendet werden soll, muss man beim Erstellen der Run- und Debug-Konfiguration unbedingt das Häkchen bei Make entfernen!

Für mehrsprachige Applikationen erweist sich die folgende Einstellung als äusserst nützlich: "File - Settings - IDE Settings - General - Properties Files: Transparent native-to-ascii conversion". Damit kann man auch Texte mit Umlauten in die Property-Files schreiben. Diese werden im Hintergrund automatisch umgewandelt. Damit entfällt die Notwendigkeit, die Texte mit der Methode "jä".encodeAsHTML() umzuwandeln.

Eclipse

Eine ausführliche englische Beschreibung für Eclipse findet man auf dem Grails-Website. Die vorliegende Beschreibung habe ich im November 2008 für die Grails-Version 1.0.3 und Eclipse 3.4.1 aktualisiert.

Vorgehen, um Eclipse für Grails einzurichten:

  1. Grails-Plugin in Exclipse installieren: Menu Help - Software Updates - Register "Available Software" - Add Site - Location: http://dist.codehaus.org/groovy/distributions/update/ - im Repository Grails Eclipse Feature und Groovy Feature markieren - Schaltfläche Install anklicken
    Achtung: Falls Sie auch Groovy TestNG Feature installieren möchten, benötigen Sie auch das TestNG Plugin von http://beust.com/eclipse.
  2. Workspace für Grails eröffnen: Am besten sammelt man alle Grails-Projekte in einem eigenem Workspace, also File - Switch Workspace - Browse - an geeigneter Stelle ein neues Verzeichnis erstellen - OK - OK
  3. Classpath GRAILS_HOME einrichten: Window - Preferences - Java - Build Path - Classpath - New - Name: GRAILS_HOME - Verzeichnis eingeben, z.B. C:/Programme/programming/grails-1.0.3/grails
  4. Projekte importieren: Grails erzeugt automatisch Eclipse Projekt- und Classpath-Dateien, so dass sich bestehende Grails-Projekte ganz einfach importieren lassen: File - Import - General - Existing Projects into Workspace - Next - Verzeichnis des Projektes suchen - Finish
    Einstellungen wie Add Groovy Nature oder Project - Properties - Groovy Project Properties - Disable Groovy Compiler Generating Class Files scheinen damit automatisch vorhanden zu sein. 
    Achtung
    1. Wenn in älteren Versionen die letzterwähnte Einstellung vergessen wurde (merkt man daran, dass generate-all nicht mehr geht), dann kann man die Applikation retten, indem man alle Class-Files im Stammverzeichnis der Applikation löscht.
    2. Ein bestehendes Projekt kann nur importiert werden, wenn es im Stammverzeichnis die Datei .project aufweist. Ist diese z.B. bei einem heruntergeladenen Projekt (wie AuthorBook) nicht vorhanden, dann lässt sie sich aus einem anderen Projekt kopieren. Einzig dass Tag <name> muss man mit dem aktuellen Projektnamen anpassen.
  5. GSP-Seiten eintragen: GSP-Seiten sind nichts anderes als JSP-Seiten, die auf eine spezielle Taglib zurückgreifen, deshalb kontrolliert man, ob die folgenden zwei Einträge unter Window - Preferences vorhanden sind
    1. General - Editors - File Associations: *.gsp hinzufügen und JSP-Editor auswählen
    2. General - Content Types - JSP in Rubrik Text aufklappen - *.gsp hinzufügen

    Auf einer anderen Webseite wird stattdessen vorgeschlagen, *.gsp für XML einzutragen. Habe ich nicht ausprobiert, aber macht evtl. auch Sinn.

  6. Beim Update von Projekten aus älteren Grails-Versionen den den Pfad für das Testverzeichnis in der Datei .classpath im Stammverzeichnis der Applikation kontrollieren und evtl. ändern. Vorgehen: Rechtsklick auf das Projekt - Properties - Java Build Path - Register Source - ..grails-tests anklicken - Remove - Add Folder - test\integration und test\unit anklicken - OK - OK

Achtung: Wenn Sonderzeichen wie ä, ö, ü auf den gsp-Seiten nicht richtig angezeigt werden, dann liegt das möglicherweise am Encoding der Seiten. Am besten fährt man, wenn man alle Seiten im Defacto-Standard UTF-8 speichert. 

Achtung: Disable Groovy Compiler Generating Class Files soll nur angekreuzt sein, solange kein Debugging in Eclipse erfolgt. Will man dagegen Debuggen, dann muss man das Häkchen entfernen und einen spezielles Output-Verzeichnis einrichten wie auf dem Grails-Website beschrieben.

Grails-Applikation in Eclipse starten

Grails-Applikationen lassen sich direkt aus Eclipse starten. Falls man (z.B. wegen Oracle XE) die Grails-Applikationen unter einer anderen Port-Nummer als 8080 laufen lässt, muss man dies vor dem ersten Start noch anpassen: Run - Run Configurations - Register Arguments - VM arguments und dort den Port ändern, z.B. Dserver.port=9090. Anschliessend lässt sich die Applikation mit Run direkt starten.

Zurück zum Inhaltsverzeichnis


Architektur einer Grails-Anwendung

Grails-Applikationen setzen das MVC-Konzept um 
(Model - View - Controller)

Eine Grails-Applikation besteht u.a. aus folgenden wichtigen Teilen

Daneben können weitere Elemente vorkommen:

Dies spiegelt sich auch in der Verzeichnisstruktur, die mit "grails create-app" angelegt wurde.

Achtung: Bei der Bootstrap-Klasse besteht eine Namensverwirrung: Sie hiess früher ApplicationBootStrap.groovy und wurde dann in BootStrap.groovy umgetauft. Aufgrund dieser Namensunsicherheit sollten globale Konstanten für die Applikation nicht mehr in BootStrap.groovy definiert werden! In Version 1.0.4 wird zwar beim Scaffolding die Datei BootStrap.groovy erzeugt, aber Konstanten lassen sich nur aus ApplicationBootStrap.groovy auslesen! Fazit: Besser Config.groovy verwenden!

Eine weitere Möglichkeit für die generelle Konfiguration einer Grails-Applikation ist die Datei Config.groovy im Verzeichnis grails-app\conf\. Hier lassen sich Einstellungen und Konstanten mit derselben Syntax wie in Java-Property-Dateien festlegen:

vorspann.meinevar = "Mein Text"

Mit folgendem Code greift man anschliessend darauf zu:

x = grailsApplication.config.vorspann.meinevar

Zurück zum Inhaltsverzeichnis


Verzeichnisstruktur einer Grails-Applikation

mybookmarks: Verzeichnis der Applikation

ab Version 0.6

Zurück zum Inhaltsverzeichnis


Grails-Konventionen

Allgemeine Konventionen

Model/Datenbank (Persistenzschicht)

Business-Logik (Domain-Klassen und Services)

Webschicht (Controller und Views)

Zurück zum Inhaltsverzeichnis


Grails-Befehle

grails create-app

Erzeugt das Gerüst für eine Applikation mit allen Verzeichnissen und Unterverzeichnissen

Eingaben:

grails create-domain-class

Erzeugt das Gerüst einer Domain-Klasse.

Eingaben:

grails generate-all

Erzeugt für eine bereits existierende Domain-Klasse mit vorhandenen Feldern, z.B. \domain\User.groovy, das folgende:

grails create-controller

Erzeugt einen einzelnen Controller

Eingaben:

grails generate-views

Erzeugt Views zu einer Domain-Klasse. Views ohne Domain-Klasse muss man manuell erzeugen.

Eingaben:

grails run-app

Startet die Applikation in einer Testumgebung mit dem Jetty-Applikationsserver. Wenn der normale Port 80 bereits belegt ist, kann mit folgendem Befehl ein neuer Port zugewiesen werden:

grails -Dserver.port=9090 run-app

grails test-app

Startet die Applikation im Testmodus.

grails create-integration-test

Erzeugt eine Testklasse für Integrationstests. Hiess in früheren Versionen vermutlich create-test-suite.

Eingaben:

grails create-tag-lib

Erzeugt eine neue Tag-Library.

Eingaben

grails console

Startet eine Grails-Konsole zum Testen einer kompletten Web Applikation (aber ohne den Jetty-Server).

grails bug-report

Erzeugt ein ZIP-File auf dem Root-Verzeichnis der Applikation mit allen GSP-, groovy- und Java-Dateien sowie gewissen XML-Dateien. Allerdings scheinen die Hibernate-Dateien nicht mitzugehen, die Datasources dagegen schon (Sicherheitsloch!). Zweck ist es, alle Dateien für einen Bug Report zusammenzuhaben.

Eingaben

grails install-plugin yui

Installiert das Ajax-Plugin für Yahoo UI.

Zurück zum Inhaltsverzeichnis


GSP-Syntax

Grails Server Pages (GSP) funktionieren ähnlich wie ASP oder JSP, d.h. normale HTML-Seiten werden via GSP-Tags mit dynamischen Elementen ergänzt, die in der Sprache Groovy geschrieben sind.

Die dynamischen Elemente können auf drei Arten in die Seite eingefügt werden

  1. ${..}: in ${} stehen Ausdrücke, welche einen Wert ausgeben, d.h. Variablen, Ausdrücke oder Methoden. Entspricht <%=...%> in ASP. Da Groovy Zugriff auf das Java-API hat und gewisse Pakete automatisch importiert, sind solche Ausdrücke möglich:
    ${new java.text.SimpleDateFormat("HH:mm").format(new Date())}
  2. <% %>: Längere Codeabschnitte stehen wie bei ASP oder JSP in <%..%>. Allerdings ist es wenig sinnvoll, längeren Code tatsächlich in die GSP-Seiten zu legen. In der Architektur von Grails gehört Code je nach Zweck in Domain- und Service-Klassen oder Controller.
  3. Groovy-Tags: Groovy-Tags beginnen immer mit <g:... Für die wichtigsten Aufgaben wie Selektionen, Schleifen, Verarbeitung von Kollektionen existieren bereits Tags. Das Spezielle an Grails ist aber, dass sich eigene Tag-Libraries erstellen lassen, und zwar sehr viel einfacher als mit JSP. 

Zurück zum Inhaltsverzeichnis


Testen von Grails-Applikationen

Das Testen ist in das Grails-Framework bereits eingebaut. Jedesmal, wenn Sie mit einem Grails-Befehl ein Artefakt generieren, dann wird im Verzeichnis \test\integration\ auch eine Testklasse dazu erstellt. Diese Testklasse erhält den Namen des zugehörigen Artefakts, ergänzt mit Tests, also z.B. DesignTagLibTests.groovy.

Für die Unit-Tests greift Grails auf das bewährte JUnit zurück. Groovy bietet aber zusätzlich eine Klasse groovy.util.GroovyTestCase an, welche mit zusätzlichen praktischen assert-Methoden ergänzt.

Vorgehen:

Zurück zum Inhaltsverzeichnis


 

Grails Snippets

In diesem Kapitel geht es nicht um Code-Snippets im Sinne von Grails (siehe unten), sondern um eine Zusammenstellung von gängigen Aufgaben und ihrer Umsetzung in GSP/Groovy.

Groovy allgemein

Weil es keine primitiven Typen gibt, sind Ausdrücke wie der folgende möglich:

def produkt = 2
1.upto(7) { produkt *= it }

Regex in Groovy

Regex in Groovy bietet die gleichen Möglichkeiten wie in Java, ist aber um ein paar zusätzliche Goodies erweitert. Setzt man Regex allerdings direkt in GSP-Seiten ein, dass muss man beachten, dass bei gewisse Zeichen, z.B. beim $-Zeichen und bei Klammern, GSP und Regex sich gegenseitig in die Quere kommen können, so dass die Markierung gelegentlich zu akribischem Erbsenzählen ausartet.

Unterschied zwischen Find- und Match-Operator

Groovy führt für den Vergleich mit Regex zwei neue Operatoren ein:

assert "text" =~ /ex/

Der pattern-Operator ~String

Groovy führt für Regex zur Verbesserung einen weiteren Operator ~String ein. Mit diesem Operator lässt sich ein String in ein Objekt vom Typ java.util.regex.Pattern umwandeln. Patterns werden vorkompiliert, was bei wiederholter Anwendung zu einer besseren Performance führt. Man beachte, dass zwischen = und ~ zwingend ein Leerschlag stehen muss, um den Operator vom find-Operator =~ zu unterscheiden!

Das folgende Code-Beispiel zeigt die Performance-Vorteile des Pattern-Operators

cannonFilename = "IMG_1516.JPG"
cannonRegex = /(?i)img_\d\d\d\d\.jpg/

startZeit = System.currentTimeMillis()
25000.times{
  cannonFilename =~ cannonRegex 
}
println "Variante ohne Pattern: " +
  (System.currentTimeMillis() - startZeit)

//**********************************************
//Diese Variante ist mehr als doppelt so schnell
startZeit = System.currentTimeMillis()
cannonPattern = ~cannonRegex
25000.times {
  cannonPattern.matcher(cannonFilename)
}
println "Variante mit Pattern: " +
  (System.currentTimeMillis() - startZeit)

Regex-Operatoren

OperatorBedeutungBeispiel
==~ Pattern matching"groovy.gsp" ==~ /g.*/
/begrenzt das Pattern"groovy.gsp" ==~ /groovy\.gsp/
\maskiert das nächste Zeichen"groovy.gsp" ==~ /groovy\.gsp/
agenau 1 Vorkommen"mymail@xy.ch" ==~ /.+@.+\...?.?/
a?0 bis 1 VorkommenLänderendung hat 2 bis 4 Buchstaben
"mymail@xy.ch" ==~ /.+@.+\....?.?/
a*0 bis n VorkommenString fängt mit _ an und ist beliebig lang
"_inc" ==~ /\_[a-zA-Z]*/
a+1 bis n VorkommenString besteht nur aus Kleinbuchstaben und enthält mindestens ein Zeichen
"gugus" ==~ /[a-z]+/
.beliebiges Zeichen ausser Zeilenumbruch \n oder \r\nString besteht aus 3 beliebigen Zeichen
"x y" ==~ /.../
.*beliebiges Zeichen
kommt 0 bis n mal vor
String beginnt mit a und enthält 0 bis n weitere Zeichen 
Achtung: greedy, siehe unten!
"a25x" ==~ /a.*/
[abc]irgendeines dieser ZeichenString ist "0", "5" oder "9"
"0" ==~ /[059]/
a|bdas eine oder das andere ZeichenString endet mit ".li" oder ".ch"
"mymail@xy.li" ==~ /.+(\.li|\.ch)/
(aa)Gruppe von ZeichenFindet Haar und Heer
"Haar" ==~ /H((aa)|(ee))r/
[oO]{2}Anzahl Vorkommen (muss nicht hintereinander sein)Wahr für alle Strings, in denen mindestens einmal zwei Selbstlaute hintereinander vorkommen
"Eier" ==~ /.*[aeiou]{2}.*/
[a-z]irgendein Zeichen aus der FolgeString mit genau 2 Buchstaben
"ch" ==~ /[a-zA-Z][a-zA-Z]/
[^01]irgenein Zeichen ausser den aufgezähltenString der keine geraden Ziffern enthält
"a 1553" ==~ /[^02468]+/
^aZeile beginnt mit diesem ZeichenZeile beginnt mit einer Ziffer
"1. Punkt" ==~ /^[1-9].*/
a$Zeile endet mit diesem ZeichenZeile endet mit ">"
"<br />" ==~ /.*>$/

Spezielle Zeichen

wie in Java, die vollständige Liste ist hier
\nZeilenumbruch unter Unix (Linefeed)
\r\nZeilenumbruch unter Windows (Carriage Return und Linefeed)
\sWhitespace-Zeichen, d.h. [\t\n\x0B\f\r]
\Skein Whitespace-Zeichen
\tTabulator
\deine Zahlenziffer, d.h. [0-9]
\Dkeine Zahlenziffer, d.h. [^0-9]
\wein Wortzeichen, d.h. [a-zA-Z_0-9]
\Wkein Wortzeichen
\bein Wortende
\Bkein Wortende

Flags

Flags gelten von ihrem Auftreten bis ans Ende oder bis sie aufgehoben werde.
(?s). schliesst auch spezielle Zeichen wie den Zeilenumbruch ein
(?-s)hebt (?s) wieder auf
(?i)Gross- und Kleinschreibung spielt keine Rolle
(?-i)hebt (?i) wieder auf
(?x)ignoriert unmaskierten Whitspace und Kommentare
/aa(?si).*(?-si)\n/Mehrere Flags gleichzeitig einstellen und wieder aufheben
/ab(?i:cde)fg/Flag nur für einen Teil des Textes einstellen

Zu maskierende Zeichen

Beispiele zum Parsen von GSP/Groovy

Die Beispiele gehen davon aus, dass der Suchstring htmlencoded ist. Der gefundene String wird in der nicht encodeten Schreibweise angezeigt, d.h. so, wie er auf einer Webseite angezeigt wird.
Patterngefundene Strings 
/(?s)&lt;%.*?%&gt;/<%intcounter++%>
/(?s)&lt;g:.*?&gt;/<g:def var="name" value="${'Silvia'}" />
/(?i)img_\d\d\d\d\.jpg/IMG_1516.JPG 
/(?i)p(\d){7}\.jpg/p1000314.jpg

Beispiele von Befehlen:

Greedy und reluctant Qualifiers

Um GSP-Seiten mit verschiedenen Arten von Tags und Ausdrücken zu parsen, muss man den Unterschied zwischen "greedy quantifiers" wie .* und "reluctant qualifiers" wie .*? kennen. Am folgenden Befehl lässt sich dieser Unterschied gut erläutern.

Richtige Variante, d.h. reluctant:
mytext = mytext.replaceAll(/(?s)&lt;%.*?%&gt;/,
  {"<span class=\"groovyexpr2\">\$0</span>"})
Findet erstes Anfangs- und Schluss-Tag und umhüllt sie mit <span>-Tags, findet zweites Anfangs- und Schlusstag etc.

Falsche Variante, d.h. greedy:
mytext = mytext.replaceAll(/(?s)&lt;%.*%&gt;/,
  {"<span class=\"groovyexpr2\">\$0</span>"})
Findet erstes Anfangs- und letztes Schluss-Tag und umhüllt sie mit <span>-Tags. Alle Anfangs- und Schluss-Tags dazwischen werden ignoriert.

Metainformationen zur Applikation

Ausgangslage: Der URL für die aktuelle Seite ist
http://localhost:9090/beispiele/versuche/renderpage/snippets
Das heisst:
Mit folgendem Code kann man nun solche Metainformationen aus der Applikation holen.

Applikationsname

metadata.get('app.name')
Resultat: beispiele

Controllername

grails.util.GrailsWebUtil.getControllerFromRequest(request).controllerName
Resultat: versuche

Actionname

request.getAttribute('org.codehaus.groovy.grails.ACTION_NAME_ATTRIBUTE')
Resultat: renderpage

Domainklassen

Snippets zum Mapping von bestimmten Datentypen findet man im Abschnitt Mapping.

Feld ist nicht persistent, d.h. wird nicht in DB gespeichert

static transients = ["feldname1", "feldname2"]

Feld kann leergelassen werden

static constraints = {
  birthday(nullable: true)
}

Feld kann nur Klein- oder Grossbuchstaben oder Leerschläge enthalten

static constraints = {
  name(matches:"[A-Za-z ]+")
}

Validierung einer eindeutigen Emailadresse (kann sogar gültige wie .info von nichtgültigen wie .x Endungen entscheiden)

String email
static constraints = {
  email(email:true, unique:true)
}

Controllers

Redirect auf Root-Verzeichnis:

redirect(uri:"/")

Views

Seite mit Links auf alle Domain-Klassen

<g:each var="c" in="${grailsApplication.domainClasses}">
  <li class="controller">
    <a href="${c.logicalPropertyName}">${c.fullName}</a>
  </li>
</g:each>

Flash-Message anzeigen 

<g:if test="${flash.message}">
  <div class="message">${flash.message}</div>
</g:if>

Umwandeln eines beliebigen Strings in HTML

encodeAsHTML()

Services

Services haben einen Scope, der als static-Eigenschaft festgelegt wird, z.B.
static scope = "singleton"
Es gibt die folgenden Werte für scope
Wenn es sich um einfache Hilfsroutinen ohne Zustand handelt, dann bewährt es sich am besten, diese static zu deklarieren. Damit umgeht man Probleme mit dem Erzeugen eines Service-Objektes. Hier ein Beispiel eines Services für die Stringverarbeitung:

static class StringService {
  static def leftToFirst = {
    mytext, mychar ->
    if (mytext != null && mytext.indexOf(mychar) > -1) {
      return mytext.substring(0, mytext.indexOf(mychar))
    }
    return mytext
  }
}

Services scheinen nicht dynamisch geladen zu werden. Es braucht normalerweise einen Neustart der Applikation, um Änderungen einzulesen.

Zurück zum Inhaltsverzeichnis


Grails Tipps und Tricks

Konfiguration

Dauer der Http-Session einstellen

Die Dauer einer http-Session stellt man normalerweise in web.xml ein. Leider findet man diese Datei in einer Grails-Applikation nicht wie gewohnt im Verzeichnis WEB-INF. Mit folgendem Trick kann man trotzdem Einfluss auf die Dauer nehmen:

<session-config>
  <session-timeout>15</session-timeout>
</session-config>

Die Zahl steht natürlich für die Session-Dauer in Minuten.

Domains

Nachträgliche Änderungen an Domainklassen

Achtung: Wenn Sie nachträglich ein Feld aus einer Klasse entfernen, so wird das Feld zumindest bei Oracle XE in der Datenbank nicht automatisch ebenfalls entfernt. Da defaultmässig die Felder nicht null sein dürfen, scheitert der nächste Versuch, einen Datensatz einzufügen, weil das Feld nicht mehr im Create-Formular enthalten ist.

Lösung: das Feld manuell in der Datenbank löschen

Views

Premature end of file error umgehen

Die folgende Fehlermeldung erscheint nur auf der Konsole und anscheinend nur mit gewissen Browsern, z.B. Firefox ab Version 3. 

[Fatal Error] :-1:-1: Premature end of file.

Der Bug sollte in Grails 1.4 behoben werden. Um den Fehler zu umgehen, kann man in der Zwischenzeit in config.groovy folgende Zeile auskommentieren:

//xml: ['text/xml', 'application/xml'],

Quelle: http://jira.codehaus.org/browse/GRAILS-3088

Templates

Mit Sitemesh verfügt Grails über einen praktischen Template-Mechanismus. Um z.B. mehrere GSP-Seiten mit derselben Struktur (Seitenlayout, Navigation, Logo etc.) zu versehen, benötigt man folgende Elemente (werden beim Scaffolding automatisch erzeugt):

Codebeispiel für die Template-Datei

<html>
  <head>
    <title>
      <g:layoutTitle default="Grails" />
    </title>
    <link rel="stylesheet"
      href="${createLinkTo(dir:'css',file:'main.css')}"></link>
     <g:layoutHead />
     <g:javascript library="application" />
  </head>
  
  <body>
    <div id="spinner" class="spinner" style="display:none;">
      <img src="${createLinkTo(dir:'images',file:'spinner.gif')}" 
        alt="Spinner" />
    </div>
    <div class="logo">
      <img src="${createLinkTo(dir:'images',file:'grails_logo.jpg')}"
        alt="Grails" />
    </div>
    <g:layoutBody />
</body>
</html>

Fallstricke bei der Benennung von Templates und Controllern

Durch eigene schmerzlicher Erfahrung bin ich zu der folgenden Einsicht gelangt: Im Zusammenhang mit AJAX sollte man ein Template nicht gleich benennen wie einen Kontroller, also z.B. BeispielController.groovy und layouts\beispiel.gsp. Sonst wird das Template nämlich auch bei einem AJAX-Request geladen, der nur einen Teil der Seite aktualisiert, so dass die Template-Elemente zweimal auf der Seite vorkommen (siehe Screenshot).

Wenn man trotzdem wissen möchte, zu welchem Controller ein Layout gehört, kann man es ja stattdessen \layouts\beispiellayout.gsp nennen.

Wiederverwendung von Code Snippets

Kleine wiederverwendbare statische Code-Snippets erstellt man folgendermassen

  1. Erzeugen Sie eine Datei _meinsnippet.gsp im Verzeichnis views für ein Snippet, das allen Views zur Verfügung stehen soll, z.B.
    \grails-app\views\_nachspann.gsp
  2. Geben Sie in der Datei den statischen oder dynamischen HTML-Code ein, der wiederverwendet werden soll, z.B.
    Zurück zu 
    <a href="http://www.ecotronics.ch/">Ecotronics</a>
  3. Fügen Sie auf jeder Seite, welche dieses Snippet verwendet, folgendes Tag ein:
    <g:render template="/nachspann" />

Der Schrägstrich in .."/nachspann steht übrigens dafür, dass die Datei im Verzeichnis /views gespeichert ist. Lässt man / weg, dann wird immer in jenem Unterverzeichnis von \views\ gesucht, dessen Name dem zugehörigen Controller entspricht.

Lösch-Links in Liste einfügen

Wenn Controller und die zugehörigen Views mit Scaffolding erzeugt wurden, dann werden im Controller normalerweise Aktionen, die Daten verändern, auf mit allowedMethods auf POST beschränkt.

Damit ist es nicht möglich, auf einer Liste einen Hyperlink zum Löschen einzufügen. Abhilfe schafft man, indem man das delete:'POST' entfernt, d.h.

def allowedMethods = [save:'POST', update:'POST'] 

Fremdschlüssel in Kombinationsfeldern

In den Create- oder Edit-Seiten werden Fremdschlüssel normalerweise als Kombinationsfelder angezeigt. Allerdings wird die defaultmässig das Attribut id angezeigt, was wenig sinnvoll ist. Mit dem Attribut optionValue lässt sich ein anderes Attribut verwenden, z.B.:

optionValue="name"

Quelle: 

http://www.nejug.org/2007/include/GetGroovierWithGrails.pdf Folie 24

Als Alternative zu diesem Vorgehen versieht man den Controller mit einer toString-Methode, welche die Informationen für die Anzeige liefert.

In Grails einen Session-Counter programmieren

Es gab ein Zeit lang ein Plugin, das diverse statistische Auswertungen macht, unter anderem auch Sessions zählt. Der zuständige Programmierer musste es leider vom Netz nehmen. Mit der im Folgenden beschriebenen Vorgehensweise lässt sich aber ein einfacher Session- oder Hit-Counter ab der Grails-Version 1.0.2 selbst erstellen. Man kann sowohl die Gesamtzahl der Sessions wie die Zahl der im Moment aktiven Sessions zählen. Dazu habe ich folgende Grails-Elemente verwendet:
Achtung:

ApplicationService.groovy

Diese Datei muss im Verzeichnis \grails-app\services\ unterhalb des Applikationsverzeichnisses liegen.

class ApplicationService {
  static scope = "singleton"
  static transactional = false

  static activeSessions = 0L
  static allSessions = 0L
}

SessionService.groovy

Diese Datei muss im Verzeichnis \grails-app\services\ unterhalb des Applikationsverzeichnisses liegen.

class SessionService {
  static scope = "session";
  static transactional = false

  //Constructor
  public SessionService() {

    ApplicationService.allSessions
      = ApplicationService.allSessions + 1;

    ApplicationService.activeSessions
      = ApplicationService.activeSessions + 1;

  }

  protected void finalize() {
    if (ApplicationService.activeSessions > 0) {
      ApplicationService.activeSessions
        = ApplicationService.activeSessions - 1;

    }
  }
}

Die GSP-Seite sessioncounter.gsp

Diese Datei liegt in einem zum Controller gehörigen Unterverzeichnis von \grails-app\views\.

<%@ taglib prefix="g"  
  uri="http://grails.codehaus.org/tags" %>

<html>
  <head>
    <title>Session-Counter in Grails</title>
  </head>
  <body>
    <div class="body">
      <h1>Session-Counter in Grails</h1>
      <p>Anzahl Sessions insgesamt:
        ${ApplicationService.allSessions}</p>
      <p>Anzahl aktive Sessions:
        ${ApplicationService.activeSessions}</p>
    </div>
  </body>
</html>

AJAX nach Projekt-Updates zum Laufen bringen

In einem alten Projekt, das ich auf Version 1.0.3 aktualisiert habe, gelang es mir nicht, die allereinfachsten AJAX-Beispiele zum Laufen zu bringen. Mit Trial und Error fand ich heraus, dass das Problem verschwindet, wenn ich die Tag-Libraries UITagLib.groovy und JavascriptTagLib.groovy aus dem Verzeichnis \taglib\ entferne. Diese werden beim Erzeugen einer neuen Applikation in Version 1.0.3 nicht mehr erzeugt. Bis jetzt ist mir nichts aufgefallen, was nicht mehr laufen würde. 

Weder mit einer Suche auf grails.org noch via Google fand ich heraus, wozu diese Tag-Library genau verwendet wird und ob sie in neueren Versionen noch nötig ist.

Zurück zum Inhaltsverzeichnis


Passwortgeschützter Website mit Acegi-Plugin

Im folgenden finden Sie das Vorgehen, um das Acegi-Security-Plugin zu installieren (im Oktober 2008 ist die aktuelle Version 0.3) und einen Website in einen passwortgeschützten Website umzuwandeln. Die Anleitung folgt dem ausführlichen Tutorial auf http://www.infoq.com/articles/grails-acegi-integration.

Acegi heisst übrigens neu "Spring security" und unter diesem Titel findet man auch die notwendige Dokumentation, z.B. unter http://static.springframework.org/spring-security/site/reference/pdf/springsecurity.pdf. Das Plugin heisst aber (zumindest in der Grails-Version 1.0.3) nach wie vor acegi. 

Meine Anleitung geht davon aus, dass bereits eine funktionierende Grails-Applikation besteht, nämlich eine kleine Buchverwaltung mit den 2 Domainklassen Author und Book. Zwischen Author und Book besteht eine 1:n Beziehung, wobei Author die Mastertabelle ist. Die zweite wichtige Voraussetzung ist, dass der User, mit dem in der Datasource die Verbindung zur Datenbank erfolgt, genügend Rechte hat, damit neue Tabellen erstellt werden können, denn für die Absicherung des Sites werden neue Tabellen für Benutzer und Rollen erstellt. In der Datasource-Datei muss ausserdem dbCreate = "create" stehen. Aber Achtung, dies darf keinesfalls bei bereits existierenden Datenbanken mit vorhandenen Daten angewandt werden, weil dieser Befehl alle Daten löscht! Sollen bestehende Websites mit vorhandenen Daten gesichert werden, dann müssen die für Acegi notwendigen Tabellen von Hand erstellt werden!

  1. Kommandozeile öffnen und ins Stammverzeichnis der Applikation wechseln
  2. In der Kommandozeile das Acegi-Plugin installieren mit dem Befehl
    grails install-plugin acegi
  3. Als nächstes erzeugen wir die Benutzer und Rollen. Die Domains (und Tabellen dazu) nenne ich "AuthUser" und "Role", man kann sie aber auch anders nennen.
    grails create-auth-domains AuthUser Role
    Dieser Befehl erzeugt drei Domainklassen, nämlich die zwei genannten sowie "Requestmap.groovy". Ausserdem wird eine neue Konfigurationsdatei namens "SecurityConfig.groovy" ins conf-Verzeichnis eingefügt.
  4. Mit dem nächsten Befehl erzeugen wir die Controller und Views, damit wir Benutzer und Rollen erfassen können. Auch für Login und Logout werden Controller und Views erstellt.
    grails generate-manager
  5. Ein weiterer Befehl erstellt alle notwendigen Dateien für die Registrierung.
    grails generate-registration
  6. Nun starten Sie die Applikation, denn das Erfassen von Benutzern, Rollen und Zugriffsregeln erfolgt dort. Erscheint statt der Startseite nur eine leere Seite ohne irgendwelche Fehlermeldungen, dann heisst das wahrscheinlich, dass die notwendigen Klassen nicht erstellt werden können, weil entweder der Benutzer für die Datenbankverbindung nicht genügend Rechte hat oder weil dbCreate nicht auf create gesetzt ist. Versichern Sie sich, dass Sie auf die Controller für User und Role zugreifen können, aber warten Sie noch mit der Dateneingabe. Nach diesem Schritt sollten die notwendigen Tabellen in der Datenbank vorhanden sein.
  7. Wenn Sie nicht bei jedem Start wieder Rollen, Benutzer und andere Daten eingeben wollen, dann empfiehlt es sich, die Applikation zu stoppen, dbCreate = "update" zu setzen oder die Zeile ganz auszukommentieren, damit die Daten erhalten bleiben. Defaultmässig werden Sie gezwungen, alle Felder einer Domainklasse einzugeben. Sichergestellt wird dies nicht nur in den Domainklassen, sondern auch auf der Datenbank mit Check-Constraints. Dies ändern Sie nun, indem Sie in den Domainklassen im Block constraints für die gewünschten Felder nullable: true eingeben, z.B. 
    description(nullable: true)
    Allerdings müssen Sie dann auch auf der Datenbank die Check-Constraints für diese Felder löschen.
  8. Eine andere Variante, den Benutzerkomfort zu erhöhen, ist es, Defaultwerte für nicht ausgefüllte Werte zu setzen. Dazu bearbeitet man die save-Method im Controller. Mit folgendem Code setzt man beispielsweise die Beschreibung gleich dem Wert im Feld authority:
    if (!authority.description) {
      authority.description = params.authority
    }
  9. Sollen neue Benutzer standardmässig aktiviert sein, dann empfiehlt es sich, bereits in der Domainklasse AuthUser.groovy den Defaultwert zu setzen: 
    boolean enabled = true
  10. Starten Sie die Applikation nun wieder. Auf der Einstiegsseite wechseln Sie in den RoleController. Es sollen zwei Rollen erstellt werden, "admin" für Benutzer/innen, die Editieren dürfen und "user" für solche, welche die Daten nur ansehen dürfen. Fügen Sie also diese zwei Rollen ein.
  11. Dann gehen Sie auf den UserController und geben mindestens zwei Benutzer ein, einem mit Rolle admin und user und einen nur mit Rolle user. Wenn sie die Ratschläge aus Schritt 7 und 8 angewendet haben, dann sollten nun das Feld Description leer lassen können, ohne eine hässliche Fehlermeldung zu erhalten.
  12. Um die Regeln für den Zugriff festzulegen, öffnen Sie den RequestmapController. Die Eingaben hängen von ihren Domains und Views ab. Um der admin-Rolle beispielsweise alle Rechte für Author zu geben, machen Sie folgende Eingaben:
    URL: /author/**
    Role: admin
    User sollen dagegen nur auf die Autorenliste und die Detailansicht Zugriff haben, aber weder Datensätze einfügen noch editieren dürfen. Das ergibt:
    URL: /author/list/**
    Role: user, admin
    Auch für die index- und die show-Seite muss eine solche Regel erstellt werden.
    Gleich verfahren Sie mit allen Seiten, bei denen Sie den Zugriff beschränken möchten. 
  13. Klicken Sie nun auf der Startseite aus AuthorController. Sie werden zu einer Login-Seite umgeleitet. Geben Sie die Logindaten für den gewöhnlichen Benutzer ein. Mit korrekten Eingaben werden Sie zur Seite mit der Liste weitergeleitet. Versuchen Sie jedoch, einen Datensatz zu erzeugen oder zu ändern, dann erhalten Sie eine hässliche Fehlerseite. 
  14. Besser wäre es natürlich, die Links dazu je nach Benutzerrechten ein- und auszublenden. Genau für diesen Zweck gibt es nun Tags, die man in der Grails-Acegi-Dokumentation unter dem seltsamen Titel Artifacts findet. Um Beispielsweise den Create-Link auf der Author-Seite nur für Administratoren einzublenden, öffnet man im View-Verzeichnis die entsprechende Seite und umgibt den Link mit dem ifAllGranted-Tag:
    <g:ifAllGranted role="ROLE_ADMIN">
       <g:link class="create" action="create">
         New Author
       </g:link>
    </g:ifAllGranted>
  15. Falls nun jemand trotz allem noch auf "Access denied" landet, sollte die Seite nicht einfach die spröde Fehlerseite des Webservers sein, sondern eine normale, vom Design her an die übrigen Seiten angepasste Seite. Die Sache mit der verschönerten "Access denied"-Seite lässt sich leider zumindest im Oktober 2008 mit der Plugin-Version 0.3 aufgrund eines Bugs nicht realisieren. 

Allgemeine Hinweise:

Das Standardverfahren führt dazu, dass die Zugriffsregeln via Requestmap in der Datenbank gespeichert werden. Das ist sehr flexibel und eignet sich für Websites, wo häufig Änderungen an Benutzern und Rechten vorkommen, z.B. bei Foren, wo sich die Anwender selbst anmelden können. Es führt aber auch dazu, dass für jeden Seitenzugriff ein Datenbankzugriff erfolgt. Wenn der damit einhergehende Performanceverlust nicht akzeptabel ist, kann man stattdessen mit weniger flexiblen statischen Zugriffsregeln in der Datei SecurityConfig.groovy arbeiten. 

Die Checkbox "Remember me" auf der Loginseite setzt ein Cookie. Damit können Sie den Browser schliessen und trotzdem beim nächsten Mal eine Seite ohne Login aufrufen. Wenn Sie ausloggen, wird das Cookie allerdings wieder gelöscht.

Zurück zum Inhaltsverzeichnis


Internationalisierung (i18n)

Übersetzung mit Property Files

Die übliche Art, Websites mit Grails zu übersetzen, besteht aus Property files. Für jede gewünschte Sprache muss im Verzeichnis grails-app\i18n ein Property file mit dem Namen messages_xx.properties liegen, in dem die Sprachstrings als Properties abgelegt sind. Im Dateinamen steht xx für die Sprache und evtl. das Land, also z.B. messages_de.properties oder messages_pt_BR.properties.

Verwendung in GSP-Seiten mit message-Tag:
<g:message code="my.message.code" />

Verwendung in Groovy-Code von Controllern (aber nicht von Services!):
flash['message']='${message(code:'my.message.code')}'

Normalerweise wird die Sprache automatisch an die Einstellungen des Browsers angepasst. Man kann aber auch Links anbieten, mit denen die Benutzerinnen die Sprache wechseln können. Ein Sprachwechsel benötigt folgende Zutaten:

Die letzten zwei Massnahmen sind nötig, um den Sprachwechsel auch mit abgesicherten Websites (z.B. mit Acegi) betreiben zu können.

Die Links sehen z.B. folgendermassen aus:

<g:link controller="${params.controller}"
  action="${params.action}" 
  params="${params + [lang:'de']}">D </g:link>|
<g:link controller="${params.controller}"
  action="${params.action}" 
  params="${params + [lang:'en']}">E </g:link>|

Das ${params + [lang:'en']} ist nötig, damit die bestehenden Parameter, z.B. für die Seite show nicht einfach ersetzt, sondern mit dem lang-Parameter ergänzt werden.

Ist der HomeController mit grails create-controller Home erzeugt und index.gsp vom Verzeichnis web-app in das View-Verzeichnis dieses Controllers verschoben, dann braucht es für die Startseite noch folgendes Mapping in grails-app/conf/urlMappings.groovy (zumindest bei passwortgeschützten Applikationen im Zusammenhang mit dem Acegi-Plugin):

static_mappings = {
  / {
    controller = 'home'
    view = 'index'
  }
  ...
}

Umlaute und Sonderzeichen

Eine ständige Quelle für Ärger ist bei mehrsprachigen Applikationen auch der Zeichensatz. Damit Umlaute und Sonderzeichen korrekt dargestellt werden, bietet Grails eine Reihe von Codec-Klassen an, mit denen Text nicht nur in die korrekte Form für HTML, sondern auch für URLs oder für JavaScript (AJAX mit JSON) gebracht werden kann. Die entsprechenden Methoden lauten:

"bäh".encodeAsHTML()
"bäh".encodeAsJavaScript()
"bäh".encodeAsURL()

Ausserdem gibt es die entsprechenden Methoden zum Dekodieren, also z.B.

"b&auml;h".decodeHTML()

Beachten Sie zu diesem Thema auch das Kapitel "IDE für Grails einrichten" 

Zurück zum Inhaltsverzeichnis


Groovy

Die Programmiersprache in Rail ist Groovy.

Groovy-Features

 

Unterschiede zwischen Groovy und Java

Hier eine Liste der wichtigsten Syntaxregeln von Groovy, die anders als in Java sind:

Völlig unverständlich ist mir, dass Groovy die Bedeutung von == in Bezug auf Objekte geändert hat:

  Java Groovy
Objektvergleich obj1.equals(obj2) obj1 == obj2
Objektidentität obj1 == obj2 obj1.is(obj2)

Groovy weist gegenüber Java einige radikale Vereinfachungen auf, die im ersten Moment bestechend wirken. Allerdings frage ich mich, ob optionale Klammern und Typangaben tatsächlich Verbesserungen sind, oder ob man im gleichen Schlamassel wie bei VBScript landet (oder bei der neuen deutschen Rechtschreibung).
PS: Das Schlamassel von VBScript ist, dass man im allgemeinen immer alles darf, aber es dann im Speziellen gerade an dieser Stelle aus obskuren Gründen doch wieder nicht funktioniert.
PS2: Das Schlamassel der neuen deutschen Rechtschreibung besteht darin, dass Sowohl-als-auch-Fälle in der Orthographie unser visuelles Gedächtnis für die richtige Schreibweise untergraben. 

Groovy Datentypen

Groovy kennt, wie bereits oben erwähnt, nur Objekttypen.
Als Ersatz für primitive Typen gibt es die folgenden Objekttypen:
Integer1
Long1L
BigInteger1G
Float1.0 oder 1F
Double1.5D
BigDecimal1.0
Character'x' as char oder 'x'.toCharacter()
Achtung: im Gegensatz zu Java reichen die Apostroph nicht, um chars zu kennzeichnen, denn diese sind für normale Strings vorgesehen.

Keyword def: das Keyword def steht für "dynamisch typisiert" (dynamically typed)

Groovy kennt zwei Arten von Strings:
Zur Überprüfung von Datentypen stehen dieselben Möglichkeiten wie in Java zur Verfügung:
Beispiel für instanceof:
${grails.util.GrailsWebUtil.getControllerFromRequest(request)   instanceof GroovyObject}
Um zu überprüfen, ob ein dynamisch typisiertes Objekt eine Zahl ist, lässt sich folgender Code verwenden, denn Number steht für den Java-Datentyp java.lang.Number, den Obertyp aller numerischer Wrapper-Klassen von Byte bis BigDecimal:
def x = 15
if (x instanceof Number) {...

?:-Operator (Elvis Operator)

Wie in Java gibt es als Kurzform für ein if .. then .. else den Operator ?. Groovy hat hier allerdings noch eine elegante zusätzliche Variante: In vielen Fällen will man ja den Wert einer Variablen zurückgeben, falls dieser vorhanden ist, und andernfalls einen andern Wert setzen. Das führt zu einer Redundanz bei der Variablen, also z.B.

def geschlecht = benutzer.maennlich ? benutzer.maennlich : "unbekannt"

Mit dem sogenannten Elvis-Operator ?: lässt sich das in Groovy verkürzen zu

def geschlecht = benutzer.maennlich ?: "unbekannt"

Auf der folgenden Seite findet man übrigens eine gute Zusammenstellung aller Groovy-Operatoren: http://groovy.codehaus.org/Operators

?.-Operator (Safe Navigation Operator)

Bei der Java-Programmierung schreibt man oft Code, um sicherzustellen, dass ein Objekt wirklich vorhanden ist, bevor man auf seine Methoden zugreift. In vielen Fällen ist ein if oder try-catch-Block eine relativ schwerfällige Lösung, weil man eigentlich nur möchte, dass der Code ausgeführt wird, wenn das Object vorhanden ist, und andernfalls gar nichts passiert. Mit dem ?.-Operator hat Groovy eine elegante Lösung für dieses Problem gefunden.

Collections

Wie bereits erwähnt, hat Groovy im Vergleich zu Java die Arbeit mit Collections stark vereinfacht. Es gibt drei Typen von Collections:

Arbeit mit Ranges

Ranges sind Listen von aufeinanderfolgenden geordneten Elementen.

def range2 = 1..5
assert range2.class == groovy.lang.IntRange
assert range instanceof java.util.List

Mit Ranges lässt sich eine kompakte For-Schleife programmieren:

def result = 0
def result2 = ""
(1..5).each {
  result = result + it
  result2 = result2 + it
}
assert result == 15
assert result2 == "12345"

Allgemein nützliche Methoden für Ranges:

def range = 'a'..'g'
assert range.from == 'a'
assert range.to == 'g'

Arbeiten mit Listen

Listen sind Ansammlungen von geordneten Elementen. Der Standardtyp einer Groovy-Liste ist java.util.ArrayList. Die Elemente müssen nicht eindeutig sein.

assert [1, 2] != [2, 1]
assert [1, 2] instanceof ArrayList

Liste definieren, Anzahl Elemente abfragen, alle Elemente löschen:

def emptyList = []
def normalList = [1, 2, 3]
assert normalList.size() == 3
normalList.clear()
assert normalList == []
assert normalList.isEmpty()
assert normalList.size() == 0

Auf Elemente der Liste zugreifen (Zugriff über Positionsindex, nullbasiert):

def normalList = [1, 2, 3]
assert normalList[1] == 2
assert normalList.get(1) == 2
assert normalList.getAt(1) == 2
assert normalList[3] == null

Ein Element mit Index, Methode oder Operator hinzufügen:

def list = []
list[0] = 'a'
list << 'b'
list = list + 'c'
list += 'd'
list += ['e']
list = list + ['f', 'g']
list.add('h')
assert list == ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']

Werden beim Abfüllen mit dem Index Elemente übersprungen, dann werden diese mit null aufgefüllt:

def list = [1, 2, 3]
list[4] = 4
assert list == [1, 2, 3, null, 4]

Einzelne Elemente mit Index, Operatoren oder Methoden entfernen:

def list = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
list.remove(0)
list -= 'b'
list -= ['c', 'd', 'h']
assert list == ['e', 'f', 'g']
//pop() fetches and removes last element
assert list.pop() == 'g'
list = list - ['f', 'e']
assert list.isEmpty()

Umwandlung von Listen in Sets

def list = ['a', 'b', 'c']
def set = list as Set
assert set.class == HashSet

Mit Closures über Listen iterieren

def result = 0
[1, 2, 3, 4].each {
  result += it
}
assert result == 10

Allgemein nützliche Methoden für Listen

//Überprüfen, ob Element in Liste vorkommt
assert [3, 5, 2].contains(2)

//Liste umdrehen
assert [1, 2, 3, 4].reverse() == [4, 3, 2, 1]

//Liste sortieren
def list = [5, 7, 1, 6].sort()
assert list == [1, 5, 6, 7]
list = ['salut', 'hi', 'hello'].sort()
assert list == ["hello", "hi", "salut"]

//Lottozahlen generieren mit shuffle
list = (1..49).toList()
Collections.shuffle(list)
println list[0..5]

//verschachtelte Liste in einfache umwandeln
assert [1, 2, [3, 4]].flatten() == [1, 2, 3, 4]

//Liste in String umwandeln mit Trennzeichen
def text =[1, 2, 3, 4].join("; ")
assert text == "1; 2; 3; 4"
assert text.class == String

//Liste auf die eindeutigen Elemente reduzieren
def list = [1, 3, 3, 5, 5 ,5]
assert list.unique() == [1, 3, 5]

Arbeit mit Maps

Eine Map besteht aus Key-Value-Paaren. Die Keys müssen nicht zwingend denselben Typ haben. Die Keys sind eindeutig, die letzte Definition hat Vorrang:

assert [1: 'a', 2: 'b', 2: 'c'] == [1: 'a', 2: 'c']

Maps sind standardmässig HashMaps:

assert [:] instanceof java.util.HashMap

Map definieren, Anzahl Elemente abfragen, alle Elemente löschen:

def emptyMap = [:]
def normalMap = [1: 'a', 2: 'b', 3: 'c']
assert normalMap.size() == 3
normalMap.clear()
assert normalMap == [:]
assert normalMap.isEmpty()

Auf Elemente der Map zugreifen:

def normalMap = [1: 'a', 2: 'b', 'x':5, 3: 'c']
assert normalMap.x == 5
assert normalMap['x'] == 5
//normalMap.3 throws a MissingPropertyException 
assert normalMap[3] == 'c'
assert normalMap.get(1) == 'a'
assert normalMap.get(4) == null
assert normalMap.get(4, 'd') == 'd' //setzt Default

Einzelne Elemente hinzufügen oder entfernen:

def myMap = [:]
myMap[1] = 'a'
myMap[null] = 'b' //darf null als Key enthalten
assert myMap == [1: 'a', (null): 'b']
myMap.remove(1)
assert myMap == [(null): 'b']

Operatoren mit Maps (im Gegensatz zu Listen ist weder - noch << erlaubt)

assert ['a':4, 'x':3] + ['d':7] == 
  ['a':4, 'x':3, 'd':7] 
//letzte Map hat Vorrang 
assert ['a':4, 'x':3] + ['d':7, 'a':1] == 
  ['a':1, 'x':3, 'd':7] 

Abfragen, ob Keys oder Values vorhanden sind:

assert ['lbl.cust': 'Kunde', 
  'lbl.user': 'Benutzer'].containsKey('lbl.user')
assert ['lbl.cust': 'Kunde', 
  'lbl.user': 'Benutzer'].containsValue('Kunde')
assert map["lbl.cust"] != null
assert map.'lbl.cust' != null

Umwandlung in Listen

assert map.keySet().toList() instanceof List
assert map.values().toList() instanceof List

Umwandlung in Sets

assert [1: 'a', 2: 'b', 3: 'c'].keySet() == 
  [1, 2, 3] as Set
assert [1: 'a', 2: 'b', 3: 'c'].values() as Set ==
  ['a', 'b', 'c'] as Set
assert ['a': 1, 'b': 2].keySet() instanceof Set

Mit Closures über Maps iterieren

def map = [1:2, 2:3, 3:4]
def result = 0
map.each {key, value ->
  result = result + ( key * value)
}
assert result == 20

def map = [ax:2, bx:3, cx:4]
result = ""
map.each {item ->
  result = result + item.key + item.value
}
assert result == "ax2bx3cx4"

Arbeit mit Sets

Sets sind Ansammlungen von ungeordneten, eindeutigen Elementen. 

assert ([1, 2] as Set) instanceof java.util.HashSet

Liste definieren, Anzahl Elemente abfragen, alle Elemente löschen:

def emptySet = [] as Set
def normalSet = [1, 2, 3, 2] as Set
assert normalSet.size() == 3
normalSet.clear()
assert normalSet == [] as Set
assert normalSet.isEmpty()
assert normalSet.size() == 0
def s1 = [1, 2, 2, 3, 3, 4] as Set
def s2 = [1, 2, 3, 4] as Set
def s3 = new HashSet([1,2,3,3,3,4])
assert s1 == s2

Elemente hinzufügen oder entfernen:

def set = [] as Set
set.add('x')
set += 'y'
set << 'z'
assert set == ['x', 'y', 'z'] as Set
set.remove('z')
set -= 'y'
assert (set - set).isEmpty()

Umwandlung in Listen:

def s1 = new HashSet([1, 3, 2, 4, 1])
assert s1.toList() == [1, 2, 3, 4]

Operatoren mit Sets

def s1 = new HashSet([1, 3, 4, 1])
def s2 = [5, 7, 4, 1] as Set
assert s1 + s2 == [1, 3, 4, 5, 7] as Set
assert (s1 + s2).class == HashSet
assert s1 - s2 == [3] as Set
assert s2 - s1 == [5, 7] as Set

Allgemein nützliche Methoden für Sets

//Überprüfen, ob Element in Set vorkommt
def set = [3, 5, 2] as Set
assert set.contains(5)

Methoden, die nur für Listen, aber nicht für Sets existieren:

Weitere Codebeispiele zu Collections findet man unter

http://groovy.codehaus.org/Collections oder

http://groovy.codehaus.org/JN1015-Collections

Zurück zum Inhaltsverzeichnis


Glossar

Zurück zum Inhaltsverzeichnis


Quellen und Links

Zurück zum Inhaltsverzeichnis


Diese Webseite wurde am 03.05.18 um 22:34 von rothen ecotronics erstellt oder überarbeitet.

Impressum

Zurück zu rothen ecotronics

Printed on 100% recycled electrons!