Persistence Layer
Aus Salespoint
(→Änderungen ab Version 2010 1.1.0) |
(→Datentypen) |
||
Zeile 99: | Zeile 99: | ||
|align="center"| | |align="center"| | ||
|- | |- | ||
- | + | |java.lang.Enum | |
+ | |align="center"| | ||
+ | |align="center"| | ||
|} | |} | ||
Version vom 23:23, 9. Okt. 2010
Ein elementarer Punkt der Datenhaltung ist die Speicherung von transienten Daten auf einem persistenten Datenträger. Dies geschieht meist durch die Anbindung der Applikation an eine Datenbank. Die Aufgabe des Persistence Layer's besteht daher auf Folgendem:
- Sicherung der Objektdaten in der Datenbank
- Wiederherstellung von Objekten aus der Datenbank
- Gewährleistung der Referentiellen Integrität
- Verwaltung von Datenbankverbindungen
Inhaltsverzeichnis |
Änderungen ab Version 2010 1.1.0
Ab Version "Salespoint 2010 1.1.0" wurden einige Veränderungen gemacht, die den Umgang mit der Persistenz deutlich erleichtern sollen. Folgende Vereinfachungen wurden vorgenommen:
- Die Klassen: PersistentList/Map, CatalogImpl, StockImpl sind ab sofort speicherbar
- die oben genannten Klassen besitzen nun jeweils einen Konstruktor der weder Host noch Id benötigt. In diesem Fall wird eine ID automatisch generiert
- Zyklische Referenzen sind nun unterstützt
- Enumerationen sind speicherbar
Komponenten
Die folgende Abbildung zeigt den generellen Aufbau des Persistence Layer's in SalesPoint ab version 4.0, das die bisherige Serialisierung ablöst.
PersistenceManager
Der PersistenceManager ist die zentrale Komponente des PersistenceLayers. Er ist als Singleton implementiert und kann daher mit
PersistenceManager.getInstance();
angesprochen werden. Er Verwaltet die Datenbankverbindungen und sorgt für die Speicherung und Wiederherstellung von Objekten.
persist(Object obj)
Eine zentrale Funktion ist die Methode persist(Object obj). Sie speichert die wichtigen Daten des übergebenen Objektes und gibt einen eindeutigen Wert zurück bzw. null wenn das Objekt nicht gespeichert werden konnte. Rein theoretisch kann jedes Objekt übergeben werden... praktisch jedoch sollten diese Objekt gewisse Eigenschaften haben (siehe ...)
recover(Class class, Object ident)
recover(...) ist das Gegenstück zu persist(...). Es stellt gepeicherte Objekt wiederher. Dazu wird einerseits die Klasse des Objektes und seine eindeutiger Schlüssel benötigt. Die Methode gibt entweder das wiederhergestellte Objekt oder null im Fehlerfall zurück.
Listen
Der PersistenceManager stellt weiterhin Funktionen zur Listenverwaltung zur Verfügung die von persistenten Listen und Maps benutzt werden
Datentypen
Da die Daten letztendlich in eine Datenbank geschrieben werden müssen, nimmt der ausgewählte Datenbanktreiber ein Mapping des Java Datentyps auf den passenden SQL Datentyp vor (Unterscheiden sich trotz Standard). Dieses Mapping kann allerdings nur für bestimmte, einfache Datentypen vorgenommen werden, die in folgendr Tabelle aufgelistet sind:
Java-Typ | Besonderheiten | SQL-Typ (JavaDB) |
---|---|---|
int
java.lang.Integer | integer | |
long
java.lang.Long | bigint | |
java.lang.String | Lange Zeichketten können mittels Annotation auf TEXT gemapped werden. Dies ist allerdings nicht für ID-Felder möglich. | varchar(1024)
text |
boolean
java.lang.Boolean | smallint | |
java.math.BigDecimal | decimal | |
double
java.lang.Double | float | |
float
java.lang.Float | real | |
short
java.lang.Short | smallint | |
java.sql.Timestamp | Objekte, z.B. vom Typ java.util.Date usw. können nicht gespeichert werden. | timestamp |
byte[] | blob | |
java.lang.Class | ||
java.lang.Enum |
Die Java Class-Library besteht allerdings aus einer vielzahl weiterer Klassen, die sich nur ggf. speichern lassen. Das größte Problem zeigt sich dort in Wiederherstellung der Objekte. Ein JFrame z.B. enthält Referenzen auf einen Systemabhängigen Grafikkontext, als auch sonstige Betriebssystemschnittstellen. Es ist daher dringend davon abzuraten, diese Klassen persistent zu speichern. Weiterhin Enumerationen (Kann mittels Konstanten umgangen werden), Arrays(ausser das byte Array) und Collections nicht speicherbar. Anstatt Arrays und Collections werden die Datentypen PersistentList und PersistentMap angeboten.
Annotationen
Um die Art und Weise der Peristenz für gewisse Klassen besser kontrollieren zu können, besteht die Möglichkeit Annotationen in der Klassendefinition zu verwenden. Dazu werden die zur Verfügung stehenden Annotationen im Folgenden näher erläutert:
- PersistenceProperty: Diese Annotation ist gültig für Attribuute einer Klasse.
- follow (default: true): Hiermit wird gesteuert, ob das annotierte Feld gespeichert werden soll (true) oder nicht (false)
- isUnique (default: false): Definiert das annotierte Feld als ID-Feld. D.h., dass jede Instanz dieser Klasse eindeutig durch den Wert dieses Feldes identifiziert werden kann. Diese Annotation wird vererbt und kann in abgeleiteten Klassen überschrieben werden. Jede Klasse kan nur ein ID-Feld besitzen.
- autoAssign (default: false): Nur zulässig fpr ID-Felder. Gibt an, ob diesem Feld beim speichern, automatisch ein eindeutiger Wert zugewiesen werden soll. Der Datentyp diese Felder sollt vom Type int oder long sein.
- isLongString (default: false): Nicht zulässig für ID-Felder. Datentyp des Feldes muss vom Typ String sein. Erlaubt die Speicherung sehr langer Zeichketten (> 1024 Zeichen)
- RecoveryProperty: Diese Annotation ist für Klassendefinitionen zulässig und steuert den Wiederherstellungsprozess (Recovery)
- initialize (default: true): Gibt an, ob die Attribute des Objektes nach dessen Instanzierung automatisch mit den gespeicherten Werten besetzt werden sollen
- RecoveryConstructor: Diese Annotation ist nur für Konstruktoren zulässig. Sie darf nur einmal in einer Klassendefinition verwendet werden und gibt den Konstruktor an, der für die Recovery genutzt werden sollen. Da jedes Objekt durch einen der definierten Konstruktoren instanziiert werden muss, ist es notwendig einen Konstruktor entsprechend zu annotieren. Ist dies nicht geschehen, so wird der Konstruktor ausgewählt, der keine Parameter benötigt. Ist dieser nicht definiert, kann das Objekt nicht instanziiert werden.
- parameters: Sofern der Konstruktor Parameter benötigt, müssen entsprechende Werte bei der Instanziierung an den Konstruktor gegeben werden. Dafär können nur Werte von Feldern der Klasse benutzt werden, die gespeichert wurden. Die Namen dieser Felder müssen als String Array angegeben werden (z.B. {"Feld1", "Feld2", ...})
ClassFieldMapper
Der ClassFieldMapper filtert die Daten von Objekten und gibt weitere Informationen zu Klassen. Er ist standardgemäß als DefaultClassFieldMapper implementiert, welcher in der Lage ist Annotationen zu verarbeiten.
ClassNameEncoder
Der ClassNameEncoder kodiert Klassennamen in einen eindeutigen String der als Tabellenname in der Datenbank verwendet werden kann. Der DefaulClassFieldMapper als seine standard Implementierung hashed dazu diesen namen via SHA-1 oder MD5 oder ersetzt einfach einige Zeichen.
DatabaseConnection
Das Interface DatabaseConnection stellt eine Datenbankverbindung mit all seine Verbindungsdaten dar. Es ist selbst speicher- und wiederherstellbar. Die konkreten Implementationen für die verschiedenen Datenbanken übernehmen die Vereinheitlichung der datenbankspezifischen Operationen und Typ Konvertierungen.
DatabaseConnectionTemplates
Diese Templates sind dazu da um neue Datenbankverbindungen zu erzeugen.
PersistentMap
Ist eine Implementation der Map-Schnittstelle für eine persistente Datenbasis. Alles was der Map hinzugefügt wird, ist also aus der Datenbank wiederherstellbar. Die Map verwendet weiterhin einen Cache um Datenbankzugriffe zu vermindern. Jede dieser Maps hat eine eindeutige Id, die aus den im Konstruktor übergebenen Paramtern "host" und "id" zusammengesetzt wird. Maps mit gleicher Id arbeiten somit auf den selben Daten. Seit Version 1.1.0 des Frameworks ist es möglich PersistentMaps selbst zu speichern. Sollte eine eigene lesbare ID nicht erforderlich sein, so ist es möglich den Konstruktor ohne Angabe von ID und oder Host aufzurufen und an die Map wird eine automatisch generierte ID vergeben. Selbiges gilt für die PersistentList, Catalogs und Stocks.
PersistentList
Verhält sich ähnlich wie die PeristentMap, nur handelt es sich hierbei um eine Liste
DataBaskets
Databaskets können und sollen nicht gespeichert werden! Bei Databaskets handelt es sich NICHT, wie oft angenommen, um Warenkörbe. Databaskets implementieren lediglich Application-Level Transaktionen.
Konsequenzen für die Nutzung des Frameworks
Gespeichert werden sollten CatalogItems, StockItems und User, da diese die zentralen Klassen zur Daten- bzw. Benutzerverwaltung darstellen. Ihre Implementationen: CatalogItemImpl, StockItemImpl und User sind bereits für die Speicherung vorbereitet. Bei der konkreten Ableitung gilt es jedoch einiges zu beachten.
Jedes Objekt muss eindeutig identifizierbar sein Dazu kann entweder ein Attribut der Klasse als eindeutig gekennzeichnet werden oder der Persistence Layer wird selbst einen Schlüssel erzeugen. Folgendes Beispiel zeigt einen Ausschnitt aus der Klasse AbstractNameable, von welcher CatalogItms und StockItems erben:
@PersistenceProperty(isUnique = true)
private String m_sName;
Wie zu sehen ist, wurde das Attribut "m_sName" als eindeutig gekennzeichnet. Dies geschieht durch die Annotation PersistenceProperty. Der vergebene Schlüssel muss nur innerhalb aller Objekte dieser Klasse eindeutig sein. Alle Klassen die jetzt von AbstractNamable erben, haben ebenfalls m_sName als eindeutiges Attribut. Es besteht aber die Möglichkeit dieses durch eine erneute Annotation zu überschreiben. Es ist daher meist nicht notwendig für alle Unterklassen von CatalogItem einen anderen Schlüssel zu vergeben. StockItems hingegen können meist nicht eindeutig durch ihren Namen identifiziert werden. Daher wird für diese ein automatisch generierter Schlüssel vergeben. Folgender Ausschnitt ist aus der Klasse StockItemImpl:
@PersistenceProperty(isUnique = true, autoAssign = true)
private int m_id;
Hier wurde ein Integer Field als eindeutig gekennzeichnet. "autoAssign = true" zeigt hier an, dass ein Schlüssel generiert werden soll. Dieser Schlüssel wird nach dem Speichervorgang in das Feld geschrieben und ist von dort aus abrufbar. Die automatische Schlüsselerzeugung funktioniert nur mit Feldern vom Typ int oder long.
Folgender Code stammt aus der Klasse CatalogItemImpl:
@PersistenceProperty(follow = false)
private CatalogImpl<?> m_ciOwner;
"follow = false" zeigt an, dass der Inhalt dieses Feldes nicht gespeichert werden soll. Dies empfiehlt sich für viele Felder mit komplexen Datentypen, da diese meist nicht für die Speicherung vorbereitet wurden. Vermeiden sie außerdem unbedingt, das Referenzen in irgendeiner Form Kreise bilden, dass heißt: Dass sie irgendwann wieder auf das Ausgangsobjekt zurück verweisen.
Felder vom Typ Array können nicht gespeichert werden. Verwenden sie dazu die Klassen PeristentMap bzw. PersistentList. Ein Beispiel dafür entstammt der Klasse User:
@PersistenceProperty(follow = false)
private Map<String, Capability> m_mpCapabilities = null;
m_mpCapabilities = new PersistentMap<String, Capability>(this, null, String.class, Capability.class);
Sollten alle diese Bedingungen erfüllt sein, kann das Objekt mittels der persist Methode gespeichert werden. Für die Wiederherstellung gibt es auch einiges zu beachten. Das Objekt muss instanziiert werden. Dafür muss entweder ein Konstruktor deklariert, der keine Parameter hat, oder die folgende Methode verwendet werden:
public class Video extends CatalogItemImpl implements Descriptive, Categorizable {
.
.
.
@RecoveryConstructor(parameters = { "m_sName" })
public Video(String name) {
super(name);
}
.
.
.
}
Hier wurde ein Konstruktor explizit als Konstruktor für die Wiederherstellung gekennzeichnet. Dort muss mittels "parameters = {...}" angegeben werden in welcher Reihenfolge, welcher Parameter an den Konstruktor übergeben werden soll. Zur Erinnerung: "m_sName" ist das Feld, welches in AbstractNameable deklariert wurde und somit Bestandteil der Klasse Video ist. Besonderheiten ergeben sich bei Annonymen inneren und Lokalen Klassen: Diese Klassen kriegen implizit eine Referenz auf ihre Äußere Klasse übergeben und dass heisst der PersistentManager muss diese Referenz irgendwoher bekommen. Dies passiert dadurch, dass diese Äußere Klasse ebenfalls neu instanziiert wird, was eigentlich nicht erwünscht ist. Daher ist davon abzuraten diese Klassen für die Speicherung zu verwenden. Wer zu faul ist eine Top Level Klasse zu schreiben, kann dafür statische innere Klassen verwenden, die keine Probleme bereiten sollten.
Nachdem das Objekt instanziiert wurde, werden die gespeicherten Felder (ausgenommen sind Felder die an den Konstruktor übergeben wurden) im Objekt gesetzt. Um nach der Instanziierung eigenen Code auszuführen muss die Klasse das Interface Recoverable implementieren. Folgender Code aus CatalogItemImpl demonstriert dies:
public void recover(Map<String, Object> data, Object recoveryContext, boolean reInit)
{
if (recoveryContext instanceof CatalogImpl)
{
setCatalog((CatalogImpl) recoveryContext);
}
m_chImage = null;
}
Der Parameter data beinhaltet die Werte der einzelnen Felder. Der recoveryContext entspricht in der Regel dem Catalog bzw. Stock in dem sich das Item befindet und reInit gibt an, ob das Objekt zum ersten mal wiederhergestellt wird, oder nur seine Felder aktualisiert wurden.
Beachten sie bitte das zur Speicherung von Zeiten und Daten der Datentyp java.sql.TimeStamp verwendet werden muss.
Persistence Events
Der PersistenceManger produziert verschiedene Events wofür Event Listeners registriert werden können.
DataSourceOnChange
Wird getriggert, kurz bevor die Datenquelle geändert wird, wenn zB die Datenbank zur Laufzeit gewechselt wird. Dieses Event ist insbesondere für Databaskets interessant, da diese ein rollback ausführen sollten.
DataSourceChanged
Wird getriggert sobald sich die Datequelle geändert hat. Dies ist interessant für PersistentMaps und PersistentLists, da diese ihre Caches invalidieren müssen und das Event entsprechend weiter propagiert wird (an Cataloge, Stocks, visuelle Komponenten....)
ExternalModification
SalesPoint unterstützt das verteilte Arbeiten auf der selben Datenquelle. Sollte eine Modifikation auftreten wird dieses Event getriggert. Weiterhin werden ähnliche Maßnahmen getroffen wie bei einem DataSourceChanged Event.
Beispiel
In diesem Beispiel soll kurz demonstriert werden, wie Persistente Klassen erstellt werden können. Dieses Beispiel bietet sich als Grundlage weiterer Experimente an.
Dazu definieren wir eine Klasse A:
public class A
{
@PersistenceProperty(isUnique = true)
public String name;
public PersistentList<String> zeuchs = new PersistentList<String>(String.class);
public B b;
@RecoveryConstructor(parameters = {"name"})
public A(String name)
{
this.name = name;
}
public String toString()
{
String res = name;
if (b != null)
res += " - " + b.name;
return res;
}
}
und eine Klasse B (sichtlich sehr ähnlich zu Klasse A):
public class B
{
@PersistenceProperty(isUnique = true)
public String name;
public PersistentList<String> zeuchs = new PersistentList<String>(String.class);
public A a;
@RecoveryConstructor(parameters = {"name"})
public B(String name)
{
this.name = name;
}
public String toString()
{
String res = name;
if (a != null)
res += " - " + a.name;
return res;
}
}
Beide Klassen beinhalten jeweils die Attribute name, zeuchs und a bzw. b . Das Attribut name ist vom Datentyp String und ist daher direkt speicherbar. Weiterhin wurde das Feld annotiert und als eindeutig markiert (isUnique = true). Dies bedeutet, dass sich alle Instanzen dieser Klasse durch das Attribut name eindeutig unterscheiden lassen. Das Attribut zeuchs hingegen ist kein einfacher Datentyp. Wie bereits erklärt, kann nicht jeder nicht-primitive Datentyp gespeichert werden. Ab Version 1.1.0 des Frameworks ist die PersistentList allerdings speicherbar. Aufgrund von Unzulänglichkeiten, seitens Java ist es nötig, der persistenten Liste den Generischen parameter erneut zu übergeben. Das 3te Attribut a bzw. b ist ebenfalls kein primitiver Datentyp. Kann aller dings gespeichert werden, weil es sich um entsprechend annotierte Klassen handelt. Weiterhin wurde ein Konstruktor definiert. Da ein Konstruktor definiert wurde, der einen Parameter benötigt, wird von Java selbst kein Parameter-freier Konstruktor erstellt. Daher ist es notwendig unseren Konstruktor als RecoveryConstructor zu annotieren. Als Parameter soll der Inhalt des Feldes/Attributes name übergeben werden.
Die Klassen A und B sind somit bereit um gespeichert und wiederhergestellt zu werden. Folgender Code soll nun den Umgang mit den Klassen demonstrieren:
//TESTING
A a1 = new A("A Test 1");
A a2 = new A("A Test 2");
B b1 = new B("B Test 1");
B b2 = new B("B Test 2");
a1.b = b1;
b1.a = a1;
PersistentList<Object> p = new PersistentList<Object>("meineliste", A.class);
p.clear();
p.add(a1);
p.add(a2);
p.add(b1);
p.add(b2);
Im Beispiel werden 2 Instanzen der Klasse A als auch 2 Instanzen der Klasse B angelegt. Beachten: Alle Instanzen der Klasse A bzw. B müssen verschiedene Namen haben, da Name das ID-Feld ist. Daraufhin wird dem Objekt a1 eine Referenz auf das Objekt b1 zugewiesen und umgedreht. Es ergibt sich ein Zyklus. Zyklen sollten vermieden werden, sind ab Version 1.1.0 allerdings erlaubt. Allerdings kann es durch sog. Delayed Assignment zu Unregelmässigkeiten während der Recovery kommen... in der Regel aber nicht. Daraufhin wird eine Object-Liste mit der ID meineliste angelegt, die zunächst geleert wird und der daraufhin die erstellten Objekte hinzugefügt werden. Wird dieser Code nun ausgeführt Wird in der Datenbank eine Liste mit unseren erstellten Objekten persistent gespeichert.
Der folgende Code dient nun der Wiederherstellung dieser Liste:
//TESTING
PersistentList<Object> p = new PersistentList<Object>("meineliste", A.class);
for (Object a : p)
{
System.out.println(a.toString());
}
Wie vorher wird auch hier dieselbe PersistentList angelegt, mit der selben ID. Alle Persistenten Listen, mit selbem Host und selber ID operieren auf den selben Daten! Die persistente Liste wäre somit wiederhegstellt und kann nun wie gewohnt mittels Iterator durchlaufen werden. Die Ausgabe zeigt, dass alle Objekte erfolgreich wiederhegestellt wurden:
A Test 1 - B Test 1
A Test 2
B Test 1 - A Test 1
B Test 2