Aus Linux-Magazin 04/2003

Objektpersistenz mit dem Torque-Framework

JSP-Seiten stellen die Datenobjekte in Webanwendungen dar. Deren Speicherung in einer Datenbank übernimmt eine weitere Abstraktionsschicht. Das muss nicht immer die gewaltige J2EE-Architektur sein, auch Torque (Drehmoment) hat den Dreh raus.

Für kleinere Webanwendungen sind sowohl komplexe J2EE-Server von Bea oder IBM, aber auch das freie JBoss[1] meist unnötig. Deshalb gibt es unabhängig von J2EE eine ganze Reihe von Frameworks für die Speicherung von Objekten in Datenbanken. Einige davon wurden schon in früheren Folgen des Coffee-Shops vorgestellt, zum Beispiel die Java Data Objects (JDO)[2] oder das im Enhydra-Server integrierte Persistenz-Framework DODS[3].

Auch das Jakarta-Projekt hat ein Teilprojekt für persistente Objekte: Torque, Englisch für Drehmoment, was auf die Herkunft aus dem Turbine-Umfeld hindeutet[4]. Der Duden kennt für “torquieren” noch eine andere Bedeutung: foltern oder quälen. Auch das hat seinen Sinn: Torque zeichnet sich durch fehlende und veraltete Dokumentationen wie kaum ein anderes Projekt aus, es war eine echte Qual, zu den ersten nutzbaren Ergebnissen zu kommen. Trotzdem: Hat man erst mal den Dreh raus, spart Torque viel Arbeit. Im Folgenden geht es also zunächst um die Integration in die Entwicklungsumgebung und anschließend um die Einbindung der Datenobjekte in die JSPs.

Download und Installation

Ein umfangreiches Binärpaket der aktuellen Version 3.0 im TGZ-Format mit 3,5 MByte Größe steht auf der Torque-Homepage[4] zum Download bereit. Das Quellpaket ist mit knapp 400 KByte deutlich kleiner, was für Java eher unüblich ist. Trotzdem ist das Binärpaket die bessere Wahl, denn es enthält alle notwendigen JARs für Entwicklung und Betrieb (insgesamt 20 Stück).

Zu empfehlen ist das Entpacken unter »/usr/local«. Um bei späteren Upgrades flexibel zu sein, liegt es nahe, einen symbolischen Link anzulegen:

# tar -xvzf ~/torque-3.0.tar.gz -C U /usr/local
# ln -sf torque-3.0 /usr/local/torque

Wer danach in allen eigenen Build-Dateien bei der Definition von Variablen nur den symbolischen Link benutzt, muss also bei neueren Versionen nur den Link anpassen.

Das Paket selbst enthält keine nennenswerte Dokumentation. Ein Dokumentationspaket ist aber auch nicht verfügbar, hier hilft nur der Weg über »wget«, um die spärlichen und teilweise veralteten Informationsfetzen von der Website zu ziehen. Die API-Dokumentation wiederum ist zu neu, sie bezieht sich bereits auf die Entwicklerversion 3.1-dev. Am besten generiert man sich daher die API-Doks direkt aus dem Quellcode.

Torque benötigt zum Betrieb das Build-Werkzeug Ant in der aktuellen 1.5-Version. Hier ist also in den meisten Fällen ein Upgrade notwendig.

Die Torque-Architektur

Torque hilft im Kern bei zwei Fragestellungen: zum einen bei der Speicherung von neuen oder geänderten Objekten (sie werden auf ein »insert« beziehungsweise »update« abgebildet) und zum anderen beim Lesen (»select«) und Löschen (»delete«) von Objekten. Die Abbildung von Java-Objekten erfolgt dabei auf Zeilen von Tabellen. Primitive Datentypen sind die Spalten, während Objektreferenzen Foreign-Keys auf weitere Tabellen sind. Die Zuordnungen werden durch eine so genannte Schema-Datei im XML-Format beschrieben, sie hat trotz Namensgleichheit aber nichts mit XML-Schema-Definitionen oder SQL-Schema-Definitionen (DDL) zu tun.

Für die Websheet-Applikation aus dem letzten Coffee-Shop, die einfache Adressen mit Name, Vorname und E-Mail-Adresse verwaltet, ist das Schema in Listing 1 abgedruckt. Es lässt sich entweder per Editor von Hand erstellen oder Torque generiert es automatisch aus einer vorhandenen Datenbank beziehungsweise SQL-Schema-Definition heraus (beides per vordefinierter Ant-Task, Abbildung 1). Letztere Methode ist praktisch, wenn die Webanwendung nicht neu entsteht, sondern ein Frontend zu einer existierenden Datenbank ist.

Aus dem Schema erzeugt Torque dann die notwendigen Java-Klassen sowie alle SQL-Statements, um die Datenbank inklusive aller Tabellen aufzubauen. Per Ant-Task sind die SQL-Statements auch gleich ausführbar. In diesem Punkt haben die Torque-Entwickler ein dickes Lob verdient, denn die perfekte Einbindung aller Schritte durch vordefinierte Ant-Tasks erleichtert sowohl den Entwicklungsprozess als auch die spätere Produktionseinführung.

Zwei Klassen sind genug

Je Objekttyp werden zwei Java-Klassen erzeugt (abgesehen von einer internen Hilfsklasse), in diesem Fall »BaseWebsheet.java« sowie »BaseWebsheetPeer .java«. Die erste Klasse ist eine normale Java-Bean mit Setter/Getter-Methoden für alle Attribute (Name, Vorname, E-Mail) und mehreren »save()«-Methoden zum Speichern des Objekts. Die zweite Klasse mit dem Peer-Suffix ist für Operationen auf der Tabelle zuständig, insbesondere für Selects und Deletes.

Eine nachträgliche Änderung dieser Klassen mit eigener Logik wäre zwar möglich, doch gingen dadurch bei einer neuen Generierung nach einer Schema-Änderung auch alle Erweiterungen verloren. Vererbung löst dieses Problem, deshalb generiert Torque auch gleich zwei Stubs als abgeleitete Klassen, also die entsprechenden Klassennamen ohne das Base-Präfix.

Es gibt allerdings zwei gute Gründe, diese Stubs lieber nicht zu verwenden. Erstens liegen sie im selben Package wie die anderen automatisch genierten Klassen. Zum anderen stehen sie auch im selben Verzeichnis. Das macht ein einfaches Clean-Target unmöglich, das den gesamten generierten Verzeichnisbaum löscht. Torque bietet wohlweislich deshalb auch keins an.

Objektzentrierte Sicht

Für das Verständnis von Torque ist die objektzentrierte Sichtweise sehr wichtig. Intern haben Torque-Objekte ein Attribut, das anzeigt, ob das Objekt neu ist oder nicht. Aufgrund dieses Attributs erfolgt bei der »save()«-Methode automatisch die Ausführung eines »INSERT« oder »UPDATE«. Die mit »new« innerhalb des Java-Codes erstellten Objekte sind immer neu. Die Speicherung solcher Objekte kann deshalb durchaus eine Exception zur Folge haben, wenn ein »INSERT« an einem Primary-Key Constraint scheitert.

Für die eigene Programmlogik ist es deshalb entscheidend, ob sie der Objektsicht folgt oder datenbankzentriert ist. Für die Objektsicht bietet Torque die bessere Unterstützung. Genauso wie sich Java-Objekte unterscheiden, auch wenn sie den gleichen Inhalt haben, müssen diese Objekte auch in unterschiedlichen Zeilen der Datenbanktabelle abgespeichert werden. Eindeutige IDs, unabhängig vom Inhalt, sind also dafür notwendig.

In der Schema-Definition muss deshalb stehen, ob eine entsprechende ID automatisch generiert werden soll. Manche Datenbanken bieten hier SQL-Erweiterungen an, aber Torque selbst hat mit seinem ID-Broker einen Datenbank-unabhängigen Mechanismus.

Neben der Generierungskomponente stellt Torque eine Laufzeitumgebung für die persistenten Objekte bereit. Sie besteht aus 20 JAR-Bibliotheken. Welche davon tatsächlich zur Laufzeit notwendig sind, ist leider nirgends dokumentiert, zumindest bei zweien davon, »ant-1.5.jar« und »junit-3.8.1.jar« erscheint dies eher unwahrscheinlich. Auch die von Torque generierten, applikationsspezifischen Klassen verpackt man sinnvollerweise in eine JAR-Datei.

Torque zur Laufzeit

Die Torque-Laufzeitumgebung und die von Torque generierten Klassen sind letztlich nichts anderes als Hilfskomponenten für die eigentliche Webanwendung. Im Entwicklungsprozess kopiert man deshalb diese Bibliotheken in das »WEB-INF/lib«-Verzeichnis der Anwendung. Die Datei »build.xml« aus der Tomcat-Musteranwendung enthält ein anpassbares Prepare-Target; für den eigentlichen Kopiervorgang bietet das Build-Werkzeug Ant eine vordefinierte Copy-Task an (siehe Listing 2).

Listing 1: Websheet-Schema

01 <?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
02 <!DOCTYPE database SYSTEM
03 "http://jakarta.apache.org/turbine/dtd/database.dtd">
04
05 <database name="websheet" defaultIdMethod="idbroker">
06   <table name="websheet" description="Websheet Table">
07     <column name="websheet_id"
08        required="true"
09        primaryKey="true"
10        type="INTEGER"
11        description="Row Id" />
12       <column name="first_name"
13          required="true"
14          type="VARCHAR"
15          size="128"
16          description="First Name" />
17    <column name="last_name"
18       required="true"
19       type="VARCHAR"
20       size="128"
21       description="Last Name" />
22    <column name="email_address"
23       required="true"
24       type="VARCHAR"
25       size="128"
26       description="Email Address" />
27   </table>
28 </database>

Listing 2: Kopieren der JAR-Dateien

012 <project name="Websheet" default="compile" basedir=".">
013
014   ....

442   <target name="prepare" depends="torque">
443
444     <!-- Create build directories as needed -->
445     <mkdir  dir="${build.home}"/>
446     <mkdir  dir="${build.home}/WEB-INF"/>
447     <mkdir  dir="${build.home}/WEB-INF/classes"/>
448
449
450     <!-- Copy static content of this web application -->
451     <copy todir="${build.home}">
452       <fileset dir="${web.home}"/>
453     </copy>
454
455     <!-- Copy external dependencies as required -->
456     <!-- *** CUSTOMIZE HERE AS REQUIRED BY  YOUR APPLICATION *** -->
457     <mkdir  dir="${build.home}/WEB-INF/lib"/>
458     <copy file="${websheet.torque.jar}" todir="${build.home}/WEB-INF/lib"/>
459     <copy todir="${build.home}/WEB-INF/lib">
460        <fileset dir="${torque.home}/lib"/>
461     </copy>

466   </target>
467
468   ....

512 </project>

Die Anwendung muss Torque vor der Verwendung der generierten Klassen konfigurieren und initialisieren. Das geschieht über eine einfache Properties-Datei, sie ist unter[5] abrufbar. Die Konfigurationsparameter fallen fast ausschließlich in zwei Klassen: Umfang und Ort des Loggings einerseits, andererseits Informationen für den Zugriff auf die eigentliche Datenbank, etwa DB-URL, Name, Passwort und Treiberklasse.

Bei einer normalen Zwei-Schichten-Anwendung führt der Client die Initialisierung durch. Bei einer Webanwendung dagegen muss dies der Container tun. Der Servlet-Standard sieht für diesen Zweck so genannte Context Listener vor, die in der Anwendungs-Konfigurationsdatei »web.xml« registriert sind (Listing 3). Beim Erstellen und Löschen des Anwendungskontexts erfolgt dann die Notifikation des Listeners (Listing 4).

Zusammenfügen der Teile

Der Anwendungsfall “Datensatz erstellen” dient hier als Beispiel, um einmal das Zusammenspiel aller Komponenten zu demonstrieren. Abbildung 2 zeigt die Eingabeseite für einen neuen Datensatz. Der »Save«-Button ruft das Save-Record-Servlet auf (Listing 5). Es erzeugt zuerst eine Websheet-Bean, die von der Torque-generierten Klasse »Websheet« abgeleitet ist. Anschließend überträgt das Servlet die Werte aus den Eingabefeldern der Webseite in die Bean. Dank einer Utility-Klasse aus dem Jakarta-Commons-Projekt ist dies sehr einfach (Zeile 59 in Listing 5). Die entsprechende Bibliothek gehört zur Torque-Laufzeitumgebung und steht damit auch für die Anwendung bereit.

Nach einer Validierung, die hier nur prüft, ob alle Felder gefüllt sind und der Klammeraffe in der E-Mail-Adresse vorhanden ist, ruft das Servlet die geerbte »save()«-Methode der Websheet-Bean auf (Zeile 65). Als Letztes speichert es das Objekt in der Session (Zeile 72), damit steht das Objekt allen Webseiten unter dem Namen »currentRecord« zur Verfügung, so auch der Folgeseite »SaveResultsView«, die noch einmal die gerade gespeicherten Daten anzeigt.

Der Code des Servlets ist nicht sehr lang, die wesentlichen Vorgänge – das Füllen der Bean-Attribute mit den Werten der Eingabefelder und das Speichern der Bean in der Datenbank – erfolgen mit zwei Zeilen. Der Rest sind Validierung und Fehlerbehandlung. Beachtenswert ist auch, dass der Servlet-Code unabhängig von den verarbeiteten Daten ist. Er funktioniert genauso, wenn die Webanwendung statt Adressen etwa eine Mitfahrerbörse verwaltet.

Zu Torque gibt es Alternativen. Wer nicht das gesamte Enhydra-Framework einsetzen will, sollte einen Blick auf das OJB-Projekt werfen. OJB steht für Object Bridge und wird ebenfalls unter dem Jakarta-Dach entwickelt[6]. Dieses Framework spielt aber in einer anderen Liga, es implementiert die ODMG-APIs für persistente Objekte und plant auch die vollständige Unterstützung von JDO. Im Unterschied zu Torque darf und muss sich der Anwender mit Transaktionen herumschlagen.

Weitere Alternativen mit einem gewissen Reifegrad sind Hibernate[7] und Castor[8], die wie Torque und OJB mittels XML ein Mapping zwischen Tabellen und Objekten definieren.

Listing 3: Context Listener in der »web.xml«

001 <!DOCTYPE web-app
002     PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
003     "http://java.sun.com/dtd/web-app_2_3.dtd">
004
005 <web-app>
006
007     ....

015     <!-- Listener -->
016
017     <listener>
018       <listener-class>
019          de.bablokb.websheet.listener.TorqueInitializer
020       </listener-class>
021     </listener>
022
023     <!-- Servlet name-class mappings -->
024
025     ....

178 </web-app>

Listing 4: »TorqueInitializer.java«

22 package de.bablokb.websheet.listener;
23
24 import java.io.*;
25 import javax.servlet.*;
26 import javax.servlet.http.*;
27 import org.apache.torque.Torque;
28 import org.apache.commons.configuration.PropertiesConfiguration;

37 public class TorqueInitializer implements ServletContextListener {
38
39   public void contextInitialized(ServletContextEvent sce) {
40     ServletContext ct = sce.getServletContext();
41     try {
42       PropertiesConfiguration config = new PropertiesConfiguration();
43       ct.log("initializing Torque...");
44       config.load(ct.getResourceAsStream("/WEB-INF/Torque.properties"));
45       Torque.init(config);
46       ct.log("...done");
47     } catch (Exception e) {
48       ct.log("initializing of Torque failed",e);
49     }
50   }
51
52   public void contextDestroyed(ServletContextEvent sce) {
53   }
54 }

finally{}

Torque realisiert einen einfacher Ansatz für Objektpersistenz. Es integriert sich dank vordefinierter Ant-Tasks gut in den Entwicklungsprozess, Konfiguration und Nutzung sind unkompliziert. Torque erhebt nicht den Anspruch, alle Probleme zu lösen, selbst die spärliche Dokumentation lässt Einschränkungen erkennen, etwa bei der Query-Performance.

Zwei Kritikpunkte bleiben. Es mangelt an umfassender Dokumentation, dafür ist die Mailingliste hilfreich. Der zweite Kritikpunkt richtet sich an Jakarta allgemein. Alle Teilprojekte sind nur oberflächlich integriert. Sie verwenden zwar viele gemeinsame Komponenten, die in das Commons-Projekt ausgelagert wurden. Was aber fehlt, ist ein Sync-Punkt. Was nutzen die besten Commons-Bibliotheken, wenn Tomcat und jede der Jakarta-Anwendungen jeweils verschiedene Versionen verwenden? So schleppt jede Anwendung eine große Liste von JAR-Bibliotheken mit sich rum, was die Webarchive aufbläht und zu subtilen Fehlern führen kann.

In der nächsten Folge geht es um Taglibs und die JSP Standard Taglib Library, dort findet Torque dann Verwendung für die Datenbankabfrage und die Darstellung des Ergebnisses.

In eigener Sache

Dies war der fünfzigste Coffee-Shop – und in dieser Zeit hat sich viel verändert: Vor vier Jahren war Linux für Java eher eine Randplattform, nur dank des Blackdown-Teams gab es überhaupt einen Port. Mit der Verbreitung von Linux auf den Servern hat sich das zum Glück geändert. Über Kritik, Lob, Anregungen und Wünsche habe ich mich immer gefreut und sie sind nach wie vor willkommen. Wer also ein besonderes Thema gern behandelt sehen möchte, sollte sich einfach melden. (uwo)

Abbildung 1: Torque als Generierungswerkzeug

Abbildung 1: Torque als Generierungswerkzeug

Abbildung 2: Der Eingabedialog für einen neuen Datensatz.

Abbildung 2: Der Eingabedialog für einen neuen Datensatz.

Listing 5: »SaveRecordServlet.java«

22 package de.bablokb.websheet.servlets;
23
24 import java.io.*;
25 import java.util.*;
26 import javax.servlet.*;
27 import javax.servlet.http.*;
28
29 import org.apache.commons.beanutils.*;
30 import de.bablokb.websheet.beans.*;

39 public class SaveRecordServlet extends HttpServlet {
40
41   private final String
42     TARGET_JSP = "/SaveResultsView.jsp",
43     SOURCE_JSP = "/EditView.jsp";

51   public void doPost(HttpServletRequest req, HttpServletResponse res)
52                                 throws ServletException, IOException {
53     String targetJSP = TARGET_JSP;
54     ServletContext ctx = getServletContext();
55     Map parameters = req.getParameterMap();
56     WebsheetBean currentRecord = new WebsheetBean();
57     MessageBean errorMessage = new MessageBean();
58     try {
59       BeanUtils.populate(currentRecord,parameters);
60       if (!currentRecord.isValid()) {
61         errorMessage.setMessage("Invalid or missing input!");
62         req.setAttribute("errorMessage",errorMessage);
63         targetJSP = SOURCE_JSP;
64       } else
65         currentRecord.save();
66     } catch (Exception e) {
67       ctx.log("failed to save data",e);
68       errorMessage.setMessage("failed to save data!");
69       req.setAttribute("errorMessage",errorMessage);
70       targetJSP = SOURCE_JSP;
71     }
72     req.getSession().setAttribute("currentRecord",currentRecord);
73     ctx.getRequestDispatcher(res.encodeURL(targetJSP)).
74       forward(req,res);
75   }
76 }

Infos

[1] JBoss EJB-Server: [http://www.jboss.org]

[2] Coffee-Shop: “Brücken zur Datenbank”, Linux-Magazin 11/01, S. 126 (auch online)

[3] Coffee-Shop: “Im Zeichen des Otters”, Linux-Magazin 08/00, S. 128 (auch online)

[4] Torque : [http://jakarta.apache.org/torque/]

[5] Quellcode von Websheet: [http://www.bablokb.de/LinMag/websheet.tgz]

[6] Homepage des Object-Bridge-Projekts: [http://jakarta.apache.org/ojb/]

[7] Hibernate: [http://hibernate.bluemars.net]

[8] Castor aus dem Exolab-Projekt: [http://castor.exolab.org/]

Der Autor

Bernhard Bablok arbeitet bei der Allianz Versicherungs AG in dem Bereich Data-Warehouse-Systeme. Wenn er nicht gerade Musik hört, mit dem Fahrrad oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um die Objektorientierung. Er ist unter [coffee-shop@bablokb.de] zu erreichen.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben