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:

pw.println "<g:link controller=\"${property.referencedDomainClass.propertyName}\" params=\"['${domainClass.propertyName}.id':${domainClass.propertyName}?.id]\" action=\"create\">Add ${property.referencedDomainClass.shortName}</g:link>"

Diese eine Zeile wird durch folgende Codezeilen ersetzt:

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.


Kommentare

Ansicht der Kommentare: (Linear | Verschachtelt)

  1. Thomas schreibt:

    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 schreibt:

    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 schreibt:

    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 schreibt:

    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 schreibt:

    "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 schreibt:

    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 schreibt:

    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. Sebastian Kurt schreibt:

    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


Kommentar schreiben


Umschließende Sterne heben ein Wort hervor (*wort*), per _wort_ kann ein Wort unterstrichen werden.
Standard-Text Smilies wie :-) und ;-) werden zu Bildern konvertiert.

Um maschinelle und automatische Übertragung von Spamkommentaren zu verhindern, bitte die Zeichenfolge im dargestellten Bild in der Eingabemaske eintragen. Nur wenn die Zeichenfolge richtig eingegeben wurde, kann der Kommentar angenommen werden. Bitte beachten Sie, dass Ihr Browser Cookies unterstützen muss um dieses Verfahren anzuwenden.
CAPTCHA