BaseLink vereinfacht die Nutzung von Datenbanken mit Java.
1. Überblick
Die Java-Plattform enthält mit der Java Database Connectivity (JDBC) eine umfangreiche Sammlung von Werkzeugen zum Umgang mit Datenbanken. Allerdings erfordert die Programmierung immer wieder benötigter Routineaufgaben wie beispielsweise des CRUD-Musters (Create, Read, Update, Delete) vergleichsweise viel Boilerplate Code.
Hier kommt BaseLink ins Spiel und liefert eine Sammlung von Methoden, die viele wiederkehrende Aufgaben bei der Arbeit mit Datenbanken ausführen so dass Entwickler sie nicht immer wieder neu schreiben müssen. Ferner liefert BaseLink eine einfache Form der objektrelationalen Abbildung um Java-Objekte in Datenbank-Tabellen und umgekehrt Tabelleninhalte in Java-Objekte zu überführen.
2. Installation
BaseLink erfordert eine Java-Ablaufumgebung (engl. Java Runtime Environment, JRE). Das JRE ist von AdoptOpenJDK frei erhältlich. Nach Beschaffung des JRE sind zur Installation von BaseLink die folgenden Schritte nötig:
-
Herunterladen des Verteilpakets von BaseLink
-
Entpacken der Datei
baselink.zip
-
Kopieren der Datei
BaseLink.jar
in den Klassenpfad der Anwendung
Der Klassenpfad bei Java-Webanwendungen ist beispielsweise der Ordner WEB-INF/lib
des Webarchivs oder im Falle des Servers Tomcat der Ordner $CATALINA_BASE/lib
.
3. Datenbanktreiber
BaseLink setzt auf die Package javax.sql
auf, in der die Java Database Connectivity (JDBC) gemäß Spezifikation implementiert ist. Voraussetzung zur Nutzung einer Datenbank über JDBC ist die Verfügbarkeit eines JDBC-konformen Datenbanktreibers.
Der Datenbanktreiber liefert eine Implementierung aller Methoden der JDBC speziell für den betreffenden Datenbanktyp. Er wird als eindeutiger Name einer Klasse angegeben wie beispielsweise org.apache.derby.jdbc.ClientDriver
. Der Hersteller einer Datenbank liefert die Angabe des Treibernamens jeweils in der Dokumentation zur Datenbank. Dort ist auch angegeben, wo der Treiber zu finden ist, bei der Datenbank Derby ist das zum Beispiel die Klassenbibliothek derbyclient.jar
. Diese muss sich im Klassenpfad von Programmen befinden, die mit Derby-Datenbanken arbeiten möchten.
Ist der Datenbanktreiber wie erwähnt im Zugriff wird er mit BaseLink entweder über die Methoden setDriverName
oder setDatabase
des PersistenceManagers angegeben oder indirekt über die Angabe einer DataSource mit der Methode setDataSourceName
.
<Resource name="jdbc/NewUserDB"
auth="Container"
type="javax.sql.DataSource"
username="dbadmin"
password="changeit"
driverClassName="org.apache.derby.jdbc.ClientDriver"
url="jdbc:derby://localhost:1527/udb;create=true"
maxActive="60"
maxIdle="30"
maxWait="5000"
/>
Das obige Beispiel einer DataSource enthält die Angabe des Datenbanktreibers im Attribut driverClassName
.
Zur Laufzeit wird beim Aufruf der Methode setDataSourceName
dem PersistenceManager der Ausdruck jdbc/NewUserDB
übergeben. Über das Java Naming and Directory Interface (JNDI) wird daraufhin die Beschreibung der so benannten DataSource lokalisiert, gelesen, in ein Objekt der Klasse DataSource
überführt und als Quellangabe für Datenbankzugriffe verwendet.
Hierbei wird dann der Datenbanktreiber jeweils der Angabe aus der DataSource entnommen.
4. Beispieltabelle
Um die Arbeit mit BaseLink zu veranschaulichen und den Teil auf der Seite der Datenbank zu repräsentieren wird in dieser Dokumentation die folgende Beispieltabelle verwendet:
USERS
CREATE TABLE APP.USERS
(
USER_NAME VARCHAR(250) NOT NULL,
USER_FIRST VARCHAR(250),
USER_LAST VARCHAR(250),
USER_EMAIL VARCHAR(250),
PRIMARY KEY (USER_NAME)
);
Die Beispieltabelle enthält der Einfachheit halber nur Felder des Datentyps VARCHAR. BaseLink unterstützt aber alle Datentypen, die in einer Datenbank vorkommen können, solange ein JDBC-konformer Datenbanktreiber für die Datenbank existiert.
5. Objektrelationale Abbildung
BaseLink verwendet Transferobjekte (engl. Data Transfer Objects, DTO) als Entwurfsmuster zur Abbildung von Objekten in Datenbanken. Orientiert an der Struktur einer Datenbanktabelle werden DTO mit Annotationen versehen, um die Getter und Setter des Objekts zu kennzeichnen, die zum Lesen und Schreiben einzelener Felder eines Datensatzes dienen (siehe auch DTO erzeugen). Folgende Annotation werden von BaseLink hierzu verwendet:
-
DBTable
- Name der Tabelle, an die ein Objekt gebunden ist -
DBColumn
- Name des Feldes, aus dem ein Getter des Objekts seinen Inhalt bezieht -
DBPrimaryKey
- Name des Feldes, über das der primäre Index der Tabelle gebildet wurde. Dies kann auch eine Aufzählung von Feldern sein.
Dabei gelten folgende Vorgaben zwingend:
-
Es muss stets der Getter einer Eigenschaft mit der entsprechenden Annotation zum Tabellenfeld versehen werden.
-
Die Namen der Methoden von Gettern müssen mit
get
, von Settern mitset
beginnen -
Die Namen der Objekteigenschaft sowie ihres Getters und Setters müssen gleich lauten.
-
Beispiel: Eigenschaft
id
, GettergetId
, SettersetId
-
-
Datenbanktabellen müssen einen Primärindex haben
Das folgende Beispiel des Objektes User
zeigt den Einsatz der Annotationen für DTO.
@DBTable(name="app.users")
@DBPrimaryKey({"user_name"})
public class User {
private String id;
private String firstName;
private String lastName;
private String email;
@DBColumn(name = "user_name")
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
// ..weiterer Code hier
}
Anmerkung: Beim relationalen Datenbanksystem Derby werden andere Elemente wie Tabellen oder Indizes einem Schema zugewiesen. Tabellen werden daher mit [Schemaname].[Tabellenname]
bezeichnet. Wenn nichts anderes angegeben wird, unterscheidet Derby standardmäßig zwischen Systemtabellen (Schema SYS
) und Tabellen einer Anwendung (Schema APP
).
5.1. Wandler-Objekte
Ist eine Klasse wie im vorigen Abschnitt beschrieben mittels Annotationen an eine Datenbanktabelle gebunden, können Objekte dieser Klasse als DTO auf die Datenbank abgebildet werden. BaseLink kann für solche DTO mit Hilfe der Klasse GenericRecord
das SQL für die Datenbankoperationen dynamisch erzeugen.
Stattdessen kann für die Klasse eines DTO auch zuvor ein Wandler-Objekt gebildet werden:
Record userWandler = new GenericRecord(User.class);
Mit dieser Vorgehensweise muss BaseLink nicht immer erst implizit einen Wandler für ein DTO bilden und später wieder freigeben. Ein so erzeugtes Wandler-Objekt ist für alle DTOs einer Klasse wiederverwendbar. Im Falle häufiger Datenbankoperationen kann damit effizienter gearbeitet werden.
6. Datenbankoperationen
Dieser Abschnitt beschreibt die von BaseLink direkt unterstützten Datenbankoperationen. Im Bereich Hilfmittel und Tipps werden darüber hinaus zusätzliche Vorgehensweisen im Zusammenhang mit der Nutzung von BaseLink beschrieben.
6.1. PersistenceManager
Der PersistenceManager
ist die zentrale Klasse von BaseLink. Sie stellt Verbindungen zu einer Datenbank her und hält Methoden zur Arbeit mit der Datenbank bereit. Eine Instanz des PersistenceManagers repräsentiert eine Datenbank.
Instanzen des PersistenceManagers verbrauchen ansonsten keine Ressourcen, sie können bedenkenlos nach Bedarf instanziiert werden, die Garbage Collection der Java Virtual Machine gibt sie automatisch wieder restlos frei. Lediglich im Falle der Nutzung von Transaktionen sollten die Verbindungen zur Datenbank stets geschlossen werden bevor ein PersistenceManager der Garbage Collection überlassen wird.
6.2. Speichern, Ändern, Löschen
Für das Speichern, Ändern und Löschen von Daten liefert BaseLink die Standardmethoden insert
, update
und delete
. Sie sind jeweils gleich aufgebaut und erfordern im einfachsten Fall als Parameter das Objekt, das gespeichert, geändert oder gelöscht werden soll.
User user = new User();
user.setId("testfred");
user.setFirstName("Fred");
user.setLastName("Test");
user.setEMail("fred.test@example.com");
PersistenceManager db = new PersistenceManager();
db.setDataSourceName("jdbc/UserDatabase");
db.insert(user);
Wenn viele Datenbankoperationen ausgeführt werden sollen kann es effizienter sein, einen Wandler für eine Klasse bereitzuhalten, dann muss er nicht bei jeder Operation gebildet und wieder freigegeben werden (vgl. Abschnitt Wandler-Objekte). BaseLink hält für diesen Zweck eine Variante bereit, die als zusätzlichen Parameter einen Wandler akzeptiert.
private PersistenceManager db;
private Record userWandler;
public void initDb() {
db = new PersistenceManager();
db.setDataSourceName("jdbc/UserDatabase");
userWandler = new GenericRecord(User.class);
}
public User create(User user) {
return db.insert(user, userWandler);
}
Diese Vorgehensweise funktioniert so auch für die Methoden update
und delete
. Der Rückgabewert ist stets entweder das übergebene Objekt oder null
, falls die Operation nicht durchgeführt werden konnte (vgl. Fehlerbehandlung). Für eine Rückgabe erzeugter Schlüssel siehe Abschnitt Schlüsselrückgabe.
6.3. Daten lesen
Die Methode select
des PersistenceManagers dient in verschiedenen Varianten zum Lesen aus der Datenbank. Das geschieht stets mit Hilfe von Ausdrücken der Structured Query Language (SQL). Hierbei unterstützt BaseLink hauptsächlich an zwei Stellen:
-
Datensätze aus der Datenbank werden mit Hilfe der Klasse
GenericRecord
in Objekte überführt (objektrelationale Abbildung) -
Parameter in Aufrufen der Methode
select
werden dynamisch in veränderliche Teile eines SQL-Ausdrucks übertragen
Für das Gelingen von Punkt 1 oben muss die SQL-Abfrage die Felder ausgeben, die vom Zielobjekt erwartet werden. Die Parameter an den Aufruf von select
müssen in der Reihenfolge übergeben werden, in der sie im SQL-Ausdruck enthalten sind, damit Punkt 2 funktioniert.
Die obigen Funktionen werden in folgendem Beispiel veranschaulicht:
getUser
im folgenden Beispielcode zeigt das Lesen aus der Datenbankprivate PersistenceManager db;
private Record userWandler;
public void initDb() {
db = new PersistenceManager();
db.setDataSourceName("jdbc/UserDatabase");
userWandler = new GenericRecord(User.class);
}
public List getUser(String name1, String name2) {
String sql =
"select user_name,user_first,user_last,user_email" +
" from app.users" +
" where user_first = ? " +
" or user_first = ?";
return db.select(sql, userWandler, Record.WITHOUT_BLOBS, name1, name2);
}
Wird die Methode getUser
in obigem Beispielcode mit zwei Vornamen gerufen wie z.B. in
List nutzerliste = getUser("Maria", "Peter");
gibt sie ein Objekt der Klasse List
zurück. Diese Liste enthält alle Benutzer, deren Vorname Maria oder Peter lautet, als Objekte der Klasse User
.
Ein Beispiel, das freilich so in der Praxis vermutlich nicht vorkommt. Es zeigt aber, dass die Methode select
mit einer unterschiedlichen Anzahl von Parametern gerufen werden kann. Die Parameter richten sich nach den veränderlichen Elementen des SQL-Ausdrucks. Im Beispielcode werden die mit Fragezeichen markierten Platzhalter der SQL-Abfrage where user_first = ? or user_first = ?
mit den Inhalten der Parameter name1
und name2
befüllt.
Damit SQL-Ausdrücke in der Praxis nicht wie im Beispiel hart codiert im Quellcode enthalten sein müssen, empfiehlt sich eine Vorgehensweise, wie sie der Abschnitt SQL-Konfiguration beschreibt.
Der Parameter Record.WITHOUT_BLOBS
regelt, ob eine Abfrage den Inhalt von Binary Large Objects (BLOBs) ausgibt oder nicht. Er kann verwendet werden, um die Ausgabe zu beschleunigen, wenn es absehbar ist, dass die Abfrage keine BLOBs betrifft oder deren Inhalt nicht benötigt wird.
6.3.1. Lesen ohne objektrelationale Abbildung
Varianten der Methode select
des PersistenceManagers, die ohne GenericRecord
auskommen, wandeln Datenbankinhalte nicht in Objekte sondern liefern eine statische Liste aus Listen mit Strings. Mit diesen Methoden lassen sich dynamisch alle Ergebnisse eines beliebigen SQL-Ausdrucks in Objekte abbilden, ohne Code zu schreiben, der besonders auf die jeweilige SQL-Abfrage bezogen sein muss.
Jede Liste aus Strings repräsentiert dabei die Feldinhalte eines Datensatzes. Mit der zuvor gezeigten Beispieltabelle würde für die Abfrage
List nutzerliste = db.select("select * from app.users", Record.WITHOUT_BLOBS);
im Rückgabewert nutzerliste
ein Objekt der Klasse List
wie folgt entstehen:
List { List { "user_name", "user_first", "user_last", "user_email" }, List { "testfred", "Fred", "Test", "fred.test@example.com" }, List { "muellermaria", "Maria", "Mueller", "maria.mueller@example.com" }, List { "panpeter", "Peter", "Pan", "peter.pan@example.com" } }
Inhalte des Rückgabewerts nutzerliste
können z.B. mit List.get(Datensatznr).get(Feldnr)
entnommen werden. Der Ausdruck System.out.println(nutzerliste.get(2).get(3));
würde also maria.mueller@example.com
ausgeben.
6.3.2. Feldname anstelle von Feldnummer
In einer Abwandlung der Vorgehensweise aus dem vorigen Abschnitt liefert ein Aufruf von
List nutzerliste = db.select("select * from app.users");
eine Liste aus Objekten der Klasse Map
, die jeweils einen Datensatz enthalten und Feldinhalte über die Namen der Felder zugänglich machen. Mit dem obigen Beispielaufruf würde etwa System.out.println(nutzerliste.get(2).get("user_email"));
der Ausgabe aus dem vorigen Abschnitt entsprechen, aber mit der Angabe des Feldnamens funktionieren.
6.3.3. Nutzung von ResultSets
Gelesene Inhalte werden mit BaseLink stets als statische Datenobjekte ausgegeben. Datenbankcursor, mit denen dynamisch durch Teilmengen von Datenbankinhalten navigiert werden kann, wie sie von JDBC-ResultSets geliefert werden, sind nicht Teil des Leistungsumfangs von BaseLink, sie können mit den Klassen der Package javax.sql
direkt genutzt werden.
6.3.4. Zusammenfassung
Wird die Methode select
des PersistenceManagers von BaseLink mit einem Wandler-Objekt verwendet, bildet BaseLink aus den Datenbankinhalten die gewünschten Objekte, vorausgesetzt, die SQL-Abfrage liefert die Felder, die das Zielobjekt erwartet.
Die Methode select
kann ohne individuelle Gestaltung des Codes eine unterschiedliche Anzahl Parameter annehmen und auf diese Weise mit derselben Methode beliebige generische SQL-Abfragen dynamisch verarbeiten. SQL-Abfragen mit variablen Anteilen wie z.B. select * from app.users where user_name = ?
oder select * from app.users where user_first = ? or user_last like ?
können mit derselben Methode ohne individuelle Anpassungen des Code gleichermaßen verarbeitet werden.
Die Ausgabe von Datenbankinhalten in ihrer generischen Form ohne objektrelationale Abbildung lässt eine nahezu unbegrenzte Anzahl von Abfragemöglichkeiten mit ein und derselben Methode zu. Darüber hinaus können die Methoden toList
von BaseLink für weitere individuelle Anforderungen verwendet werden.
6.4. Transaktionen
Mit Transaktionen lassen sich mehrere Datenbankoperationen zu Funktionsblöcken zusammenfassen die entweder ganz durchgeführt oder ganz rückgängig gemacht werden. Auf diese Weise wird sichergestellt, dass voneinander abhängende Operationen im Falle von Fehlern keine inkonsistenten Zustände in der Datenbank hinterlassen.
BaseLink stellt Transaktionen über die Methoden getConnection
, startTransaction
, commit
, rollback
sowie closeConnectionFinally
des PersistenceManagers bereit. Deren Nutzung ist im folgenden Code-Beispiel dargestellt:
public void dbTransaction(User user) {
PersistenceManager db = new PersistenceManager();
db.setDataSourceName("jdbc/MyFancyDatabase");
Connection c = db.getConnection();
c.startTransaction();
try {
db.insert(c, user);
// ..hier weitere Datenbankoperationen..
c.commit();
} catch(Exception ex) {
c.rollback();
} finally {
db.closeConnectionFinally(c);
}
}
Nach dem Aufruf von startTransaction
können beliebige Datenbankoperationen stattfinden, die einstweilen in der Datenbank noch nicht wirksam werden. Passiert bei einer Operation ein Fehler, werden mit dem Aufruf von rollback
im catch
-Zweig alle Operationen seit startTransaction
rückgängig gemacht. Nur, wenn alle Operationen ohne Fehler durchgeführt werden, sorgt der Aufruf von commit
zum Schluß dafür, dass alle Operationen in der Datenbank dauerhaft wirksam werden.
Die relevanten Methoden von BaseLink wie z.B. insert
, update
, select
usw. sehen zur Arbeit mit Transaktionen nach obigem Muster jeweils Varianten vor, die ein Connection-Objekt als Parameter akzeptieren.
Wichtig: Bei der Arbeit mit Transaktionen muss wie im obigen Beispiel sichergestellt werden, dass eine Datenbankverbindung stets geschlossen wird weil andernfalls Speicherlecks entstehen können (siehe Ressourcenverwaltung).
6.5. Schlüsselrückgabe
Viele Datenbanksysteme bieten die Möglichkeit, eindeutige Schlüssel für Tabellen zu verwalten. Wenn in solchen Tabellen ein neuer Datensatz angelegt wird, sorgt das Datenbanksystem dafür, dass ihm ein eindeutiger Schlüssel zugewiesen wird. Das folgende SQL-Beispiel aus der Derby-Dokumentation zeigt diesen Fall
create table greetings
(i int generated always as identity, ch char(50));
insert into greetings values (DEFAULT, 'hello');
insert into greetings(ch) values ('bonjour');
Mit BaseLink lässt sich ein so erzeugter eindeutiger Schlüssel mit der Methode executeWithKeys
ermitteln. Ein Aufruf wie beispielsweise
PersistenceManager db = new PersistenceManager();
db.setDataSourceName("jdbc/MyFancyDatabase");
List keys = db.executeWithKeys("insert into app.greetings(ch) values('Guten Tag')");
liefert den erzeugten Schlüssel in keys
zurück. Hier noch beschreiben, wie keys
aufgebaut ist und gelesen wird.
Eine Alternative zur Nutzung datenbankspezifischer Mechanismen zur Schlüsselerzeugung zeigt der Abschnitt Eigene Schlüssel.
6.6. SQL-Skripte
Mit PersistenceManager.executeScript
liefert BaseLink eine Methode zur Ausführung von SQL-Skripten. Die Methode lautet wie folgt
PersistenceManager db = new PersistenceManager();
db.setDataSourceName("jdbc/MyFancyDatabase");
int[] ergebnis = db.executeScript(getSqlSkript());
Die Methode executeScript
erwartet als Parameter einen String mit dem Skriptinhalt. Im Beispiel oben wird angenommen, dass eine separate Methode getSqlSkript
das Skript liefert. Einzelne SQL-Kommandos müssen darin zeilenweise enthalten sein und jeweils mit Semikolon enden.
Der Rückgabewert ist ein Array mit ganzzahligen Werten, die für jede Zeile des Skripts entweder Statement.SUCCESS_NO_INFO oder die Anzahl der von ihr betroffenen Datensätze angeben. Sind alle Rückgabewerte ungleich Statement.EXECUTE_FAILED, war die Ausführung des gesamten Skriptes erfolgreich.
Im Abschnitt Datenbankstruktur erzeugen wird gezeigt, wie mit executeScript
beispielsweise Skripte zur Anlage einer ganzen Datenbankstruktur ausgeführt werden können.
6.7. Fehlerbehandlung
Alle Methoden von BaseLink fangen Fehler ab und vermerken die Fälle im Protokoll. Die Klasse de.uhilger.baselink.PersistenceManager
legt dafür einen entsprechenden Logger an. Über die Logging-Konfiguration eigener Apps kann gesteuert werden, ob Fehler protokolliert werden und in welchem Umfang.
Es werden also beim Aufruf von BaseLink keine Exceptions geworfen. Ein Fehler lässt sich nur am Rückgabewert einzelner Methoden ablesen.
7. Hilfsmittel und Tipps
Neben den von BaseLink unterstützten Datenbankoperationen gibt es eine Reihe von Anwendungsfällen, die im Zusammenspiel mit dem Einsatz von BaseLink näher betrachtet werden sollten. Diese sind in diesem Abschnitt beschrieben.
7.1. DTO erzeugen
Zur Nutzung von BaseLink werden Datenbankstrukturen mit Annotationen an Java-Klassen gebunden (vgl. objektrelationale Abbildung). BaseLink unterstützt diese Aufgabe mit der Methode Util.generateDTO
. Mit Übergabe eines Tabellennamens wie in folgendem Beispiel erzeugt die Methode den Quellcode einer kompletten Klasse mit Annotationen passend zur Struktur der Datenbanktabelle.
PersistenceManager db = new PersistenceManager();
db.setDataSourceName("jdbc/MyFancyDatabase");
System.out.println(new Util.generateDTO(db, "APP", "USER"));
Die Ausgabe des Quellcodes auf der Konsole kann direkt als neue Klasse gespeichert und kompiliert oder zuvor nach Wunsch von Hand ergänzt und geändert werden. Die Methode generateDAO
erspart dabei die Tipparbeit bei der Entwicklung erheblich.
7.2. SQL-Konfiguration
Bei der Arbeit mit relationalen Datenbanken ist die Structure Query Language (SQL) ein zentrales Hilfsmittel. SQL wird wie eine Programmiersprache verwendet, beinhaltet aber veränderliche Teile der Datenbankstruktur wie z.B. Tabellen- oder Feldnamen. Bei Änderungen der Datenbankstruktur müssen SQL-Ausdrücke angepasst werden. Um Eingriffe in den Quellcode von Anwendungen zu vermeiden sollte SQL stets außerhalb des Quellcodes angelegt sein.
Die Java-Plattform liefert mit der Klasse java.util.Properties
eine Möglichkeit, veränderliche Programmbestandteile wie z.B. Konfigurationseinstellungen außerhalb des Quellcodes zu halten und eignet sich damit für die Verwendung von SQL.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="getUserNameList">
select user_name from app.users
</entry>
<entry key="getUserRoles">
select role_name from app.user_roles where user_name = ? order by role_name
</entry>
<entry key="getUser">
select * from app.users where user_name = ?
</entry>
</properties>
Werden SQL-Kommandos in eine XML-Struktur notiert wie im obigen Beispiel, können sie zur Laufzeit gelesen werden. Die Klasse Properties
liefert eine bequeme Methode zum Lesen von Eigenschaften aus einer wie im Beispiel strukturierten XML-Datei.
public Properties sqlLesen(String dateiname) {
File sqlFile = new File(dateiname);
Properties sql = new Properties();
sql.loadFromXML(new FileInputStream(sqlFile));
return sql;
}
Ein auf diese Weise gelesenes Properties
-Objekt lässt sich wie folgt in Methoden von BaseLink einbauen.
private PersistenceManager db;
private Record userWandler;
private Properties sql;
public static final String SQL_GET_USER = "getUser";
public void initDb() {
db = new PersistenceManager();
db.setDataSourceName("jdbc/UserDatabase");
userWandler = new GenericRecord(User.class);
sql = sqlLesen("/pfad/zu/sql.properties");
}
public User getUser(String userId) {
String sqlCmd = sql.getProperty(SQL_GET_USER);
return db.select(sqlCmd, userWandler, Record.WITHOUT_BLOBS, userId);
}
In der Methode getUser
des obigen Beispiels wird ein SQL-Kommando aus einem zuvor in der Methode initDb
gelesenen Properties
-Objekt entnommen und der Methode select
von BaseLink übergeben. Ändert sich später einmal ein Teil der Datenbankstruktur, kann ohne Eingriff in den Quellcode das SQL in der Konfigurationsdatei angepasst werden.
7.3. Prüfung Datenbankstruktur
Datenbanksysteme unterscheiden sich in der Art, ob und wie mit ihnen der Zustand der Struktur einer Datenbank geprüft werden kann. Bei Derby wird beispielsweise mit einem URL wie jdbc:derby://localhost:1527/udb;create=true
eine Datenbank angelegt, falls sie noch nicht existiert. Sie besitzt dann aber noch keine Datenbankstruktur im Innern.
In Kombination mit einer SQL-Konfiguration aus dem vorigen Abschnitt kann mit BaseLink in nur wenigen Zeilen Code festgestellt werden, ob eine Datenbank bereits die gewünschte Datenstruktur beinhaltet. In besagter SQL-Konfiguration wird dafür zunächst die folgende SQL-Abfrage hinterlegt.
select s.schemaname, t.tablename
from SYS.SYSSCHEMAS as s,
SYS.SYSTABLES as t
where s.schemaid = t.schemaid
and s.schemaname = ?
and t.tablename = ?
Damit kann für eine Tabelle der eigenen App geprüft werden, ob sie bereits angelegt wurde wie im folgenden Code gezeigt.
public boolean dbVorhanden(PersistenceManager db, String sql) {
boolean istVorhanden = false;
List<List<String>> list = db.select(sql, Record.WITHOUT_BLOBS, "APP", "USERS");
if(list != null && list.size() > 1) {
istVorhanden = true;
logger.fine("Datenbank ist vorhanden");
}
return istVorhanden;
}
Nötigenfalls können die Angaben APP
und USERS
noch über eine Environment-Variable parametriert werden, um die Methode veränderlich zu halten.
7.4. Datenbankstruktur anlegen
Setzt eine App auf eine bestimmte Datenbankstruktur auf, ist es im Zuge der Installation dieser App mitunter nötig, neben den sonstigen Bestandteilen der App auch die entsprechende Datenbankstruktur initial anzulegen. Mit den Hilfsmitteln der SQL-Konfiguration und der Prüfung der Datenstruktur aus den vorangegangenen Abschnitten kann dies mit wenigen zusätzlichen Handgriffen erledigt werden.
Die Vorgehensweise dazu sieht die Hinterlegung eines Skripts zur Erzeugung der Datenbankstrukur vor. Viele Datenbanksysteme beinhalten Werkzeuge, mit denen die komplette Struktur einer Datenbank als SQL-Skript erzeugt werden kann. Bei Derby ist dies z.B. dblook
. Oft entsteht aber auch das entsprechende Skript nach und nach aus den SQL-Kommandos, die ein Entwickler zur Erstellung einzelner Tabellen nach und nach herstellt.
Davon ausgehend, dass ein Skript zur Erstellung einer Datenbank in der Datei create_database.sql
vorliegt, ist mit dem folgenden Beispielcode die Datenbankstruktur im Handumdrehen angelegt.
public void initDb() {
PersistenceManager db = new PersistenceManager();
db.setDataSourceName("jdbc/UserDatabase");
if(!dbVorhanden(db, sql.getProperty(SQL_DB_VORHANDEN))) {
int[] ergebnis = db.executeScript(getSqlSkript());
}
}
private String getSqlSkript() throws Exception {
File skript = new File("/pfad/zu/create_database.sql");
return fromStream(new FileInputStream(skript));
}
private String fromStream(InputStream in) throws IOException
{
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder out = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
out.append(line);
}
return out.toString();
}
Im obigen Beispiel wird zunächt geprüft, ob die Datenbankstruktur bereits besteht, indem mit dem Aufruf von dbVorhanden
nachgesehen wird, ob die Tabelle app.user
vorhanden ist. Wenn die Tabelle fehlt, wird das Skript create_database.sql
aus einer Datei gelesen und ausgeführt.
7.5. Methoden zum Schließen
BaseLink implementiert für eigene Zwecke Methoden zum Schließen von Verbindungen, ResultSets und Statements. In Fällen einer individuellen Nutzung der Package javax.sql
können diese Methoden verwendet und so Aufwand für die wiederholte eigene Implementierung gespart werden. Die Methoden sind
-
PersistenceManager.closeResultSetFinally
-
PersistenceManager.closeStatementFinally
-
PersistenceManager.closeConnectionFinally
7.6. toList
Eine weitere Methode zur direkten Verwendung von BaseLink-Elementen ist die Methode toList
. Sie verwandelt ein beliebiges ResultSet in eine statische Liste wie es für das Lesen von Daten ohne objektrelationale Abbildung von BaseLink implementiert ist.
Wann immer also eine individuelle Nutzung der Package javax.sql
mit der Rückgabe eines ResultSets endet, kann die Methode toList
von BaseLink angewendet werden. Hierbei bietet sich auch die Kombination der Verwendung von toList
und den Methoden zum Schließen an.
7.7. Eigene Schlüssel
Im Abschnitt Schlüsselrückgabe ist beschrieben, wie Datenbankfunktionen zur Erzeugung eindeutiger Schlüssel mit BaseLink eingebunden werden können. Ein Nachteil solcher Funktionen ist deren in der Regel nicht standardkonforme SQL-Syntax. Wenn das Datenbanksystem gewechselt werden soll, muss eine App angepasst werden, wenn sie eine solche Funktion nutzt.
Eine Alternative ist es, einen datenbankunabhängigen Mechanismus zu verwenden wie er beispielsweise im Projekt radiozentrale zur Anwendung kommt. Die nachfolgenden Codebeispiele sind diesem Projekt entlehnt.
Sie könnten so auch Teil von BaseLink sein, Datenbanken müssten dann aber stets mit den angegebenen Tabellen ausgestattet sein. Aus diesem Grund soll es an dieser Stelle genügen, den Mechanismus als Kopiervorlage für andere Projekte vorzustellen.
7.7.1. Tabelle zur Verwaltung eindeutiger Schlüssel
Zur Verwaltung von Schlüsseln in einer Datenbank wird die folgende Tabelle erzeugt:
keytable
zur Vergabe eindeutiger Schlüsselcreate table app.keytable (
key_name varchar(80) not null,
key_next int,
primary key (key_name)
);
create index key_name_next ON app.keytable (key_name,key_next);
Jeder Schlüssel wird in der Tabelle keytable
mit einem eindeutigen Namen versehen.
7.7.2. SQL zur Schlüsselverwaltung
Mit der folgenden SQL-Abfrage kann geprüft werden, wie der nächste Schlüssel lautet.
select key_next
from app.keytable
where key_name = ?
Ist auf diese Weise der nächste Schlüssel ermittelt, kann das folgende SQL-Kommando verwendet werden, um die Schlüsseltabelle mit den nächsten Schlussel zu aktualisieren (inkrement).
update app.keytable
set key_next = ?
where key_name = ?
and key_next = ?
7.7.3. Anwendungsbeispiel mit eindeutigem Schlüssel
Wir stellen uns nun vor, dass in unserer App eine Tabelle erstellt wird, die einen eindeutigen Schlüssel enthält wie etwa die Tabelle sender
für die Speicherung von Radiosendern aus der App radiozentrale.
sender
mit dem eindeutigen Schlüssel im Feld sender_id
create table app.sender
(
sender_id int not null,
sender_name varchar(1024) not null,
sender_url varchar(1024),
sender_logo varchar(1024),
primary key (sender_id)
);
create index sname on app.sender (sender_name);
insert into app.sender (sender_id, sender_name, sender_url, sender_logo) values (1, 'hr info', 'http://hr-hrinfo-live.cast.addradio.de/hr/hrinfo/live/mp3/128/stream.mp3', '../bilder/hr-info.png');
Für die Verwaltung der sender_id
wird in der Tabelle keytable
ein Eintrag wie folgt angelegt:
keytable
zur Verwaltung der sender_id
insert into app.keytable (key_name, key_next) values ('sender_id',2);
Nun kann mit folgender Methode ein neuer Sender angelegt werden.
private PersistenceManager db;
private Record senderWandler;
public void initDb() {
db = new PersistenceManager();
db.setDataSourceName("jdbc/RadioDatabase");
senderWandler = new GenericRecord(Sender.class);
}
public Sender neuerSender(Sender sender) {
Sender neuerSender = null;
int nextKey = getNextId(db, "sender_id");
if(nextKey > -1) {
sender.setId(nextKey);
Object o = db.insert(sender, senderWandler);
if(o instanceof Sender) {
neuerSender = (Sender) o;
logger.fine("Sender erstellt: " + sender.getId() + " " + sender.getName());
} else {
logger.info("Sender konnte nicht erstellt werden: " + sender.getName());
}
} else {
logger.info("Sender konnte nicht erstellt werden, nextKey ist -1");
}
return neuerSender;
}
In der obigen Methode wird mit getNextId
eine neue Sender-ID ermittelt. Die neue Sender-ID wird im Objekt mit dem neuen Sender eingetragen. Dann wird das neue Sender-Objekt mitsamt der neuen ID gespeichert.
War diese Operation erfolgreich, wird das Sender-Objekt mit der neu erzeugten ID als Rückgabewert der aufrufenden Methode zurückgemeldet. Im Falle eines Fehlers bleibt das Rückgabeobjekt null
.
7.7.4. Methode getNextId
Mit der Methode getNextId
wird der Wert für einen Schlüssel ermittelt, der als nächstes für einen Tabelleneintrag mit eindeutigem Schlüsssel vergeben werden muss.
public int getNextId(PersistenceManager db, String key) {
int nextKey = -1;
List list = db.select(getSql(SQL_GET_NEXT_KEY), Record.WITHOUT_BLOBS, key);
if(list != null && list.size() > 1) {
nextKey = Integer.parseInt(list.get(1).get(0)); // erster Datensatz ist Ueberschrift
if(nextKey > -1) {
int numRows = db.execute(getSql(SQL_INCREMENT_KEY), nextKey+1, key, nextKey);
if(numRows < 1) {
nextKey = -1;
}
}
}
return nextKey;
}
Die Methode getNextKey
verwendet den Mechanismus zur SQL-Konfiguration um mit getSql
die SQL-Kommandos zur Ermittlung des nächsten Schlüssels sowie zu dessen Inkrement abzurufen. Mit diesen SQL-Kommandos wird der nächste Schlüssel ermittelt und anschließend hochgezählt.
7.7.5. Schlussbemerkungen
Anzumerken ist noch, dass die Methode getNextId
fehlschlagen kann, wenn zwischen der Ermittlung des nächsten Schlüssels und dem Inkrement des Schlüssels ein konkurrierender Zugriff dem Inkrement zuvor kommt. In diesem Fall würde das verwendete SQL-Kommando keinen Eintrag zum Hochzählen finden, weil der Wert des nächsten Schlüssels dann schon vom konkurrierenden Prozess hochgezählt wurde. Das muss so sein, andernfalls könnte es zu Doppelungen des eindeutigen Schlüssels kommen. Die Methode zur Anlage eines neuen Senders berücksichtigt dies, indem der neue Sender nur angelegt wird, wenn der neue Schlüssel größer als -1 ist.
Ebenfalls nicht unerwähnt bleiben sollte der Fall, dass das Anlegen des Senders fehlschlagen kann. Im Beispiel würde in diesem Fall zwar nichts Schlimmes passieren, es könnte aber eine Lücke in der Reihenfolge der vergebenen IDs entstehen. Es gäbe dann IDs, die keinem Tabelleneintrag zugewiesen sind. Dies lässt sich vermeiden, wenn der gesamte Ablauf in einer Transaktion ausgeführt wird, wurde aber der Einfachheit halber in diesem Beispiel weggelassen.
Alle Elemente der obigen Darstellung können als generische Vorlage für eigene Zwecke dienen. Nur die Sendertabelle und das dafür benötigte Transferobjekt Sender
müssen an eigene Belange angepasst werden.
Die Verwendung eines solchen Mechanismus lässt die betreffende App unabhängig von proprietären Mechanismen eines bestimmten Datenbanksystems bleiben. Allerdings muss die App damit auch selbst sicherstellen, dass zur Vergabe von eindeutigen Schlüsseln nur der eigene Mechanismus verwendet wird, da andernfalls Schlüssel in den Tabellen gespeichert werden könnten, die nicht der gewünschen Form entsprechen.
8. Verbindung zur Datenbank
Um Inhalte in einer Datenbank zu verwenden muss eine Verbindung zur Datenbank aufgebaut werden. Das Öffnen und Schließen von Datenbankverbindungen geht mit Latenzen einher und offene Verbindungen belegen Systemressourcen.
Viele Web- und Applikationsserver wie zum Beispiel Tomcat halten einen Connection Pool bereit, der selbsttätig dafür sorgt, Datenbankverbindungen zu öffnen und zu schließen. Der Connection Pool verwendet Verbindungen mehrfach und sorgt für einen Ausgleich zwischen Ressourcenverbrauch einerseits sowie Latenzen beim Verbindungsauf- und -abbau andererseits.
Der bei Tomcat eingesetzte Connection Pool DBCP ist auch separat erhältlich. Zudem gibt es eine Reihe von weiteren Klassenbibliotheken, die einen Connection Pool implementieren, beispielsweise C3P0, UCP, BoneCP, H2 und so weiter.
BaseLink stellt Verbindungen zur Datenbank nach Bedarf her und schließt sie selbsttätig (Ausnahme: Transaktionen, siehe dort). Abhängig davon, ob die betreffende Datenbank über eine DataSource eingebunden ist oder direkt über einen JDBC-URL wird die Verbindung dabei tatsächlich geöffnet und geschlossen (URL) oder das Öffnen und Schließen vom Connection Pool verwaltet (DataSource), wenn ein solcher in Benutzung ist.
9. Ressourcenverwaltung
Die Java Virtual Machine (JVM) beinhaltet eine automatische Speicherverwaltung, der Mechanismus der Garbage Collection (GC) sorgt automatisch dafür, dass nicht mehr benötigte Ressourcen wieder freigegeben werden. In der Regel funktioniert das auch, wenn Referenzen auf Objekte nicht explizit von der App aufgelöst werden, die sie instanziiert hat.
Im Falle der JDBC sind Objekte der Klasse Connection potentielle Kandidaten für Ressourcenlecks, wenn die Garbage Collection nicht erkennen kann, dass eine Datenbankverbindung nicht mehr benötigt wird.
BaseLink sorgt dafür, Datenbankverbindungen selbst nach Bedarf zu öffnen und zu schließen womit eine ordentliche Ressourcenverwaltung sichergestellt ist. Einzige Ausnahme sind Fälle, in denen zum Zwecke der Verwendung von Transaktionen eine Datenbankverbindung an die App herausgereicht wird. Hier sollte stets die App darauf achten, eine Verbindung zu schließen, wenn sie nicht mehr benötigt wird.
10. Lizenz
BaseLink ist freie Software und wird unter den Bedingungen der GNU General Public License zur Verfügung gestellt.