Ulrichs Werkbank

Persistenz für Java-Objekte
Eine universelle und einfache Art, relationale Datenbanken mit Java zu verwenden

Copyright (c) Ulrich Hilger
5. März 2011
alle Rechte vorbehalten

 

Nicht erst mit Erscheinen von Java gibt es einen elementaren Bruch zwischen objektorientierter Programmierung und relationalen Datenbanken: Wie bekommt man Objekte aus dem Hauptspeicher in eine Datenbank und wieder zurück, ohne für jede Klasse individuellen Code zu bauen? Das Thema Persistenz ist in der Java-Welt ein delikates Thema, da die Java Plattform keine eigene Lösung für das dauerhafte Speichern von Objekten mitbringt. 

Freilich gibt es mit der Java Database Connectivity (JDBC) eine universelle Möglichkeit zur Verbindung von Java mit Datenbanken, die keine Wünsche öffen läßt. JDBC implementiert allerdings lediglich die "Mechanik" der Datenbanknutzung, eine einfache Möglichkeit zur Speicherung von Objekten ist das noch nicht.

JDBC ist keine komplette Persistenzschicht

Jede Anwendung, die mit JDBC arbeitet, erfordert ihre eigene Persistenzschicht, die Datenzugriffsobjekte mit der typischen CRUD-Logik (Create, Read, Update, Delete) 'verdrahtet'. Gerade der Code für diese typischen Schritte ist in großen Teilen immer wieder gleich. Gleichartige Teile der Methode zum Lesen aus einer relationalen Datenbank sehen zum Beispiel etwa so aus:

 

 

 

    Connection conn = null;
    Statement stmt = null// Or PreparedStatement if needed
    ResultSet rs = null;
    try {
      conn = ... get connection from connection pool ...
      stmt = conn.createStatement("select ...");
      rs = stmt.executeQuery();
      // ... iterate through the result set ...
      rs.close();
      rs = null;
      stmt.close();
      stmt = null;
      conn.close()// Return to connection pool
      conn = null// Make sure we don't close it twice
    catch (SQLException e) {
      // ... deal with errors ...
    finally {
      // Always make sure result sets and statements are closed,
      // and the connection is returned to the pool
      if (rs != null) {
        try {
          rs.close();
        catch (SQLException e) {
          ;
        }
        rs = null;
      }
      if (stmt != null) {
        try {
          stmt.close();
        catch (SQLException e) {
          ;
        }
        stmt = null;
      }
      if (conn != null) {
        try {
          conn.close();
        catch (SQLException e) {
          ;
        }
        conn = null;
      }
    }
Java2html

 

(Quelle: Tomcat-Dokumentation)

Jeder Datenbankzugriff, sei es Insert, Update, Delete oder Select erfordert eine individuelle Methode für jedes Objekt, das gelesen oder geschrieben werden soll. Ohne eine Programmbibliothek, die hierfür eine generische Lösung bietet, kommt so eine bedeutende Menge weitgehend redundanter Code und entsprechend hoher Aufwand zusammen.

Die Java Persistence API ist sehr umfangreich

Mit der Java Persistence API (JPA) gibt es mittlerweile eine Spezifikation und auch einige Implementationen. Die JPA ist eine Spezifikation, die Persistenzfunktionen zur Anwendung hin standardisiert. Wesentlicher und großer Vorteil der JPA aus Sicht von Anwendungen: Man kann stets dieselbe Sammlung von Werkzeugen für die Persistenz einsetzen, ganz unabhängig von deren Implementierung und auch unabhänging davon, wie und wo Objekte gespeichert werden. Allerdings ergeben sich bei näherem Hinsehen ein, zwei Schwierigkeiten.

  • Die JPA ist wegen ihres universellen Ansatzes sehr umfangreich. Sie ist monolithisch und nicht modular aufgebaut. Man kann sie nur ganz oder garnicht nutzen.
  • Die Implementierung ist nicht Teil der Java Plattform. Damit man sie benutzen kann, muss jede Anwendung eine komplette JPA-Implementierung verteilt bekommen, auch, wenn jeweils nur ein Bruchteil der API benötigt wird. 

Diese beiden Punkte sind der Grund, warum das typische Ökosystem für den Einsatz der JPA ein Applikationsserver der Kategorie Glassfish, Geronimo oder Websphere ist. Nimmt man anstelle eines Applikationsservers einen Servlet Container wie Tomcat, hat man beträchtlichen zusätzlichen Aufwand für die Einrichtung von JPA und trotzdem einen großen zusätzlichen "Overhead". Immerhin "wiegt" eine Implementierng der JPA wie z.B. OpenJPA nicht weniger als 127 MB, wohlgemerkt ohne Applikationsserver.

Zum Vergleich: Das gesamte Java Runtime Environment hat rund 90 MB, ein Tomcat hat nicht einmal 10 MB, ein "Mini-Applikationsserver" bestehend aus Tomcat und Derby hat weniger als 15 MB. Zur Laufzeit ist der Ressourcenbedarf von JPA um Größenordnungen höher als das, was ein Servlet Container wie Tomcat einschließlich Datenbank benötigt und nocheinmal um Größenordnungen mehr, wenn JPA zusammen mit einem Applikationsserver wie Geronimo läuft, vom erhöhten Betriebs- und Wartungsaufwand ganz zu schweigen.

Kurzum, für sehr viele Anwendungen ist diese Infrastruktur zu groß. Wenn man nur eine einfache Persistenzschicht benötigt, lohnt es für viele Anwendungen nicht, den Aufwand für Implementierung und Betrieb der JPA zu unternehmen. 

Eine universelle Persistenzschicht

Klammert man die JPA aus, fehlt dennoch nicht viel für eine wiederverwendbare Persistenzschicht: Eine Möglichkeit, beliebige Klassen an eine Datenbankstruktur zu binden und einen Anwendungskern, der das CRUD-Muster implementiert. Zudem die Möglichkeit, in der Datenbank gespeicherte Informationen in Objekte zurückzuführen.

Fügt man diesen Anforderungen die Kapselung von Objekten mit Bezug zur Datenbank wie z.B. Connection, Statement und ResultSet hinzu, erhält man obendrein eine saubere, robuste und wiederverwendbare Handhabung dieser Elemente, was besonders in Verbindung mit Connection Pools praktisch ist.

All dies leistet die Programmbibliothek BaseLink. BaseLink ist gerade einmal 20k klein, der Ressourcenbedarf zur Laufzeit ist sehr gering. Wie man die Klassenbibliothek verwenden kann, um den Aufwand für Datenbankzugriffe zu verringern, wird im nachfolgend näher betrachtet.

Der Weg in die Datenbank

Möchte man Objekte einer Anwendung dauerhaft speichern, werden folgende Operationen benötigt:

  • Hinzufügen eines neuen Objekts: Create
  • Ändern eines bereits gespeicherten Objekts: Update
  • Löschen eines bereits gespeicherten Objekts: Delete

Die von jedem relationalen Datenbanksystem implementierte Structured Query Language (SQL) hält hierfür die Operationen INSERT, UPDATE und DELETE bereit und die Aufgabe einer Persistenz-Schicht ist, die SQL-Befehle dieser Operationen dynamisch für beliebige Objekte passend zu erzeugen. Mit Übergabe eines zu speichernden Objekts soll die Persistenz-Schicht selbst entscheiden, wie die Übersetzung in die Datenbank vonstatten geht.

Bindung mit Annotationen

Ganz ohne Bezug zur Datenbank geht das nicht. Deshalb werden die Teile eines Objekts, die in einer Datenbank gespeichert werden sollen, mit der Hilfe von Annotationen gekennzeichnet. Hierbei werden einer Klasse im Quellcode Annotationen wie z.B. DBTable(name) oder DBColumn(name) hinzugefügt. Diese kann die Persistenzschicht heranziehen, um sie zur Laufzeit dynamisch für SQL-Ausdrücke zu verwenden.

Ein Nachteil der Benutzung von Annotationen ist deren statisch im Quellcode eingebettete Bindung. Das können Entwickler allerdings sehr einfach um Konfigurationsdateien erweitern. Seien es Annotationen oder Konfigurationsdateien, in einem von beidem muss die Bindung zwischen Java-Objekten und Datenbankstrukturen hinterlegt sein.

BaseLink unterstützt die Bildung von Annotationen mit der Methode createDAO der Klasse de.uhilger.baselink.Util. Mit ihr können Datenbankzugriffsobjekte (Database Access Objects, DAO) für eine gegebene Datenbanktabelle generiert werden. Auf diese Weise kann die Herstellung der Bindungen komplett automatisert werden. Ändert sich die Datenbanktabelle wird einfach das DAO neu generiert.

Beispiel

Mit BaseLink schrumpft die Methode zur Speicherung eines Objekts in der Datenbank auf den folgenden Code zusammen:

User user = new User();
user.setId("ulrich");
user.setPassword("s3cret");
new PersistenceManager("jdbc/UserDB").insert(user);

Einzige Voraussetzung ist, dass das die Klasse des zu speichernden Objekts, im Beipiel oben die Klasse User, Annotationen für Tabellen- und Feldnamen enthält:

@DBTable(name="app.users")
@DBPrimaryKey({"user_name"})
public class User {
    private String id;
    private String pw;
   
    @DBColumn(name = "user_name")
    public String getId() {
        return id;
    }
[...usw.]   

Man kann die Objekte, die man ohnehin in der Anwendung benötigt, einfach mit den oben gezeigten Auszeichnungen versehen, schon werden sie ohne weiteres in einer Datenbank gespeichert.

Der Weg aus der Datenbank

Ähnlich einfach erfolgt die Entnahme aus der Datenbank. Ein beliebiger SQL-Ausdruck liefert Datensätze aus der Datenbank, die BaseLink in Objekte übersetzt. Wieder ist die Voraussetzung, dass ein passendes Objekt für die betreffenden Sätze existiert, das mittels Annotationen die Bindung an die Datenbank enthält.

GenericRecord userRec = new GenericRecord(User.class);
PersistenceManager pm = new PersistenceManager("jdbc/UserDB");
List users = pm.select("select * from " + userRec.getTableName(), userRec);

Der Einfachheit halber liefert select immer eine Liste, für einzelne Werte kann man anschließend das erste Listenelement lesen, z.B. list.get(0).

Individuelle Fälle

Neben dem CRUD-Muster bietet die Structured Query Language (SQL) unendlich viele Möglichkeiten, individuell auf eine Datenbank zuzugreifen. Für diese Vielzahl ist es wegen der mit der Individualität der Zugriffe abnehmenden Wiederverwendbarkeit nicht sinnvoll zu versuchen, alles in eine Programmbibliothek zu fassen. Ebensowenig Sinn hat mangels Zusatznutzen, SQL durch eine objektorientierte "Zugriffsprache" zu ersetzen. Ein pragmatischer Kompromiß ist, für individuelle Datenbankzugriffe SQL zu verwenden und lediglich einen generischen Weg vorzusehen, wie Objekte in die Datenbank übertragen und aus ihr entnommen werden.

Für individuelle Fälle lohnt es oft nicht, eigens ein Datenzugriffsobjekt zu erstellen. Man liest einen einzelnen Wert aus der Datenbank oder schreibt ihn dort hin, entnimmt Informationen, die sich aus verschiedenen Teilen der Datenbank zusammensetzen. Hier genügt es, wenn die Persistenz-Schicht das Lesen und Schreiben übernimmt, ohne automatisch ein vorgefertigtes Objekt zu verwenden. Für diese Fälle liefert BaseLink die folgenden Methoden:

  • PersistenceManager.execute(String sql)sowie
  • PersistenceManager.executeWithKeys(String sql) zum Schreiben in die Datenbank und 
  • PersistenceManager.select(String sql) zum Lesen.

Die Methoden arbeiten einfach mit einem SQL-String. select gibt ein Objekt der Klasse List zurück, das einfach eine Liste aus Objekten der Klasse Map enthält, eines für jeden Datensatz. Mit List.get(satznummer).get("feldname") kann auf einzelne Elemente der Rückgabedatenmenge zugegriffen werden.

Natürlich deckt auch dieses Angebot nicht alle individuellen Fälle ab, verringert aber den Aufwand für viele weitere Arten von Datenbankzugriffen.

Fazit

Das Thema Persistenz kann sehr weit gefasst werden, wie die ausufernden Implementierungen der Java Persistence API zeigen. Möchte man den Aufwand für den Einsatz der JPA sparen, genügen für einfache Datenbankzugriffe bereits einige wenige Klassen. Eine schlanke und pragmatische Lösung für solche Datenbankzugriffe liefert das Paket BaseLink, mit dem bereits wesentliche Teile einer Persistenzschicht bereitgestellt werden.

Mit einer schmalen Persistenzschicht wie BaseLink kann man sich ohne großen Aufwand auf die Herstellung der Datenbankzugriffe konzentrieren, die von einer Anwendung jeweils individuell benötigt werden. Man spart den stets wiederkehrenden "JDBC Boilerplate Code" und kann sich auf eine gleichbleibend robuste, stabile Datenbankanbindung verlassen.