Webframeworks für JVM-Sprachen: Rails gegen Grails

Seite 2: ORM, GORM und Controller

Inhaltsverzeichnis

Architektonisch besteht Rails aus mehreren Ruby-Gems, sogenannten Core-Extensions wie ActiveRecord für objektrelationale Abbildungen (Object Relational Mapping, ORM), ActiveSupport für Ruby-Spracherweiterungen und ActionPack für Routing, Controller und View-Rendering. Grails hingegen vereint zum Großteil bewährte Java-Projekte wie Spring-Komponenten für Request Routing oder Hibernate für ORM und vereinfacht sie in Set-up und Konfiguration.

ORM ist das geheimnisvolle Bindeglied zwischen den Datenbanktabellen und Objekten der Anwendung. Die das Active-Record Pattern (nach Fowler) zugrunde legenden ORMs beider Frameworks bieten ein ähnliches Feature Set, vor allem bei dynamischen Find*-Methoden, Query-APIs, Validierung und Callbacks.

Rails zieht bewusst keine Trennung von Domain-Logik und Persistenz. Vor allem, wenn der Controller einfach bleiben soll, landet alles in den jeweiligen Domain-Klassen (in Rails Models genannt). Derartige Ableitungen von ActiveRecord::Base enthalten die Datenstruktur, die Constraints und eben Fachlogik, die oft mit anderen Models – schlimmstenfalls mit anderen Datenquellen oder Webservices – kommuniziert. Der Ausweg daraus ist steinig (Separierung) und erhält kaum Unterstützung durch das Framework. Positiv dagegen ist, dass in Rails der ORM-Austausch bereits eingeplant ist und DataMapper beziehungsweise sein Nachfolger RubyObjectMapper gerade bei Adaptionen aus der NoSQL-Ecke eine Alternative sein kann.

Grails Object Relational Mapping (GORM) ist ein mächtiges Groovy-Pendant, das konsequent auf Hibernate, ein weiteres, schlachterprobtes Java-ORM, setzt. Dabei sind zunächst auch Mapper-Klassen zu definieren (in Grails Domains genannt), die den Datenzugriff, die Validierung (Constraints) aber auch grundlegende Domain-Logik enthalten. Für nicht relationale Datenbanken gibt es GORM-Adapter für Neo4j, Redis, standardisierte REST-Clients und so weiter.

Wenn man mit Rails ActiveRecord und ähnlichem gearbeitet hat, fühlt sich das Hibernate-Konzept zunächst fremd an, denn ein save() oder delete() hält erst einmal alle Änderungen in seiner Session (Datenbank, nicht HTTP) und gibt sie erst an die Datenbank weiter, wenn der darüber liegende Thread (beispielsweise eine Controller Action) beendet oder ein manuelles Session-Flush ausgerufen wird. Noch heikler ist das automatische Speichern von mit get("SomeId") geladenen Entitäten, ohne explizites save(). Grails bietet zur sauberen Strukturierung der Fachlogik eine Services-Schicht, die sich vergleichsweise einfach einbinden lässt (Namenskonvention, Auto-Injection), standardmäßig transaktional arbeitet und unter dem Gesichtspunkt erste Wahl für Datenbankinteraktionen aus dem Controller heraus ist.

Viele Entwickler ziehen zum Beispiel SQLite in Rails oder HSQLDB in Grails (In-Memory) Oracle im lokalen Set-up vor. Dafür und wenn die Datenbank-Migrationen unter den Blicken der Administratoren durch die Testumgebungen in Richtung Produktion wandern, braucht es ein unabhängiges, textbasiertes Format. In den meisten Projekten kommt hierfür Liquibase (via Plug-in) zum Einsatz. Rails liefert zudem eine eigene Migrations-DSL, die sich mit Rake-Tasks auf der Ziel-DB ausführen lässt. Grails wiederum kennt unterschiedliche Konfigurations-Modi, die die Domain-Klassen mit der Datenbankstruktur beim App-Start oder Klassen-Reload abgleichen und gegebenenfalls anpassen.

Ein Beispiel für Aufbau und Vererbung einer GORM Domain-Klasse sieht wie folgt aus:

// GORM Composition
class Address {
String number
String code
}
class Person {
Address homeAddress
Address workAddress
static embedded = ['homeAddress', 'workAddress']
}

// GORM Inheritance (mittels Typ-Spalte oder in einer weiteren Tabelle)
class Content {
String author
}
class BlogEntry extends Content {
URL url
}

Viel mehr, als Daten aus der Webschicht entgegenzunehmen und nach deren Verarbeitung eine Response (beispielsweise an die View delegiert) zurückzuliefern, muss ein Rails/Grails-Controller gar nicht. Beide Frameworks unterstützen bei der Routing-Konfiguration und bieten Callback-Interceptors. Regelbasierendes, übergreifendes Filtern lässt sich in Rails in der Rack-Middleware beziehungsweise in Grails über Servlet- oder Grails-Filter (mit Hooks zum Beispiel beim View Rendering einer Aktion) implementieren.

Unter anderem hat die Rails-Community "Fat Model, Skinny Controller" als Pattern manifestiert, das Wiederverwendbarkeit und besseres Testing verspricht. Wenn Projekte ohne strukturelle Vorgaben wachsen, führt das unweigerlich zu aufgeblasenen Model-Klassen mit verwässerter Verantwortlichkeit. Grails gibt daher mit Command- und Service-Objekten Möglichkeiten vor, den Persistenz-Teil abzukoppeln. In Rails hingegen gibt es kaum Standards dazu. Dennoch kann man sich bei Framework-unabhängigen Paradigmen wie Data Context Interaction (DCI) oder Hexagonal Architecture Anregungen holen.

Während Rails auch dem Thema Data Binding kaum Beachtung schenkt, kann Grails beispielsweise Request-Parameter automatisiert auf nahezu beliebige Datenstrukturen mappen, die sich in der Signatur der Action-Methode vorgegeben lassen. Ist der Parameter eine Domain-Klasse oder ein CommandObject, kann das Programm ihn sogar vor der eigentlichen Verarbeitung validieren und Assoziationen laden.

// Parameter-Mapping und ggf. TypeCast von <id>
def show(String id) {
respond Campaign.get(id)
}

// Binding und Validierung von <campaignCommand>
def create(CampaignCommand campaignCommand) {
respond new Campaign(campaignCmd.properties)
}

// Laden der GORM Domain <campaign>
def edit(Campaign campaign) {
respond campaign
}