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

Hinterlasse eine Antwort

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