... gegen Computerkopfschmerzen

Grails: Tutorial 1 – Anwendung inkl. Bild-Upload

In diesem Tutorial wird beschrieben, wie man mit Grails eine Web-Anwendung erstellt, die auf Daten einer MySQL-Datenbank zugreift.

Inhalt:
Voraussetzungen:
  • Grails wurde installiert und create-app ausgeführt (siehe: )
  • MySQL wurde installiert, die Datenbank teddies und ein Benutzer pieper angelegt, und der MySQL-Server ist gestartet (siehe: )
Die hier vorgestellte Anwendung dient der Katalogisierung einer Teddybärensammlung. Zu jedem Teddy können verschiedene Informationen hinterlegt werden und es kann ein Bild hochgeladen werden. Außerdem gibt es eine Tabelle mit Teddyherstellern.
Es folgt eine Schritt-für-Schritt-Anleitung. Die fertigen Dateien des Projektes kann man hier herunterladen: Grails-Anwendung inkl. Bild-Upload.

Domain-Klassen für Teddies und Hersteller anlegen

grails create-app wurde bereits ausgeführt (Anleitung hier), die Grails-Anwendung liegt im Verzeichnis ~/Grails/Teddy-DB.
Teddy-DB % cd ~/Grails/Teddy-DB

Teddy-DB % grails create-domain-class Teddies
| Created grails-app/domain/teddies/Teddies.groovy
| Created src/test/groovy/teddies/TeddiesSpec.groovy


Teddy-DB % grails create-domain-class Hersteller
| Created grails-app/domain/teddies/Hersteller.groovy
| Created src/test/groovy/teddies/HerstellerSpec.groovy

Der Fehler
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.codehaus.groovy.reflection.CachedClass (file:/Users/pieper/.sdkman/candidates/grails/4.0.8/lib/org.codehaus.groovy/groovy/jars/groovy-2.5.14.jar) to method java.lang.Object.finalize()
WARNING: Please consider reporting this to the maintainers of org.codehaus.groovy.reflection.CachedClass
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

kann ignoriert werden. Er taucht andauernd auf.

Die gerade erzeugten Dateien werden jetzt mit Inhalt gefüllt.
Der Hersteller wird durch Name, Ort, Land und URL gekennzeichnet und bekommt ein Feld für Bemerkungen. Zu jedem Hersteller können mehrere Teddies eingetragen werden. Das Feld name muß gefüllt werden.
Teddy-DB % vi grails-app/domain/teddies/Hersteller.groovy

package teddies

class Hersteller {

String name
String ort
String land
String url
String bemerkung
static hasMany = [ teddies:Teddies]

static constraints = {
name(unique:true, blank:false)
ort(nullable:true)
land(nullable:true)
url(nullable:true)
bemerkung(nullable:true)

}
String toString() {
name
}

}

Der Teddy bekommt je ein Feld für Name, Kaufdatum und -preis, Wert, Gesamtauflage, Artikelnummer, Nummer, Geburtsdatum, Größe, Schätze und Beschreibung und es kann ein Foto hochgeladen werden. Zu jedem Teddy gibt es genau einen Hersteller.
Teddy-DB % vi grails-app/domain/teddies/Teddies.groovy 

package teddies

class Teddies {

String name
String kaufdatum
Float kaufpreis
Float wert
Integer gesamtauflage
String artikelnummer
Integer nummer
String beschreibung
String geburtsdatum
Float groesse
byte[] featuredImageBytes
String featuredImageContentType
String schaetze

static belongsTo = [ hersteller:Hersteller]

static constraints = {
name(nullable:true)
beschreibung(nullable:true)
groesse(nullable:true)
schaetze(nullable:true)
hersteller(nullable:false)
geburtsdatum(nullable:true)
wert(nullable:true)
artikelnummer(nullable:true)
gesamtauflage(nullable: true)
nummer(nullable:true)
kaufpreis(nullable: true)
kaufdatum(nullable: true)
featuredImageBytes nullable: true
featuredImageContentType nullable: true
}
static mapping = {
featuredImageBytes column: 'featured_image_bytes', sqlType: 'longblob'
}

String toString() {
name
}
}

Die Reihenfolge unter static constraints bestimmt die Reihenfolge der Felder in der Weboberfläche.
Alle Feldnamen werden klein geschrieben.
String toString() {
name

sorgt dafür, daß im Hersteller-View die Bezeichnung des Teddies anstelle der ID angezeigt wird und umgekehrt.

MySQL-Datenquelle konfigurieren

Damit Grails die MySQL-Datenbank nutzt, muß der Java-Connector in die Datei build.gradle im Bereich dependencies { } im unteren Teil der Datei eingefügt werden.
Teddy-DB % vi build.gradle

einfügen:

runtime "com.bertramlabs.plugins:asset-pipeline-grails:3.2.4"
runtime "mysql:mysql-connector-java:8.0.23"
testCompile "io.micronaut:micronaut-inject-groovy"

(Ganz oben in build.gradle steht die Zeile
 repositories {
maven { url "https://repo.grails.org/grails/core" }

Der Connector wird von dort geladen.)

Als nächstes muß die Datenquelle auf MySQL geändert werden, sowie der Name der Datenbank, der Datenbankbenutzer und sein Passwort hinterlegt werden.
Teddy-DB % vi grails-app/conf/application.yml 

ändern:

dataSource:
pooled: true
jmxExport: true
driverClassName: com.mysql.jdbc.Driver
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
username: root
password: 01020304

environments:
development:
dataSource:
dbCreate: create
url: jdbc:mysql://localhost/teddies

test:
dataSource:
dbCreate: none
url: jdbc:mysql://localhost/teddies

production:
dataSource:
dbCreate: none
url: jdbc:mysql://localhost/teddies


Vor dem ersten Aufruf von Grails muß hier der Benutzer root hinterlegt werden, damit die Datenbanktabellen angelegt werden können.
Es lohnt sich, sich mit den verschiedenen Bedeutungen von dbCreate vertraut zu machen, da je nach Einstellung alle Daten gelöscht werden (s. Grails-Dokumentation).
Ich stelle während der Entwicklungsphase dbCreate auf create, danach auf none.
Die Datenbank teddies und der Benutzer pieper müssen in mysql angelegt werden (siehe MacOS: Installation von MySQL)

Jetzt kann man die Applikation testen:
Teddy-DB % grails run-app

Im Browser http://localhost:8080 aufrufen:
Grails: Browser

Grails mit Ctrl-C beenden.
Jetzt wird der Datenbankbenutzer pieper eingetragen, der keine Rechte zum Anlegen und Löschen von Tabellen hat, und dbCreate auf none gesetzt. Damit verhindert man, daß Grails die Datenbanktabellen beim Starten und/oder Stoppen der Anwendung löscht. Wenn man nachträglich das Datenmodell ändert, muß man die Einstellungen entsprechend anpassen.
Teddy-DB % vi grails-app/conf/application.yml 

ändern:

dataSource:
pooled: true
jmxExport: true
driverClassName: com.mysql.jdbc.Driver
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
username: pieper
password: 010203

environments:
development:
dataSource:
dbCreate: none


Controller und Views erzeugen

Grails erzeugt automatisch Controller und Views zum Anlegen, Ändern etc. von Daten. Die Views werden später noch angepaßt.
Teddy-DB % grails generate-all Teddies

| Resolving Dependencies. Please wait...

CONFIGURE SUCCESSFUL in 6s
| Rendered template Controller.groovy to destination grails-app/controllers/teddies/TeddiesController.groovy
| Rendered template Service.groovy to destination grails-app/services/teddies/TeddiesService.groovy
| Rendered template Spec.groovy to destination src/test/groovy/teddies/TeddiesControllerSpec.groovy
| Rendered template ServiceSpec.groovy to destination src/integration-test/groovy/teddies/TeddiesServiceSpec.groovy
| Scaffolding completed for grails-app/domain/teddies/Teddies.groovy
| Rendered template show.gsp to destination grails-app/views/teddies/show.gsp
| Rendered template index.gsp to destination grails-app/views/teddies/index.gsp
| Rendered template create.gsp to destination grails-app/views/teddies/create.gsp
| Rendered template edit.gsp to destination grails-app/views/teddies/edit.gsp
| Views generated for grails-app/domain/teddies/Teddies.groovy

Teddy-DB % grails generate-all Hersteller
| Rendered template Controller.groovy to destination grails-app/controllers/teddies/HerstellerController.groovy
| Rendered template Service.groovy to destination grails-app/services/teddies/HerstellerService.groovy
| Rendered template Spec.groovy to destination src/test/groovy/teddies/HerstellerControllerSpec.groovy
| Rendered template ServiceSpec.groovy to destination src/integration-test/groovy/teddies/HerstellerServiceSpec.groovy
| Scaffolding completed for grails-app/domain/teddies/Hersteller.groovy
| Rendered template show.gsp to destination grails-app/views/hersteller/show.gsp
| Rendered template index.gsp to destination grails-app/views/hersteller/index.gsp
| Rendered template create.gsp to destination grails-app/views/hersteller/create.gsp
| Rendered template edit.gsp to destination grails-app/views/hersteller/edit.gsp
| Views generated for grails-app/domain/teddies/Hersteller.groovy

Grails starten:
Teddy-DB % grails run-app 

Jetzt sieht man die zwei Controller:
zwei Controller

Ein Klick auf teddies.HerstellerController zeigt die Herstellerliste:
Hersteller-Liste

Hier kann man einen Hersteller anlegen:
Hersteller anlegen

Jetzt können ein paar Testdatensätze angelegt werden: zuerst ein Hersteller, dann Teddies.

Die Optik der Weboberfläche anpassen

Jetzt ist es an der Zeit, das Aussehen der Weboberfläche anzupassen. Man kann Grails laufen lassen, während man die Änderungen vornimmt. Es reicht aus, den Browser aufzufrischen.

Grails-Logo ersetzen, Überschrift hinzufügen und die Fußzeile löschen

Zuerst wird das Grails-Logo ersetzt, ein Text in der Kopfzeile hinzugefügt und der Inhalt der Fußzeile gelöscht:
Teddy-DB % vi grails-app/views/layouts/main.gsp

anpassen:

<a class="navbar-brand" href="/#"><asset:image src="teddy_logo.jpg" alt="süßer Teddy"/></a>

einfügen:

</button>
<p style="color: #fbdbff; font-size: 3.5em"> Piepers Däddi gontrola </p>
<div class="collapse navbar-collapse" aria-expanded="false" style="height: 0.8px;" id="navbarContent">

alle 24 Zeilen innerhalb der footer row löschen:

<div class="footer row" role="contentinfo">
</div>

Die Datei teddy_logo.jpg wird in das Verzeichnis grails-app/assets/images/ kopiert.

Einstiegsseite anpassen

Der weiße Grails-Kelch soll weg und es soll ein eigener Text angezeigt werden.

Teddy-DB % vi grails-app/views/index.gsp

löschen:

<asset:image src="grails-cupsonly-logo-white.svg" class="grails-logo"/>

ändern:

<section class="row colset-2-its">
<h1>Willkommen bei Piepers Däddi gontrola!</h1>

<p style="width:75%">
Hier geht es zu den Teddies und Herstellern:
</p>

<div id="controllers" role="navigation">
<h2>Controller:</h2>
<ul>
<g:each var="c" in="${grailsApplication.controllerClasses.sort { it.fullName } }">
<li class="controller">
<g:link controller="${c.logicalPropertyName}">${c.shortName}</g:link>
</li>
</g:each>
</ul>
</div>
</section>

shortName statt fullName sorgt dafür, daß der Name der Datenbank im Controllernamen nicht mit angezeigt wird: HerstellerController statt teddies.HerstellerController.

So sieht die Einstiegsseite jetzt aus:
fertige Einstiegsseite

Bezeichnungen der Felder anpassen

Den Feldbezeichnungen fehlt noch die richtige Schreibweise (Größe statt Groesse):
Feldbezeichnungen mit Umlauten und Eszett

Die Bezeichnungen für die Felder können hier angepaßt werden:
Teddy-DB % vi grails-app/i18n/messages_de.properties

am Ende einfügen:

teddies.groesse.label=Größe
teddies.schaetze.label=Schätze


Syntax: <full packagePath>.<domain name>.<propertyName>.<attribute>'= <message>

Schaltfläche zum Absprung von der Hersteller-Liste in die Teddy-Liste hinzufügen

Teddy-DB % vi grails-app/views/hersteller/index.gsp

einfügen:

<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
<li><a class="list" href="${createLink(uri: '/teddies')}"><g:message code="Teddies"/></a></li>
</ul>

Schaltfläche zum Absprung von der Teddy-Liste in die Hersteller-Liste hinzufügen

Teddy-DB % vi grails-app/views/teddies/index.gsp

einfügen:

<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
<li><a class="list" href="${createLink(uri: '/hersteller')}"><g:message code="Hersteller"/></a></li>
</ul>

So sieht jetzt die Listenansicht der Teddies aus:
mit Schaltfläche Hersteller

Oberhalb der Liste befindet sich der neue Link auf die Herstellerliste.
Im Moment werden nur die ersten sieben Felder zum Teddy angezeigt, das wird später noch geändert.

Löschen-Schaltfläche für Hersteller deaktivieren

Wenn ein Hersteller gelöscht wird, werden automatisch alle dazugehörigen Teddies mit gelöscht. Das kann schmerzhaft sein, deshalb wird der Löschen-Knopf beim Hersteller entfernt.
Teddy-DB % vi grails-app/views/hersteller/show.gsp

auskommentieren:

<!— <input class="delete" type="submit" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" /> —>

Bilder hochladen

Die Anwendung funktioniert soweit schon ganz gut. Um Fotos hochzuladen, muß man noch ein paar Änderungen vornehmen. Zuerst den Controller um den Bildupload erweitern:
Teddy-DB % vi grails-app/controllers/teddies/TeddiesController.groovy

ersetzen:

static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]

def index() {
params.max = Math.min( params.max ? params.max.toInteger() : 10, 100)
[ teddiesInstanceList: Teddies.list( params ), teddiesInstanceTotal: Teddies.count() ]
}


def show(Long id) {

einfügen:

}

def featuredImage(Long id) {
Teddies teddies = teddiesService.get(id)
if (!teddies || teddies.featuredImageBytes == null) {
notFound()
return
}
render file: teddies.featuredImageBytes,
contentType: teddies.featuredImageContentType
}

def editFeaturedImage(Long id) {
Teddies teddies = teddiesService.get(id)
if (!teddies) {
notFound()
}
[teddies: teddies]
}

def uploadFeaturedImage(FeaturedImageCommand cmd) {
if (cmd == null) {
notFound()
return
}

if (cmd.hasErrors()) {
respond(cmd.errors, model: [teddies: cmd], view: 'editFeaturedImage')
return
}

Teddies teddies = teddiesService.update(cmd.id,
cmd.version,
cmd.featuredImageFile.bytes,
cmd.featuredImageFile.contentType)

if (teddies == null) {
notFound()
return
}

if (teddies.hasErrors()) {
respond(teddies.errors, model: [teddies: teddies], view: 'editFeaturedImage')
return
}

Locale locale = request.locale
// flash.message = crudMessageService.message(CRUD.UPDATE, domainName(locale), cmd.id, locale)
redirect teddies
}


protected void notFound() {

Die Datei FeaturedImageCoommand.groovy erzeugen:
Teddy-DB % vi grails-app/controllers/teddies/FeaturedImageCommand.groovy 

package teddies

import grails.validation.Validateable
import org.springframework.web.multipart.MultipartFile

class FeaturedImageCommand implements Validateable {
MultipartFile featuredImageFile
Long id
Integer version

static constraints = {
id nullable: false
version nullable: false
featuredImageFile validator: { val, obj ->
if ( val == null ) {
return false
}
if ( val.empty ) {
return false
}

['jpeg', 'jpg', 'png'].any { extension -> // <1>
val.originalFilename?.toLowerCase()?.endsWith(extension)
}
}
}
}

Jetzt die Listenansicht so ändern, daß das Bild angezeigt wird, sofern eines vorhanden ist (den Inhalt der Datei vollständig ersetzen).
Teddy-DB % vi grails-app/views/teddies/index.gsp

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'teddies.label', default: 'Teddies')}" />
<title><g:message code="default.list.label" args="[entityName]" /></title>
</head>
<body>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
<li><a class="list" href="${createLink(uri: '/hersteller')}"><g:message code="Hersteller"/></a></li>
</ul>
</div>
<div id="list-hersteller" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<div class="list">
<table>
<thead>
<tr>
<g:sortableColumn property="id" title="ID" />
<g:sortableColumn property="name" title="Name" />
<g:sortableColumn property="beschreibung" title="Beschreibung" />
<g:sortableColumn property="groesse" title="Größe" />
<g:sortableColumn property="schaetze" title="Schätze" />
<g:sortableColumn property="hersteller" title="Hersteller" />
<g:sortableColumn property="artikelnummer" title="Artikelnummer" />
<g:sortableColumn property="kaufdatum" title="Kaufdatum" />
<g:sortableColumn property="kaufpreis" title="Kaufpreis" />
<g:sortableColumn property="nummer" title="Nummer" />
<g:sortableColumn property="gesamtauflage" title="Gesamtauflage" />
<g:sortableColumn property="geburtsdatum" title="Geburtsdatum" />
<g:sortableColumn property="wert" title="Wert" />
<g:sortableColumn property="featuredImageBytes" title="Bild" />
</tr>
</thead>

<tbody>
<g:each in="${teddiesInstanceList}" status="i" var="teddiesInstance">
<tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
<td><g:link action="show" id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'id')}</g:link></td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'name')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'beschreibung')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'groesse')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'schaetze')}</td>
<td valign="top" class="value"><g:link controller="hersteller" action="show" id="${teddiesInstance?.hersteller?.id}">${teddiesInstance?.hersteller?.name?.encodeAsHTML()}</g:link></td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'artikelnummer')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'kaufdatum')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'kaufpreis')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'nummer')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'gesamtauflage')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'geburtsdatum')}</td>
<td><id="${teddiesInstance.id}">${fieldValue(bean:teddiesInstance, field:'wert')}</td>
<td> <g:if test="${teddiesInstance?.featuredImageBytes}">
<img width="50" class="featuredImageBytes" src="${createLink(controller:'teddies', action:'featuredImage', id:teddiesInstance?.id)}" />
</g:if>
</td>
</tr>
</g:each>
</tbody>
</table>
</div>
<div class="pagination">
<g:paginate total="${teddiesInstanceTotal}" />
</div>
</div>
</body>
</html>

(Das ist kein schöner Code, aber ich habe keine bessere Möglichkeit gefunden.)
In der Liste werden jetzt alle Felder angezeigt:
mit Bild

In den Ändern-, Anzeigen- und Anlegen-Sichten werden nun die Felder featuredImageBytes und featuredImageContentType ausgeblendet:
Teddy-DB % vi grails-app/views/teddies/edit.gsp

ergänzen:

<f:all bean="teddies" except="featuredImageBytes,featuredImageContentType"/>

Teddy-DB % vi grails-app/views/teddies/create.gsp

ergänzen:

<f:all bean="teddies" except="featuredImageBytes,featuredImageContentType"/>

In der Anzeige-Sicht wird zusätzlich eine Schaltfläche ergänzt, die auf eine Seite zum Hochladen des Bildes führt:
Teddy-DB% vi grails-app/views/teddies/show.gsp

ergänzen:

<f:all bean="teddies" except="featuredImageBytes,featuredImageContentType"/>

einfügen:

<g:link class="edit" action="edit" resource="${this.teddies}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
<g:link class="edit" action="editFeaturedImage" resource="${this.teddies}"><g:message code="teddies.featuredImageUrl.edit.label" default="Bild hochladen" /></g:link>

Jetzt ist eine neue Schaltfläche Bild bearbeiten zu sehen:
mit Schaltfläche Bild bearbeiten

Jetzt muß noch die Seite zum Hochladen des Bildes angelegt werden:
Teddy-DB % cp grails-app/views/teddies/edit.gsp  grails-app/views/teddies/editFeaturedImage.gsp 

und modifiziert werden:
Teddy-DB% vi grails-app/views/teddies/editFeaturedImage.gsp 

<g:form bis </g:form> durch <g:uploadForm ersetzen:

<g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
<g:hiddenField name="id" value="${this.teddies?.id}" />
<g:hiddenField name="version" value="${this.teddies?.version}" />
<input type="file" name="featuredImageFile" />
<fieldset class="buttons">
<input class="save" type="submit" value="${message(code: 'teddies.featuredImage.upload.label', default: 'Hochladen')}" />
</fieldset>
</g:uploadForm>


Sie sieht so aus:
Seite zum Bildupload


Als nächstes wird der Inhalt von TeddiesService.groovy verändert:
Teddy-DB % vi grails-app/services/teddies/TeddiesService.groovy

ändern:

package teddies

import grails.gorm.services.Service

@Service(Teddies)
interface TeddiesService {

Teddies get(Long id)

List<Teddies> list(Map args)

Number count()

void delete(Serializable id)

Teddies update(Serializable id, Long version, byte[] featuredImageBytes, String featuredImageContentType)

Teddies save(Teddies teddies)
}

Jetzt kann man ein Foto hochladen und bekommt es in der Liste angezeigt.
Liste mit Bild

Wenn ein Bild vorhanden ist, soll es auch in der Detailsicht des Teddies gezeigt werden:
Teddy-DB % vi grails-app/views/teddies/show.gsp

einfügen:

<g:if test="${this.teddies.featuredImageBytes}">
<img src="<g:createLink controller="teddies" action="featuredImage" id="${this.teddies.id}"/>" width="200"/>
</g:if>

<f:display bean="teddies" except="featuredImageBytes,featuredImageContentType"/>

Detailseite mit Bild

Fertig!

Produktivsetzung

Wenn man fertig ist, kann man ein lauffähiges WAR erzeugen:
Teddy-DB % grails package

> Task :compileGroovyPages

BUILD SUCCESSFUL in 20s
7 actionable tasks: 6 executed, 1 up-to-date
<-------------> 0% WAITING
> IDLE
> IDLE
| Built application to build/libs using environment: production

Die .war-Datei kann so aufgerufen werden:
% ~/.sdkman/candidates/java/current/bin/java -Dgrails.env=prod -jar ~/Grails/Teddy-DB/build/libs/Teddy-DB-0.1.war

Und so gestoppt werden:
% pkill -f Dgrails


Lose Enden

Maximale Größe der Bilddateien

Die Standardgröße für Dateien beträgt 128 KB. Sie kann in der Datei grails-app/conf/application.yml unter
grails:
controllers:
upload:
maxFileSize: 2000000
maxRequestSize: 2000000

angepaßt werden (Zeilen ggf. einfügen).

Kopiertes Grails-Projekt läuft nicht

Wenn man ein Grails-Projekt an einen anderen Ort kopiert hat und es dann nicht läuft
| Error Command [run-app] error: Profile [org.grails.profiles:base:4.0.0] declares an invalid dependency on parent profile [org.grails.profiles:base:4.0.0] (Use --stacktrace to see the full trace) 

hilft das Löschen der Datei grails-app/build/.dependencies

Dokumentationen

Bei der Erstellung dieses Projekts waren folgende Anleitungen besonders hilfreich:

https://docs.grails.org/4.0.8/guide/single.html
https://guides.grails.org/grails-upload-file/guide/index.html
https://de.slideshare.net/ashishkirpan/grails-connecting-tomysql-13327214


macOS Catalina
Grails Version: 4.0.8
JVM Version: 11.0.10
MySQL Community Server 8.0.23