Model-View-Controller

Autor: Alexander Treptow [minf2622]
Stand: 26.11.2006
 

... [ Seminar "Betriebssysteme, Werkzeuge für das Web und Programmiersprachen" ] ... [ Inhaltsübersicht ] ... Dynamik ...

Übersicht: MVC


Controller

Alle Controller eines Projektes befinden sich immer im "<project_name>/grails-app/controller"-Verzeichnis und enden auf Controller.groovy.

Controller sind in einem MVC (Model-View-Controller) Framework dafür zuständig Antworten auf Anfragen vorzubereiten oder diese zu erstellen. Daher gibt es die Möglichkeit Antworten selbst zu generieren oder an ein View zu delegieren. Mit antworten ist in diesem Fall der Code gemeint, der zum Client der Web-Applikation übertragen wird und die Ansicht, sowie den Inhalt der Seite bestimmt, die dieser abgerufen hat.

In Grails wird der Name des Controllers als URI gemappt und jede Aktion des Controllers auf eine URI in der URI Controller-Namens. Das bedeutet, bei wenn die Applikation auf dem "localhost" läuft, der Projektname "bookapp" ist und einen Controller mit dem Namen "BookController.groovy" besitzt, der über eine "list"-Closure verfügt, dann kann diese über die URI "http://localhost/bookapp/Book/list" referenziert werden. Die zugehörige Controller-Klasse würde wie folgt aussehen:

class BookController {
    def list = {
        // logischer Ablauf und erstellen des Models

        return model
    }
}

Wenn man standardmäßig beim Aufrufen des Controllers ("http://localhost/bookapp/Book") selbst die "list"-Aktion aufrufen möchte, gibt es zwei Möglichkeiten dies zu realisieren.

  1. Man erstellt eine "index"-Closure, die mit Hilfe der "redirect"-Methode auf die Aktion "list" verweist.
    def index = {
        redirect( action : list )
    }
    
  2. Man erstellt ein Attribut "defaultAction" und setzt es auf den Namen der Aktion, hier "list".
    def defaultAction = "list"


Normalerweise haben Views und Controller in einem MVC keine Eigenschaften. in Grails können aber in einem Controller Eigenschaften erstellt werden, diese werden bei einem impliziten return-Statement an die View weitergeleitet. Im folgenden Codebeispiel verwendet die "list"-Closure eine implizite Zurückgabe des Models, die "show"-Closure hingegen eine explizite.

class BookController {
    List books
    List authors

    def list = {
         books = Book.list()
         author = Author.list()
    }

    def show = {
         def b = Book.get( params[ 'id' ] )
         return [ Book : b ]
    }
}

An die View werden in Grails immer Maps zurückgegeben und die hier verwendete Variable "params" ist eine der verfügbaren Umgebungsvariablen, die in jeder Aktion verfügbar sind, ohne dass sie explizit als Parameter angegeben sind.



Zur Anzeige kann man zusätzlich so genannte "Flash Scopes" verwenden, diese stammen ebenfalls aus Rails und speichern temporär Attribute, die ausschließlich für die nächste Anfrage zur Verfügung stehen sollen. Dies kann z.B. verwendet werden um eine Nachricht direkt vor einer Weiterleitung auszugeben. Im folgenden Beispiel wird eine die "list"-Aktion ausgeführt und eine Fehlermeldung ausgegeben, wenn die ID des zu löschenden Buches nicht gefunden wurde.

    def delete = {
        def b = Book.get( params[ 'id' ] )
        if ( !b ) {
            flash[ 'message' ] = "Book not found for id ${ params[ 'id' ] }"
            redirect( action : list )
        }
        // normale Reaktion der Aktion, wenn das Buch existiert
    }


Was ist wenn man nun aber direkt aus dem Controller etwas rendern möchte? Speziell in Ajax-Applikationen ist so etwas sehr hilfreich. Grails stellt hierfür die flexible "render"-Methode zur Verfügung.

Man kann mit diese Methode z.B. normalen Text direkt rendern,

    render "Hello World"

Closures mit Schleifen, etc.,

    render {
        for ( b in books ) {
            div( id : b.id , b.title )
        }
    }

einen speziellen View,

    render( view : 'show' )

ein Template für eine bestimmte Zusammenstellung

    render( template : 'book_template' , collection : Book.list() )

oder auch XML

    render( text : "<xml>irgendein xml-code</xml>" , contentType : "text/xml" , encoding : "UTF-8" )

Wie man weiter oben schon gesehen hat, gibt es in Grails auch die Möglichkeit Aktionen umzuleiten. Hierfür ist die "redirect"-Methode zuständig. Diese Methode kann einfach auf eine andere Aktion referenzieren, auf eine URI oder auf eine andere Methode in einem anderen Controller. Optional können auch die Parameter mit übergeben werden, mit denen die Aktion aufgerufen wurde.

    redirect( action : login , params : [ "id" : "${ params[ 'id' ] }" ] )

    redirect( action : "/Author/list" )

    redirect( controller : 'home' , action : 'index' )


Angenommen man möchte die "list"-Aktion in mehreren Views verwenden, aber für jede der verwendenden Aktionen zusätzlich weiteren Content hinzufügen. Grails bietet dafür die "chain"-Methode an, mit der sich Aktionsketten bilden lassen.

class ChainController {
    def bookListWithAuthors = {
        chain( action : bookList , model : [ "authors" : Author.list() ] )
    }
  
    def bookList = {
        // def model = chainModel[ "authors" ]
        return [ "books" : Book.list() ]
    }
}

Der Aufruf von "bookListWithAuthors" würde folgendes Model zurückgeben:

    [ "authors" : Author.list() , "books" : Book.list() ]
über "chainModel" kann auf einzelne Models der vorherigen Aktionen zugegriffen werden um diese z.B. zu bearbeiten. Dafür sollte man beachten, dass man, wenn man auf Objekten die z.B. über die "get"-Methode aus der Datenbank ausgelesen hat, direkt auf der Datenbank arbeitet. Somit ist es sicherer in der "chain"-Methode nur neu erzeugte Objekte zu übergeben, da dies dann keine direkte Verbindung mehr zu den Daten in der Datenbank haben.

Optional können bei der "chain"-Methode, wie bei der "redirect"-Methode Parameter mit übergeben werden.

    chain( action : bookList , model : [ "authors" : Author.list() ] , params : [ "id" : "${ params[ 'id' ] }" ] )


Grails hat noch ein weiteres sehr hilfreiches Konzept aus Rails übernommen, die Action Interceptoren, die in Rails Filter genannt werden, sind Eigenschaften, die Aktionen bedingt ausführen. Es gibt zwei Arten von Interceptoren. Der "beforeInterceptor" wird ausgeführt, bevor die eigentliche Aktion ausgeführt wird. Der "afterInterceptor" erst nachdem eine Aktion ausgeführt wurde.

"beforeInterceptor"-Eigenschaften können die Ausführung von Aktionen beeinflussen. Dieser würde es nicht tun:

    def beforeInterceptor = {
        println 'Tracing action ${ actionUri }'
    }
Dieser hingegen würde für alle Aktionen außer der "login"-Aktion die "auth"-Methode aufrufen, die private ist. Diese überprüft ob der Benutzer eingeloggt ist, ansonsten wird die aufgerufene Aktion nicht ausgeführt sondern die "login"-Aktion gestartet. Hierbei ist zu beachten Methoden, durch Klammern nach dem Namen gekennzeichnet, sind den Defaulteinstellungen nach private und Aktionen sind public.
    def beforeInterceptor = [ action : this.&auth , except : 'login' ]

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

    def login = {
        // zeige die login-Seite an
    }
Es ist ebenfalls notwendig, dass die "login"-Aktion ausgeschlossen ist von der Überprüfung des Interceptors, ansonsten würde der Controller in eine Endlosschleife laufen und die aufgerufene Methode des Interceptors muss einen bool'schen-Wert zurückgeben, da ansonsten die aufgerufene Aktion ausgeführt wird. Standardmäßig wird "true" zurückgegeben.

Alternativ zum "except"-Parameter kann man einen "only"-Parameter angeben, der Interceptor wird dann nur bei Aufruf dieser Aktionen ausgeführt.

    def afterInterceptor [ action : this.&auth , only : [ 'controlPanel' , 'secure' ] ] 

Der "afterInterceptor" bekommt als Argument das Ergebnis-Model und kann diese nachträglich bearbeiten.

    def afterInterceptor = { model ->
        println 'Tracing action ${ actionUri }'
    }

[ Top ]

Views & Layout

Grails unterstützt die Erstellung von Views mit Java Server Pages und Groovy Server Pages, dabei wird ein Konventionsmechanismus verwendet, der einer Aktion eines Controllers eine View zuordnet. Der folgende Controller

class BookController {
    def list = {
        [ "books" : Book.list() ]
    }
}
würde eine Map von Büchern an die View weiterreichen, die View, die dabei aufgerufen wird, muss sich im Verzeichnis "grails-app/views/book" befinden und list.jsp oder list.gsp lauten. Der Inhalt könnte nach GSP wie folgt aussehen:
<html>
    <head>
        <title> Book list </title>
    </head>
    <body>
        <h1> Book list </h1>
        <table>
            <tr>
                <th> Title </th>
                <th> Author </th>
            </tr>

            <g:each in=" ${ books } ">
                <tr>
                    <td> ${ it.title } </td>
                    <td> ${ it.author } </td>
                </tr>
            </g:each>
        </table>
    </body>
</html>


Wenn man in Grails Layouts erstellen möchte, also einheitliche Ansichten für mehrere Views, so dass auch leicht große Seiten mit vielen Unterseiten ein einheitliches Layout vorweisen, kann man dies mit Hilfe des Grails Support für SiteMesh. Dabei gibt es zwei Möglichkeiten solche Layouts zu erstellen. Die erste Möglichkeit ist es eine View mit einem Layout über das "layout"-Metatag zu assoziieren.

<html>
    <head>
        <meta name="layout" content="main"></meta>
    </head>
    <body>This is my content!</body>
</html>

Im nächsten Schritt muss dann ein Layout erstellt werden, das sich main.gsp nennt und in "grails-app/views/layouts" liegt.

<html>
    <head>
        <title><g:layoutTitle default="Ein Beispiellayout" /></title>
        <g:layoutHead />
    </head>
    <body onload=" ${ pageProperty( name : ' body.onload ' ) } ">
        <div class="menu">
            <!-- hier kommt mein menü hin -->
        </div>
        <div class="body">
            <g:layoutBody />
        </div>
    </body>
</html>

Die zweite Möglichkeit ist die des "layout by convention", dafür hat man z.B. den oben angegebenen Controller. Nun erstellt man sich ein Layout "book.gsp" dieses legt man im Ordner "grails-app/views/layouts" an. Das Layout wird von nun an für alle Aktionen des "BookController" verwendet. Man kann zusätzlich noch Layouts für einzelne Aktionen definieren. Dafür legt man z.B. die Datei "grails-app/views/layouts/book/list.gsp" an. Diese überlagert dann, dadurch dass sie spezifischer ist das "book.gsp"-Layout.


[ Top ]

GORM (Grails Object Relational Mapping)

GORM bildet im MVC-Framework Grails das Model ab, es sind Klassen, die Eigenschaften haben und Beziehungen untereinander definieren. Sie bilden also eine relationale Datenbank nach. Die Datenbank selbst wird normalerweise mit Hibernate anhand dieser Klassen erstellt. Domain-Klassen, so werden diese in Grails genannt, können einfach mit "grails create-domain-class" erstellt werden. Man sollte sich beim Aufrufen dieses Befehls im Root-Verzeichnis des Projektes befinden. Die Klasse wird dann im Verzeichnis "grails-app/domain" angelegt. Jede Domain-Klasse besitzt standardmäßig zwei Attribute, die vom Typ "Long" sind, sie "id" und die "version". Diese Attribute müssen nicht in der Domain-Klasse eingetragen sein, sie existieren immer.

1:N-Beziehungen zu erstellen ist sehr einfach, dafür wird eine Eigenschaft vom Typ "Set" erstellt und zu der "relatesToMany"-Map hinzugefügt. Wobei es für 1:1- oder N:1-Beziehungen in Grails nicht einmal notwendig ist diese zu der "relationships"-Map hinzuzufügen. Die "relationships"-Map enthält alle Einträge, die über "relatesToMany", "belongsTo" oder "hasMany" hinzugefügt wurden.

class Author {
    Long id
    Long version

    def relatesToMany = [ books : Book ]

    String name
    Set books = new HashSet()
}
Diese Klasse definiert erstmal eine unidirektionale 1:N-Beziehung, um diese bidirektional zu machen, muss in der referenzierten Klasse das "belongsTo"-Attribut definiert werden.

class Book {
    Long id
    Long version

    def belongsTo = Author

    Author author
    String title
}

Die "Book"-Domain-Klasse könnte hierbei auch zu mehreren Klassen gehören also eine N:1-Beziehung realisieren.

    def belongsTo = [ Author , Publisher ]

N:M-Beziehungen werden über das "hasMany"-Attribut definiert. Dabei müssen beide Seiten dieses Attribut verwenden. Eine Seite benötigt zusätzlich noch das "belongsTo"-Attribut, damit eine eindeutige Beziehung besteht, ansonsten wird ein Fehler beim Starten der Applikation ausgegeben.

class Book {
    def belongsTo = Author
    def hasMany = [ authors : Author ]
    // ...
}
class Author {
    def hasMany = [ books : Book ]
    // ...
}

Im Normalfall sind alle Eigenschaften einer Domainklasse über die volle Laufzeit vorhanden (non-transient) und müssen immer angegeben werden (required). Dies kann mit den Eigenschaften "optionals" und "transients" geändert werden. Zudem ist es möglich erforderlichen Angaben direkt Defaul-Werte zuzuweisen.

class Book {
    def optionals = [ "releaseDate" ]
    def transients = [ "digitalCopy" ]

    String title = ''
    String author = '[author unknown]'
    Date releaseDate
    File digitalCopy
}

Allerdings wird man ohne CRUD-Operationen wenig mit einer Datenbank anfangen können, da diese für das Arbeiten mit den Daten auf der Datenbank zuständig sind. CRUD steht für Create/Read/Update/Delete und beschreibt damit die vier Basisoperationen.
Um in Grails neue Einträge in die Datenbank einzufügen muss man die "save"-Methode verwenden, die jede Domain-Klasse besitzt. In Grails-Versionen < 0.3 mussten die Abhängigkeiten noch direkt angegeben und eingetragen werden. Ab Version 0.3 ist dies nicht mehr nötig.

Vor Version 0.3 hätte man folgendes schreiben müssen:

    def a = new Author( name : "Stephen King" )
    def b = new Book( title : "The Shining" , author : a)
    a.books.add(b)
    a.save()

Nun ist es möglich das über dynamische Methoden abzukürzen, die zudem den Author im Buch hinzufügen:

    def a = new Author( name : "Stephen King" )
                      .addBook( new Book( title : "The Shining" ) )
                      .addBook( new Book( title : "The Stand" ) )
    a.save()

Um Daten bzw. Domain-Klassen Instanzen aus der Datenbank auszulesen gibt es einerseits einige vordefinierte Methoden, aber zusätzlich noch dynamische Methoden, die theoretisch beliebig lang sein können. Die statischen Methoden sind z.B. "get", "findAll", "list" oder "listOrderedByX", wobei letztere schon gewisse Dynamik besitzt.

    Book.get(1)                                     -> Liest anhand der ID aus (hier ID = 1)
    Book.findAll()                                  -> Liest alle Einträge die zur Book-Domain gehören aus
    Book.list(10)                                   -> Liest die ersten 10 Einträge aus
    Book.listOrderedByTitle()                       -> Liest alle Einträge aus und sortiert diese anhand des Titels
    Book.find( new Book( title : 'The Shining' ) )  -> Sucht anhand einer Beispielinstanz

Andere Möglichkeiten mit dynamische Methoden sind:

    Book.findbyTitle( "The Stand" )
    Book.findByTitleLike( "Lord of the%" )
    Book.findByReleaseDateBetween( firstDate , lastDate )
    Book.findByTitleLikeOrReleaseDateLessThan( "%Guide to the Galax%" , someDate )

    Book.findAllByAuthor( Author.get( 1 ) )                                        -> Sucht anhand der Beziehung
    Book.findAllByAuthor( Author.get( 1 ) , [ sort : 'title' , order : 'asc' ] )   -> und nun mit Sortierung

Um die nun ausgelesenen Einträge zu verändern, sind die Update-Operationen notwendig. Diese sind in Grails recht intuitiv zu handhaben. Man kann auf ein Attribut eines ausgelesenen Objektes direkt schreiben. Diese Veränderungen werden am Ende einer Aktion aus dem Controller in die Datenbank übertragen.

    def b = Book.get(1)
    b.releaseDate = new Date()

Dieses Verhalten ist nicht immer erwünscht, denn so können unzulässige Einträge in die Datenbank gelangen. Um das zu vermeiden gibt kann man entweder die "validate"- oder die "save"-Methode verwenden, die die Konsistenz der Datenänderungen überprüfen. Zudem speichert die "save"-Methode, die Einträge sofort in die Datenbank, es sei denn eine Verletzung der Datenkonsistenz ist aufgetreten.

    def b = Book.get(1)
    b.title = null            -> würde einen Fehler verursachen
    b.save()                  -> verhindert, dass der Datensatz in die Datenbank übernommen wird

Nun zum Löschen der Objekte, dies ist denkbar einfach. Man führt auf einer ausgelesene Instanz einfach die "delete"-Methode aus.

    def b = Book.get(1)
    b.delete()

...
[ Seminar "Betriebssysteme, Werkzeuge für das Web und Programmiersprachen" ] ... [ Inhaltsübersicht ] ... [ Top ] ... Dynamik ...