Richtiges Many-To-Many mit Grails

Will man mit GRAILS ein Real-World-System beschreiben und dabei das schnelle Erstellen von CRUD-Applikationen nutzen, steht man schnell vor dem Problem dass
damit Many-To-Many Verknüpfungen nicht gehen.

Es wird zwar angeboten, wie bei One-To-Many, aber wenn „Add …“ gewählt wird, kann zwar ein neues Child angelegt werden, aber die Verbindung wird nicht
hergestellt.

Auch beim One-To-Many gibt es auf der Many-Side Einschränkungen: es kann nur ein Child per „Add …“ hinzugefügt werden. Weder wird das Zuordnen eines bereits
angelegten Childs ermöglicht, noch wird das Löschen angeboten.

Ausserdem ist die Doku nicht konsistent: Beim Domain-Mapping mit GORM heißt es:

„Grails supports many-to-many relationships by defining a hasMany on both
sides of the relationship and having a belongsTo on the side that owns the relationship“.

Richtig ist aber das belongsTo gehört auf die Child-Side, nicht auf die Parent-Side, so wie es auch im zugehörigen Beispiel ist.

Wichtig ist hier nämlich:

„The owning side of the relationship, in this case Author, takes
responsibility for persisting the relationship and is the only side
that can cascade saves across.“

Also nur die Parent-Side hat den Cascading-Save und somit sollte nur von dort aus geändert werden. Jedenfalls werden nur die von der Parent-Side ausgemachten
Änderungen ohne weiteres persistiert. Würde man von der Client-Side aus ändern, dann müßte die zugehörige Änderung im Parent explizit gesetzt und persistiert werden.

Um die Unterstützung für Many-To-Many in den mit Scaffolding generierten Views zu bekommen, sind die Templates anzupassen. Wie das geht beschreibt der folgende
Artikel.

Viel von den folgenden Codeschnipseln und Konzepten ist durch andere Artikel inspiriert (siehe Referenzen). Was hier zusätzlich dargestellt wird, ist die komplette Logik zu
generieren auch in den Controllern. Und zu guter letzt ist ein komplettes Beispiel beigefügt.

Der erste Schritt für eigene, angepasste Templates ist immer:

grails install-templates

Damit kopiert GRAILS die Standard-Templates lokal ins Projekt und benutzt fortan diese.

Dann sucht man im Verzeichnis src/templates/scaffolding das renderEditor.template. Dieses ist für das Erzeugen der Edit-Controls für die einzelnen Properties
einer Domain-Klasse zuständig.

Hier gibt es eine Methode „renderOneToMany“, die zunächst eine Liste der bereits
verknüpften Objekte mit Link auf deren „Show“-Url ausgibt und dann das schon bekannte „Add …“ anbietet. Dieses „Add“ wird durch folgende Zeile erzeugt:

if( property.oneToMany ) {
    pw.println
" <span
class=\"buttons\"><g:link
   controller=\"${property.referencedDomainClass.propertyName}\"
params=\"['${domainClass.propertyName}.id':${domainClass.propertyName}?.id]\"
action=\"create\"  class=\"create\">Add</g:link></span>"     
 }      
 if( property.isOwningSide() ) { 
 	pw.println
" <span
class=\"buttons\"><g:link
controller=\"${property.referencedDomainClass.propertyName}\"
params=\"['${domainClass.propertyName}.id':${domainClass.propertyName}?.id,
'source':'${domainClass.propertyName}',
'class':'${property.referencedDomainClass.name}',
'dest':'${property.name}','callback':'link']\"
action=\"list\" class=\"save\">Assoc</g:link></span>";   
 	pw.println
" <span
class=\"buttons\"><g:link
controller=\"${property.referencedDomainClass.propertyName}\"
params=\"['${domainClass.propertyName}.id':${domainClass.propertyName}?.id,
'source':'${domainClass.propertyName}',
'class':'${property.referencedDomainClass.name}',
'dest':'${property.name}','callback':'unlink']\"
action=\"list\" class=\"delete\">Remove</g:link></span>";    
 }

Dadurch werden zusätzlich ein „Assoc …“ zum Verbinden und ein „Remove …“ angeboten, falls man von der „Owning-Side“ aus das Objekt editiert.

Somit kann man einerseits auch beim One-To-Many die Assoziation vom Parent aus direkt ändern. Aber – viel wichtiger – Many-To-Many funktioniert so endlich wie
gewünscht.

Um das Bild komplett zu machen, fehlen allerdings noch zwei Bausteine. Die neuen Buttons „Assoc“ und „Remove“ leiten zunächst beide zur „List“-Action der verknüpften Klasse weiter, somit wird der normale List-View verwendet, um das Element auszuwählen, welches verknüpft bzw. gelöscht werden soll. Damit diese Auswahl funktioniert, muss der List-View entsprechend erweitert werden. Im Template
list.gsp wird der „Show“-Link:

<g:link action="show" id="\${${propertyName}.id}">
	\${fieldValue(bean:${propertyName}, field:'${p.name}')}
</g:link>

fallweise durch einen „Choose“-Link ersetzt:

<g:if test="\${params.callback}">
  <g:link action="choose" params="\${params}" id="\${${propertyName}.id}">
      \${${propertyName}.${p.name}?.encodeAsHTML()}
  </g:link>
</g:if>

<g:if test="\${!params.callback}">
  <g:link action="show" id="\${${propertyName}.id}">
     \${${propertyName}.${p.name}?.encodeAsHTML()}
  </g:link>
</g:if>

Dies geschieht – wie man sieht – immer dann, wenn der Callback-Parameter gesetzt ist. Dieser Callback-Parameter wiederum beschreibt, was das „Choose“ jeweils bewirken soll, nämlich entweder ein „Link“ oder ein „Unlink“. Damit das so funktioniert leitet die „Choose“-Action im Controller jeweils auf die „Link“ oder „Unlink“-Action weiter.

Controller-Templates

Diese drei neuen Actions sind in den standardmässig generierten Controllern nicht vorhanden. Auch hier muss also das Template für den Controller angepasst werden.

Das Template für die Controller findet sich ebenso unter src/templates/scaffolding und heißt Controller.groovy. Hier werden am Ende die folgenden Zeilen eingefügt:

def choose = {
   redirect(controller:params.source,action:params.callback,params:params)
}

def link = {  def ${propertyName} = ${className}.get(params["${propertyName}.id"])
def toLink = grailsApplication.getClassForName( params["class"]).get(params["id"])
def d = params['dest']
  ${propertyName}."\${d}".add( toLink );
  render(view:'edit',model:[${propertyName}:${propertyName}])
}

def unlink = {  def ${propertyName} = ${className}.get(params["${propertyName}.id"])
def toUnlink = grailsApplication.getClassForName( params["class"]).get(params["id"])
def d = params['dest']
  ${propertyName}."\${d}".remove( toUnlink );
  render(view:'edit',model:[${propertyName}:${propertyName}])
}

Mit den „Link“ und „Unlink“ Actions kann jeder Controller zwischen beliebigen Domain-Klassen per „add“ und „remove“ Verbindungen erzeugen oder wieder entfernen.

Mit diesen drei – eigentlich minimalen – Änderungen funktionieren nun auch Many-To-Many Verknüpfungen mit GRAILS und Scaffolding out of the box.

Ausblick

Mit der vorgeschlagenen Lösung wird zur Auswahl beim Löschen und Verknüpfen der List-View „missbraucht“. Das ist nicht in jedem Fall optimal. Denkbar wäre auch ein eigener Choose-View, der eine schönere Darstellung hat evtl. auch im Popup-Fenster.

Eine weitere Unschönheit ist die Tatsache, dass der List-View auch beim Löschen immer alle Elemente anzeigt und nicht nur die aktuell verknüpften.

Referenzen

http://www.ibm.com/developerworks/web/library/j-grails04158/index.html

http://www.stainlesscode.com/site/comments/grails_one_to_many_scaffolding/

http://reverttoconsole.com/2008/06/grails-manytomany-gorm-example/

Beispiel

Download hier.

Dieser Beitrag wurde unter Java veröffentlicht. Setze ein Lesezeichen auf den Permalink.

10 Antworten auf Richtiges Many-To-Many mit Grails

  1. Thomas sagt:

    Hallo Stefan,

    danke fuer das Bereitstellen dieser Loesung. Ich habe die 3 Files Controller.groovy, renderEditor.template und list.gsp in /src/templates/scaffolding kopiert (von Deinem Source Code), ich kann auch am Parent-Ende der Many-To-Many bei einem Object Edit -> Assoc rufen, aber wenn ich dann ein Child auswaehle, bekomme ich einen Fehler

    / /choose/2

    not found, HTTP 404, obwohl ich generate-all probiert habe und der Controller inklusive choose generiert wird. Hast Du vielleicht eine Idee?

  2. Stefan Rinke sagt:

    Wie lautet denn die vollstaendige URL? vor dem “choose” muss ja der Name des Controllers stehen, in meinem Beispiel also “../book/choose/2″.

    Ansonsten schick mir einfach mal die generierte Anwendung, dann schau ich mal rein.

    Gruss Stefan

  3. Gregor sagt:

    Es scheint ein Problem zu geben, wenn die Domainklassen in Packages stecken. Dann wirft

    def toUnlink = grailsApplication.getClassForName( params[“class”] ).get(params[“id”])

    eine NullPointerException. Wenn ich den Paketnamen vorn anstelle (z.B.
    def toUnlink = grailsApplication.getClassForName(“de.example.” + params[“class”] ).get(params[“id”])

    funktioniert das ganze einwandfrei.

    Gruß, Gregor!

  4. Stefan Undorf sagt:

    Für Domainklassen in Paketen lässt sich auch in renderEditor.template ‘class':’${property.referencedDomainClass.fullName}’ angeben statt nur der name ohne Paket. Wenn die id etwas einfacher mit ‘source.id':${domainInstance}?.id angegben wird kann die erste Zeile von link und unlink im Controller so aussehen:
    def ${propertyName} = ${domainClass.fullName}.get(params[“source.id”])

    So hats bei mir mit Klassen in Paketen funktioniert.

  5. Horst Krause sagt:

    “Richtiges Many-To-Many mit Grails” dachte ich, genau das was ich suche, super!

    Aber: Mit den Anpassungen kann man zwar Associate/De-Associate durchführen, aber ein Add für n:m geht weiterhin nicht. Der Add-Link führt auf die Create-View der anderen Seite. Dort wird aber schon der übergebene Parameter nicht berücksichtigt, so dass der Rückweg nicht mehr funktioniert. D.h. man kann nur ein Objekt der anderen Seite anlegen, dieses wird aber nicht der ursprünglichen Seite zugeordnet. Außerdem erscheint nach dem Save die list-View der anderen Seite und nicht mehr die Edit-View aus der man gestartet ist.

    Oder hab ich da jetzt was komplett falsch verstanden?

  6. Stefan Rinke sagt:

    In der Tat, da gibt’s noch ein Problem. Aber vielleicht kannst Du den Code ja ergänzen, ich komme leider im Moment gar nicht dazu.

    Gruß Stefan

  7. Horst Krause sagt:

    Ich habe ein paar Anregungen aus diesem Artikel nutzen können. Insbesondere sowas wie grailsApplication.getClassForName kannte ich vorher gar nicht und das war sehr nützlich. Vielen Dank dafür!

    Damit konnte ich meine angepassten Core-Templates nochmal um ein weiteres Feature erweitern.

    Ich habe dazu in einem anderen Forum was geschrieben:

    http://www.groovy-forum.de/read.php?3,5222

  8. existiert das Problem auch noch mit Grails 1.2? ich bekomme beim save die Fehlermeldung:

    Executing action [save] of controller [BlaController] caused exception: org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value: Bla._User_fooBackref; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value: Bla._User_fooBackref

    danke für nen Tipp
    Sebastian

  9. Gregor sagt:

    In Grails 1.2 ist der Beitrag obsolet, dort sind i18n-templates schon dabei. Du solltest das scaffolding-Verzeichnis wieder löschen.

  10. Horst Krause sagt:

    Die i18n Templates sind zwar dabei, bieten aber soweit ich weiß im Original noch immer kein echtes Many-to-many.

    Ich habe damals das Projekt http://code.google.com/p/glossy-templates/ aufgesetzt und dort stark erweiterte Scaffolding-Templates entwickelt. Evtl. kommst du damit weiter.

Hinterlasse einen Kommentar zu Thomas Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>