Für das Fach Software-Engineering sollten wir so etwas Ähnliches wie eine rudimentäre Fahrzeugverwaltung implementieren. Als Datenbanksystem konnten wir uns für Firebird oder HSQLDB entscheiden, die Entscheidung fiel für HSQLDB.
HSQLDB ist eine besondere Datenbank, denn sie kann als Logdatei-Tabellen-Datenbank betrieben werden, wie es hier auch realisiert wurde. Das heißt die Änderungen werden in einer großen SQL-Log-Datei gespeichert – beim Start wird diese Datei geladen (und damit vollständig im Arbeitsspeicher gehalten) und beim Beenden wird der aktuelle Stand der Datenbank als neue SQL-Log-Datei auf die Festplatte geschrieben.
Das bedeutet in dem Sinne dass man keine SQL-Dateien ausführen muss wie bei anderen Datenbanken; man schreibt die veränderten SQL-Befehle mit dem Texteditor in die Script-Datei, startet die eigene Anwendung neu und schon sind die geänderten Datensätze vorhanden.
Außerdem wichtig ist auch dass sie nicht nur als Netzwerk-Datenbankserver sondern auch als Embedded-Datenbank betrieben werden kann, also als eingebettete Datenbank für Einzelbenutzer-Anwendungen.
Die historische Entwicklung der Projekte dafür ist interessant, denn es sind letztendlich drei Eclipse-Projekte geworden.
1. SE_Aufgabe_8 (alt): Direktzugriff auf die Datenbank über den JDBC-Treiber. Das Paket hsqldb.jar enthält nicht nur die Datenbank, sondern auch den JDBC-Treiber. Für die Entwicklung wurde eine Hilfsklasse DataBaseHelper.java benutzt.
2. SE_Aufgabe_8 (neu): Zugriff auf die Datenbank über den objektrelationalen Mapper EclipseLink, sowie Nutzung des Model-View-Controller und Observer-Patterns.
3. SE_vert_Prototyp: Verbesserte Nutzung von EclipseLink und erstmals richtige Verwendung des Observer-Patterns von Java.
EclipseLink ist ein sogenannter objektrelationaler Mapper, über diesen werden Datenbanktabellen auf Klassen abgebildet. Dazu kann man über eine Datenbankverbindung aus Eclipse heraus die nötigen Entity-Klassen generieren lassen – dafür sollte man Foreign-Keys in die Datenbank gesetzt haben, weil nur dann erweiterte Getter generiert werden.
In Bezug auf HSQLDB und deren Verwendung als Embedded-Datenbank muss man bei der Entwicklung mit EclipseLink etwas beachten, denn für die Generierung der Entity-Klassen muss eine Datenbankverbindung im DS-Explorer von Eclipse erstellt werden – es ist aber für eine Embedded-Datenbank immer nur eine einzelne Datenbankverbindung gleichzeitig zulässig. Wenn man das eigene Programm aus Eclipse heraus startet ist das bereits die zweite Datenbankverbindung.
Die Datenbankverbindung im DS-Explorer von Eclipse zu schließen und das eigene Programm danach starten zu lassen reicht übrigens nicht aus – sobald die Datenbankverbindung im DS-Explorer von Eclipse einmal geöffnet wurde muss Eclipse beendet, die Lock-Datei der Datenbank gelöscht und Eclipse neu gestartet werden um das eigene Programm aus Eclipse (welches die gleiche Datenbank benutzt) starten zu können.
HSQLDB erstellt im Embedded-Modus nämlich eine Lock-Datei (und löscht diese bei Beendigung wieder) um sicherzustellen dass nur eine einzige Verbindung zur Datenbank erfolgt. Eine weitere Besonderheit ist dass HSQLDB automatisch eine neue Datenbank erstellt wenn es die Angegebene nicht finden konnte. Als Konsequenz daraus funktioniert der “Test Connection”-Button zur Datenbank (in Eclipse) logischerweise immer.
EclipseLink basiert auf JPA und verwendet einen SQL-ähnlichen Dialekt für SELECT-Befehle, JPQL. Zu dem Zweck sollte man sich auch bewusst machen dass die SELECT-Befehle nicht mehr direkt auf die Datenbanktabellen sondern auf die generierten Entity-Klassen zugreifen.
Erstmalig auf meiner Seite wird das Observer-Pattern in Java wie vorgesehen genutzt. Im Gegensatz zu meinen bisherigen Implementierungen sollten die ausgelesenen Objekte nämlich –mit- dem Observer-Pattern ausgeliefert werden.
Die meisten kennen das so dass man eine Methode in einer Klasse aufruft um Daten auszulesen und diese liefert sie direkt zurück. Mit der korrekten Nutzung des Observer-Patterns in Java ist das aber anders.
Nachfolgend einmal ein Beispiel, in der MainFrame-Klasse erfolgt ein Aufruf einer Methode der Controller-Klasse die Daten aus der Datenbank ausliest:
controller.selectUnternehmen();
Die entsprechende Methode im Controller sieht folgendermaßen aus:
/**
*
* Auslesen der Unternehmen-Daten
*
*/
public void selectUnternehmen() {
List<Object> results = null;
try {
Query query = em.createQuery("SELECT C FROM Unternehmen C");
results = query.getResultList();
} catch (Exception e) {
e.printStackTrace(System.out);
}
/**
* Views aktualisieren
*/
tellChangedData(results);
}
Das heißt die Daten werden durch die Methode nicht direkt zurückgeliefert; stattdessen wird die Methode tellChangedData mit der Liste der ausgelesenen Objekte aufgerufen - diese kapselt die Daten in ein Objekt der ResultContent-Klasse und übergibt sie an die Methode submitData, die sie an die Views weiterreicht.
/**
*
* Daten weiterreichen, gekapselt in ein ResultContent-Objekt
*
* @param results Liste mit eventuellen Ergebnissen
*/
private void tellChangedData(List<Object> results)
{
if (results != null) {
if (results.size() > 0) {
submitData(new ResultContent(results.get(0), results));
} else {
submitData(null);
}
} else {
submitData(null);
}
}
/**
* benachrichtige Views, dass Daten geändert wurden.
*
* @param object
* geändertes Objekt
*/
private void submitData(Object object) {
// setChanged() markiert das Objekt als sendebereit, sodass bei
// notifyObservers() Meldungen gegeben werden.
setChanged();
notifyObservers(object);
}
In Java kommt man eher unelegant an den inneren Typ einer generischen Klasse, deswegen die Kapselung über die ResultContent-Klasse. Diese sieht übrigens folgendermaßen aus:
public class ResultContent
{
private Object listType = null;
private List<Object> listContent = null;
public ResultContent(Object listType, List<Object> listContent)
{
this.listType = listType;
this.listContent = listContent;
}
public Object getListType()
{
return listType;
}
public List<Object> getListContent()
{
return listContent;
}
public boolean isEmpty()
{
return (listType == null || listContent == null);
}
}
Die Methode getListContent() von ResultContent liefert die enthaltenen Daten zurück, während getListType() den inneren Typ der generischen Liste zurückliefert.
Nach der Kapselung in ein Objekt der ResultContent-Klasse wird wie gesagt die submitData-Methode mit dem Objekt der Daten aufgerufen welche sie an die Views weiterreicht. Hierbei ist übrigens zu beachten dass die Methoden setChanged() und notifyObservers() von Java für den Aufruf einen Objekt-Kontext benötigen, also nicht von einer statischen Methode aus aufgerufen werden können.
In den Views gibt es dann eine update()-Methode, die die empfangenen Daten auswertet:
/**
* Benachrichtung vom Observer-Pattern
*/
@Override
public void update(Observable arg0, Object arg1) {
/**
* Daten in View übernehmen
*/
if (arg1 != null) {
if (((ResultContent) arg1).getListType() instanceof Unternehmen) {
setScrollPane(unternehmenTable);
List<Object> data = ((ResultContent) arg1).getListContent();
for (Object f : data) {
unternehmenTable.dtm
.addRow(new Object[] { ((Unternehmen) f).getName() });
}
}
} else {
/**
* wenn kein Objekt zurückgeliefert wurde alle Daten aus dem RAM löschen.
*/
ansprechpartnerTable.clearTable();
unternehmenTable.clearTable();
zustaendigkeitTable.clearTable();
}
// Aufruf der validate-Methode für Neuzeichnen notwendig!
validate();
repaint();
}
Das heißt der View wird von ausgelesenen Daten aus der Datenbank informiert und entscheidet dann anhand des Typs der empfangenen Daten, ob die Daten für ihn relevant sind – und aktualisiert die Anzeige entsprechend.
Für die ResultContent-Klasse ist es notwendig als inneren Typ der generischen Liste den Grundtyp Object zu verwenden, denn alle generierten Entity-Klassen von EclipseLink leiten von Object ab.
Auf diese Weise ist das objektorientierte Prinzip vollständig umgesetzt. Zur Verdeutlichung des Observer-Patterns könnte man folgendes Beispiel einmal bringen:
1. Bestimmte Daten werden 10x aus der Datenbank ausgelesen und für jeden Anfrager direkt zurückgeliefert
2. Bestimmte Daten werden 1x ausgelesen und an alle Empfänger verteilt
Es ist klar dass bei Nutzung des Observer-Patterns – entsprechend Fall 2 – der Datenbankserver entlastet wird, was aber bei einer eingebetteten Datenbank keine große Rolle spielt. Wichtiger ist – vor allem bei nicht-modalen Anwendungen – dass stets alle Views auf dem gleichen Datenstand sind, und das wird mit dem Observer-Pattern gewährleistet.
Java bietet eine Observer-Implementierung bereits an, sie muss nur genutzt werden. Der Nutzung der Observer-Implementierung in Java sprechen keine Nachteile entgegen, schließlich werden nur Referenzen auf das Objekt an alle Views verteilt. Innerhalb einer Einzelplatz-Anwendung spricht nichts gegen die Nutzung.
Sobald aber der Datenbankserver auf einen anderen Rechner ausgelagert wird ist die Nutzung der Observer-Implementierung von Java allein nicht mehr ausreichend. Denn wenn Client A Daten in die Datenbank schreibt, bekommt Client B davon nichts mit. Für diesen Fall gibt es übrigens Message-Broker-Software wie Apache ActiveMQ.
Wenn man das Observer-Prinzip globaler – über ein Netzwerk – selbst implementieren würde hätte eine Entscheidung pro Observer-Prinzip eine Entlastung des Datenbankservers, aber auch eine eventuelle Erhöhung des allgemeinen Netzwerktraffics zur Folge. Schließlich werden dann einmal angefragte Daten an alle Clients – auch wenn diese sie gerade nicht benötigen – verteilt. Dann müsste man eventuell den Empfänger-Kreis einschränken; grundsätzlich halte ich aber ausschließliche Punkt-zu-Punkt-Datenlieferungen für veraltet.
Das Model-View-Controller-Prinzip ist jedenfalls richtig elegant erst mit dem Observer-Pattern umgesetzt. Übrigens, wenn Daten neu in die Datenbank geschrieben werden müssen natürlich alle SELECT-Methoden, die davon betroffen sind, erneut aufgerufen werden. An der Stelle könnte man sich noch einen Automatismus ausdenken - etwa dass alle Foreign-Keys der Tabelle, in die man gerade Daten eingefügt hat, ausgelesen und entsprechend benannte SELECT-Methoden per Reflection automatisch aufgerufen werden – das wäre dann aber wieder schlecht zu debuggen….
Nachfolgend die Projekte zum Download. Aus platztechnischen Gründen sind die erforderlichen Bibliotheken nicht enthalten. Zum Ausführen benötigen die Projekte folgende Bibliotheksdateien, die anderswo frei heruntergeladen werden können:
- eclipselink.jar
- hsqldb.jar
- javax.persistence_2.0.4.v201112161009.jar
Alleine die Bibliothek für EclipseLink ist mit 8 MB größer als die gesamte HSQLDB-Datenbank. Damit hat man dann aber auch eine vollständige objektorientierte Umsetzung. Aus technischer Sicht ist das schon fortschrittlicher als direkt mit SQL-Befehlen zu hantieren.
Man sollte sich aber auch bewusst machen dass jede weitere Softwareschicht die eigene Anwendung verlangsamt und potenzielle neue Fehlerquellen erschliesst. Das heißt für bestimmte Zwecke kann es weiterhin sinnvoll sein direkt mit SQL-Befehlen zu arbeiten.
SE_Aufgabe_8 (alt).zip
SE_Aufgabe_8 (neu).zip
SE_vert_Prototyp.zip
Beim .NET-Framework habe ich mich übrigens intensiv mit dem objektrelationalen Mapper LINQ beschäftigt.
Die Vorgabe im Studium war übrigens nur objektorientiert zu programmieren, damit wurde sie übererfüllt. Ein objektrelationaler Mapper musste nicht benutzt werden und auch die Einhaltung des MVC-Patterns wurde nicht überprüft. Dafür wären dann auch die ein bis maximal zwei Wochen Vorgabezeit für die Aufgabe ziemlich knapp gewesen, zumal objektrelationale Mapper in den Pflichtfächern (bis auf propel für PHP) nicht gelehrt wurden.
Update 31.12.2012: Im DS-Explorer von Eclipse sollte für eine Embedded-HSQLDB-Datenbankverbindung unter "HSQLDB Profile Properties" auf der Karteikarte Optional das Attribut shutdown=true hinzugefügt werden, weil nur dann beim Schließen der Datenbankverbindung die Lock-Datei wieder gelöscht wird. Also es geht auch ohne Eclipse zu beenden und die Lock-Datei händisch zu löschen, aber eigentlich bin ich der Meinung dass das DBMS bei einer Embedded-Datenbank und dem Verlust der einzigen zulässigen Verbindung immer eigenständig herunterfahren sollte. Reconnects sollten bei Embedded-Datenbanken eigentlich nicht nötig sein....
Anhang
Beim Programmieren sind natürlich wie üblich einige Probleme aufgetreten, nachfolgend einige Tipps und Links.
- Unbedingt Foreign-Keys in die Datenbank setzen bevor die Entity-Klassen generiert werden, weil nur dann erweiterte Getter generiert werden um über den FK einer Tabelle das referenzierte Objekt der anderen Tabelle zu bekommen. Bei Projekt Nr. 2 wurden die entsprechenden Getter noch händisch erstellt, aber EclipseLink kann diese auch automatisch generieren – siehe Projekt Nr. 3.
- Es gibt verschiedene Möglichkeiten die Primary Keys in der Datenbank zu generieren, als Möglichkeiten erwähne ich 1. Autoinkrement-Felder (Indentitys) und 2. Sequenzen. Identitys sind am einfachsten umzusetzen, in Projekt Nr. 3 wurden diese benutzt. Nachfolgend ein Link mit einer Übersicht der verschiedenen ID-Generierungsarten: http://www.developerscrappad.com/408/java/java-ee/ejb3-jpa-3-ways-of-generating-primary-key-through-generatedvalue/
- EclipseLink/JPA geht bei den Primary Keys von Werten ab 1 aus, das muss mit dem Datenbankschema abgeglichen werden. Wenn man davon abweichend 0 als möglichen Primary Key zulassen will muss in der Persistence.xml folgende Zeile hinzugefügt werden: <property name="eclipselink.allow-zero-id" value="true"/>, siehe auch http://meetrohan.blogspot.de/2011/11/eclipselink-null-primary-key.html
- Ganz wichtig ist die Syntax der SQL-ähnlichen Abfragesprache JPQL von EclipseLink/JPA, siehe folgenden Link http://www.objectdb.com/java/jpa/query/jpql/structure
- Tutorials zu EclipseLink/JPA: 1. http://www.torsten-horn.de/techdocs/java-jpa.htm#JpaJavaSE 2. http://www.vogella.com/articles/JavaPersistenceAPI/article.html oder 3. http://schuchert.wikispaces.com/JPA+Tutorial+1+-+Getting+Started oder 4. http://www.prozesse-und-systeme.de/jpaEinleitung.html
- Field vs. Property-Access bei EclipseLink/JPA: http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access
- An den Connectionstring für HSQLDB sollte shutdown=true angehängt werden damit das DBMS automatisch herunterfährt sobald die letzte Verbindung geschlossen wurde, siehe auch http://hsqldb.org/doc/2.0/guide/dbproperties-chapt.html
- Bei einer eingebetteten Datenbank sollte der Transaktionstyp auf RESOURCE_LOCAL gestellt werden, siehe auch http://thought-bytes.blogspot.de/2007/04/hello-world-with-standalone-java.html
- Bei der erstmaligen Benutzung von EclipseLink trat hier diese Fehlermeldung auf und es musste eine neue Eclipse-Installation genutzt werden: Link.