5.1 Datenbank-Updates
Das Anlegen bzw. die Änderung des Schemas einer Business-Object-Definition führt zur Erstellung einer neuen Datenbanktabelle auf der Datenbank. Wurde das Schema geändert, dann hat die neue Tabelle eine andere Struktur als die schon vorhandene. So können bzw. neue Attribute hinzugefügt oder gelöscht worden sein oder der Datentyp eines Attributes wurde geändert. Generell ist eine Datenübernahme aus der bisherigen in die neue Datenbanktabelle des Business Objects notwendig, um keine Daten zu verlieren.
Aufgrund der angesprochenen Strukturänderungen kann es notwendig sein, die Daten bei der Übernahme von der alten in die neue Tabelle zu konvertieren. So müssen bei neu eingeführten Attributen Defaultwerte gesetzt werden, um einen initialen Zustand festzulegen. Bei einer Datentypänderung eines Attributes sind Funktionen nötig, welche die Attributwerte zwischen dem alten und neuen Datentyp konvertieren können. Um das zu erreichen, hat der Entwickler ein Update-Programm für das betreffende Business Object zu hinterlegen, welches vom System zur Datenübernahme zwischen alter und neuer Datenbanktabelle sowie zur Konvertierung der Daten aufgerufen wird.
Bei größeren Strukturänderungen, die mehr als ein Business Object betreffen, auch bei Änderungen in der Semantik von Attributen müssen komplexere Konvertierungs-vorschriften angebbar sein, um die Daten zu korrigieren bzw. zu migrieren. Dazu dient im ERP-System ein spezieller Typ von Anwendungen, die Datenaktualisierungen, der hier nicht betrachtet wird.
5.1.1 Generierung von Business Objects
Der Generierungsablauf ist eng mit dem Zustand der zugehörigen Entwicklungsaufgabe verbunden.
Um ein Business Object anzulegen bzw. bearbeiten zu können, muss es in eine existierende Entwicklungsaufgabe aufgenommen werden. Nach Änderung des Schemas des Business Objects, z.B. dem Hinzufügen eines neuen Attributes, beginnt die Generierungsphase, in der die entsprechenden Tools über die Kommandozeile des SAS ausgeführt werden müssen.
Die Generierung eines Business Objects erfolgt durch Aufruf der Tools mit entsprechenden Parametern in mehreren Schritten, die in einer festgelegten Reihenfolge auszuführen sind. Bei allen Aufrufen muss die Nummer der Entwicklungsaufgabe über den Parameter „-j:nummer“ angegeben werden oder alternativ der Name des Business Objects (Parameter -o:).
5.1.1.1 Schritt 1 – Business Object erzeugen
Der Schritt 1 gliedert sich in die Teile
- Schema,
- Source und
Diese werden im nachstehenden Tool-Aufruf zusammengefasst. Der Aufruf kann mehrmals erfolgen:
crtbo –j:nummer
Schema
Dieser Teilschritt erzeugt die Vorlage für die Tabellengenerierung auf der Datenbank (Table Description) aus den erfassten Metadaten des Business Objects.
Source
Dieser Teilschritt erzeugt die Business Object- und Mapper-Klassen für den Zugriff auf das veränderte Tabellenschema. Die generierten Java-Klassen werden im Arbeitsverzeichnis der angegebenen Entwicklungsaufgabe abgelegt. Für jedes NLS-Attribut werden ein Business Object und Mapper-Klassen erzeugt, da die Nebenspracheninhalte in eigenen Datenbanktabellen gespeichert werden.
Das Arbeitsverzeichnis zur Entwicklungsaufgabe, welches die neuen Sourcen des Business Objects enthält, muss in die Entwicklungsumgebung z. B. „Eclipse“ eingebunden und kompiliert werden. Der lokal gestartete SAS muss die kompilierten Class-Files für die Folgeschritte (s. Schritt 2 und 3) einbinden.
Der Generierungsablauf ist eng mit dem Zustand der zugehörigen Entwicklungsaufgabe verbunden. Um ein Business Object anzulegen bzw. bearbeiten zu können, muss es in eine existierende Entwicklungsaufgabe aufgenommen werden. Nach Änderung des Schemas des Business Objects, z. B. dem Hinzufügen eines neuen Attributes, beginnt die Generierungsphase, in der die entsprechenden Tools über die Kommandozeile des SAS ausgeführt werden müssen.
Generate
Dieser Teilschritt erzeugt die temporäre Datenbanktabelle zur neuen Table Description auf der Datenbank. Es werden nur die Tabellen dupliziert, deren Business Objects im Job sind (und nicht pro Aufgabe die ganze Datenbank). Wenn man z. B. ein Business Object vom Typ „Entity“ in eine Entwicklungsaufgabe aufgenommen hat, aber nicht die zugehörigen Dependents, dann würde man bei der Neuanlage einer Instanz des Entitys diese in die neue temporäre Tabelle schreiben und die zugehörigen Dependent-Instanzen in deren alte aktive Tabellen. Alle anderen Benutzer auf dem System schreiben mit den alten aktiven Mappern neue Entity-Instanzen nur in die aktiven Tabellen. Das bedeutet, man erzeugt inkonsistente Zustände in der Datenbank. Deshalb ist Umsicht angebracht beim Test von größeren Entitys.
Als Schutzfunktion sind die temporären Tabellen schreibgeschützt, so dass es zu einer Exception kommt, wenn in die Tabelle geschrieben werden soll. Eine Änderung dieses Verhaltens erreicht man mit dem Startparameter „-writeConvertedTables” des Application Servers. Diesen Parameter kann man benutzen, wenn z. B. das geänderte Business Object keine Dependents hat und man die anzupassende Anwendung in derselben Aufgabe wie das Business Object bearbeiten will.
5.1.1.2 Schritt 2 – Update-Klassen
In diesem Schritt erfolgt die Entwicklung der Update-Klassen, die für die korrekte Datenübernahme aus der im System aktiven in die neue temporäre Tabelle sorgen.
Business Objects der NLS-Attribute haben ebenfalls eigene Update-Klassen, um die Nebenspracheninhalte zwischen alter und neuer NLS-Tabelle zu konvertieren.
Weitere Informationen zu den Update-Klassen finden Sie in dem Abschnitt „Entwicklung von Update-Klassen“.
5.1.1.3 Schritt 3 – Business Object konvertieren
In diesem Schritt erfolgt die Datenübernahme aus der alten im System aktiven Tabelle in die neue temporäre Tabelle. Dazu dient der folgende Toolaufruf:
cnvbo –j:nummer
Das System übernimmt und konvertiert die Daten von der alten in die neue Datenbanktabelle unter Nutzung der neuen Update-Klassen.
Achten Sie darauf, dass die neuen Update-Klassen im Klassenpfad des Application-Servers liegen, auf dem Sie die Konvertierung durchführen.
Wenn bei Konvertierung der Daten von der alten in die neue Tabelle Fehler auftreten, dann bricht die Konvertierung ab und muss mit korrigierten Update-Klassen wiederholt werden. Dieser Schritt kann beliebig oft wiederholt werden. da die temporäre Tabelle vor der Konvertierung zuerst komplett geleert wird.
5.1.1.4 Schritt 4 – Entwicklung und Test
Anschließend erfolgt der Test der Konvertierung und die weitere Programmentwicklung und Tests. Normalerweise sind nach Schemaänderungen an einem Business Object auch Anwendungen, die das Business Object benutzen, anzupassen. Abschließend erfolgt das Einchecken der Sourcen in das System mithilfe der Anwendung „Entwicklungsaufgaben“.
Zugriff auf die temporäre Tabelle
Der lokale SAS benutzt zum Zugriff auf die Datenbanktabelle des Business Objects die neu generierten Mapper-Klassen, welche auf die temporäre Tabelle zugreifen. Für Anwendungen, die auf diesem SAS laufen, ist die neue Version die aktive Version und arbeiten so auf dieser. Andere SAS, die nicht die neuen Klassen des Business Objects eingebunden haben, arbeiten auf der alten aktiven Version.
Die Abbildung veranschaulicht nochmals die Funktionsweise:
Temporäre Views
Views werden ebenfalls zuerst temporär erzeugt, damit der Entwickler Gelegenheit hat, den geänderten View vor der Aktivierung zu testen. Ein View kann auch auf die temporären Tabellen von Business Objects zugreifen, die in der gleichen Entwicklungsaufgabe sind wie der View selbst.
Business Object aus der Aufgabe entfernen
Ein Business Object kann mit der Anwendung „Entwicklungsobjekte“ nicht aus der Entwicklungsaufgabe entfernt werden. Zum Entfernen eines Business Objects, Parts oder Views müssen Sie das Tool „rmvbo“ verwenden:
rmvbo –o:Objektname
Das Tool löscht die temporären Tabellen von der Datenbank und entfernt das Business Object sowie die dafür erzeugten Sourcen aus der Entwicklungsaufgabe.
5.1.1.5 Schritt 5 – Freigabe Entwicklungsaufgabe
Wenn Sie das Business Object und ggf. andere Entwicklungsobjekte in der Entwicklungsaufgabe bearbeitet haben, müssen Sie die Entwicklungsaufgabe freigeben. Nach der Freigabe ist die Bearbeitung des Business Objects und der anderen Entwicklungsobjekte nicht mehr möglich.
5.1.1.6 Schritt 6 – Business Object aktivieren
Dieser Schritt schließt die Generierung ab. Es werden die aktiven Tabellen mit den neuen temporären Tabellen ersetzt. Dabei wird der Inhalt unter Verwendung der Update-Klassen in die neue Tabelle übernommen:
actbo –j:nummer
5.1.1.7 Schritt 7 – Aktivierung Entwicklungsaufgabe
Zum Abschluss der Entwicklung muss die Entwicklungsaufgabe aktiviert werden.
5.1.2 Entwicklung von Update-Klassen
Die Update-Klassen enthalten Logik, um die Daten einer älteren Version eines Business Objects in die aktive Version zu konvertieren. Die Update-Klassen enthalten für jede geänderte Spalte eines Business Objects die Information, wie die Änderung pro Spalte behandelt werden soll.
Die Update-Klassen bestehen aus den folgenden Java-Klassen:
- UpdateBase
Die Klasse UpdateBase wird beim Generieren des Business Objects generiert. In der Klasse UpdateBase wird für jede Neuanlage, Änderung oder Löschung einer Spalte eine ggf. abstrakte Methode generiert. Die UpdateBase-Klasse darf nicht adaptiert werden.
- UpdateLogic
Die Klasse UpdateLogic wird einmalig mit der Klasse UpdateBase angelegt, wird aber danach nicht mehr erneut generiert. Die Klasse UpdateLogic erbt von UpdateBase und kann bei Datenmodelländerungen erweitert werden. Sie müssen alle abstrakten Methoden implementieren und können die nicht abstrakten Methoden überschreiben, um eine andere Implementierung zu hinterlegen. Eine UpdateLogic-Klasse sollte nur auf dem System geändert werden, auf dem das zugehörige Business Object bzw. die Extension angelegt oder geändert wurde.
Für jedes Business Object und jede Extension gibt es separate Update-Klassen. Jede der Update-Klassen ist für die Attribute des zugeordneten Objektes zuständig. Die Update-Klassen werden im Namensraum „com.<Entwicklungspräfix>.upd“ abgelegt, z. B. „com.cisag.upd.app…“.
Datenmodelländerungen werden für jede Spalte einzeln betrachtet. Bei jeder Neuanlage, Änderung oder Löschung wird an der Spalte vermerkt, mit welcher Version diese Spalte wie verändert worden ist. Bei der normalen Bearbeitung werden alle Änderungen von Spalten mit einem Standardverhalten registriert, das vom Entwickler überschrieben werden kann. Die Änderungen werden soweit als möglich in der gleichen Reihenfolge wie bei der Entwicklung ausgeführt.
Die Update-Klassen unterstützen die folgenden Operationen:
- Initialisieren neuer Spalten („Init-Methode“)
- Konstante Initialwerte
- Berechnete Initialwerte
- Löschen einer Spalte (Löschkennzeichen an Init-Methode)
- Datentypänderungen existierender Spalten („ChangeDatatype-Methode“)
- Kompatible Datentypänderung
- Inkompatible Datentypänderung
- Allgemeine Datenkorrekturen
Beispiel für Update-Klassen
Dieses Beispiel soll die Funktionsweise der Update-Klassen erklären. Die Änderungen im Business Object sowie die verwendeten Methoden der Update-Klassen sind in fetter Schrift angegeben.
Beispiel für Update-Klassen bei Änderungen im Standard
In diesem Beispiel wird das Business Object „Item“ zweimal verändert. Als erstes wird das Attribut „description“ von 100 auf 80 Zeichen verkürzt und danach das Attribut „unit“ hinzugefügt.
- Die Methode changeDatatype€description€str100_str80 berechnet die inkompatible Datentypänderung des Attributs „description“ von 100 auf 80 Zeichen.
- Die Methode init€unit€GUID berechnet den Initialwert für das neue Attribut „unit“.
Die Methoden werden bei der Konvertierung aufgerufen.
Beispiel Partnerentwicklung
Anlegen einer Extension und Einspielen von einer Standard-Erweiterung
Zunächst wird eine Extension für das Business Object „Item“ mit den Attributen abc_type und abc_color angelegt und danach wird die Version 1.1 von „Item“ eingespielt. Sie müssen bei einer Extension keine manuelle Konfliktbearbeitung auf den Update-Klassen des Business Objects vornehmen.
- Die Methode init€abc_type€VS berechnet den Initialwert für die Spalte abc_type.
- Die Methode init€abc_color€VS berechnet den Initialwert für die Spalte abc_color.
Bei der Anlage der Extension werden die Methoden der Update-Klassen der Extension aufgerufen. Bei der Änderung des zugrunde liegenden Business Objects „Item“ müssen die Update-Klassen nicht verändert werden und es werden keine Methoden von der Update-Klasse der Extension aufgerufen.
Init-Methode
Die Init-Methode berechnet den Initialwert beim Anlegen einer neuen Spalte. Ein Initialwert wird dann benötigt, wenn eine Spalte in der neuen Version des Business Objects existiert, nicht aber in der alten Version. Abhängig von der Update-Methode wird die Init-Methode unterschiedlich verwendet (siehe „Update-Annotation“).
Die Init-Methode wird in der Klasse „UpdateBase“ mit einem konstanten Initialwert generiert. Die Init-Methode kann in der Klasse „UpdateLogic“ ggf. mit einer anderen Update-Methode überschrieben werden, um einen abweichenden Initialwert zu berechnen.
Der Name der Init-Methode setzt sich wie folgt zusammen:
<Java-Datatype> init€<AttributePath>€
<Datatype>(CisGenericObject object)
Der Attributpfad einer Spalte entspricht dem Namen der Spalte in OQL. Eckige Klammern „[]“ und der Punkt „.“ werden jedoch durch das Dollarzeichen „$“ ersetzt, damit der Attributpfad als Methodenbezeichner verwendet werden kann.
Beispiel:
Wenn der Attributpfad in OQL „discounts[1].discountType“ ist, dann wird im Methodennamen die Zeichenkette „discounts$1$$discountType“ verwendet.
Der Parameter object enthält den Zustand der Instanz vor der Initialisierung der neuen Spalte. Bei der „Update-Methode CONSTANT“ und der „Update-Methode DATABASE“ ist der Wert des Parameters object null.
Für ein Part-Attribut wird neben den init-Methoden der zugehörigen Spalten zusätzlich die init-Methode für die boolsche Spalte des Parts generiert. Diese hat als Attributpfad den Attributpfad des Part-Attributes. Der Rückgabewert der Methode gibt an, ob der Part-Wert gültig ist oder nicht. Um ein Part-Attribut mit einem Wert zu initialisieren, muss die init-Methode für die boolsche Part-Spalte so überschrieben werden, dass diese true liefert.
Beispiel
Initialwertberechnung der Spalte „description“ mit dem Datentyp „String“ und mit der Länge 100. Die Spalte wurde in der Version 7.3 im Release 5 angelegt.
UpdateBase-Klasse
@History(created={“7.3:5.0.0“)
@Update(method=CONSTANT)
String init€description€str100(CisGenericObject object) {
return “”;
}
UpdateLogic-Klasse
@override
@Update(method=INSTANCE)
String init€description€str100(CisGenericObject object) {
return “Artikel “+object.getString(“number”);
}
ChangeDatatype-Methode
Der Datentyp einer Spalte kann verändert werden. Um den Inhalt einer Spalte vom alten in den neuen Datentyp umzuwandeln, ist eine Berechnung erforderlich. Die Berechnung erfolgt in der ChangeDatatype-Methode, die in der UpdateBase-Klasse generiert wird:
<Java-TargetDatatype>
changeDatatype€<AttributePath>€<SourceDatatype>_<TargetDatatype>(CisGenericObject object)
An der ChangeDatatype-Methode wird mit der History-Annotation mit der Eigenschaft created angegeben, mit welcher Version der Datentyp verändert wurde.
Eine Datenmodelländerung ist kompatibel, wenn der primitive Datentyp nicht verändert und die Länge des Datentyps vergrößert wird. Alle anderen Änderungen sind inkompatibel.
Beispiel:
Die Änderung von String 80 auf String 100 ist kompatibel.
Die Änderung von String 100 auf String 90 ist inkompatibel.
Die Änderung von Boolean auf ValueSet ist inkompatibel.
Wenn die Datentypänderung inkompatibel war, so werden die ChangeDatatype-Methoden in der Klasse „UpdateBase“ abstrakt generiert und müssen vom Entwickler implementiert werden. Inkompatible Datenmodelländerungen haben immer die „Update-Methode INSTANCE“ und müssen somit pro Instanz berechnet werden.
Wenn die Datentypänderung kompatibel war, so werden die ChangeDatatype-Methoden in der Klasse „UpdateBase“ als final generiert und können vom Entwickler nicht überschrieben werden. Kompatible Datenmodelländerungen haben immer die „Update-Methode CONSTANT“ und können somit auf der Datenbank berechnet werden.
Die Update-Methode einer ChangeDatatype-Methode darf nicht verändert werden.
Inkompatible Änderungen erhöhen den Konfliktbearbeitungsaufwand in Folgesystemen. Vermeiden Sie inkompatible Datenmodelländerungen. Bei der parallelen Wartung sind inkompatible Datenmodelländerungen in älteren Releases besonders problematisch.
Beispiel
UpdateBase-Klasse
@History(created={“7.1:5.0.0“})
@Update(method=UpdateMethod.CONSTANT)
final String changeDatatype€description€str80_str100(CisGenericObject object) {
return object.getString(“description”);
}
@History(created={“8.1:5.0.0“})
@Update(method=UpdateMethod.INSTANCE)
abstract String changeDatatype€description€str100_str90(CisGenericObject object);
UpdateLogic-Klasse
@override
String changeDatatype€description€str100_str90(CisGenericObject object) {
String value = object.getString(“description”);
if (value.length()<=90) {
return value;
} else {
return value.substring(0,87)+”…”;
}
}
Löschen von Instanzen
Soll eine Instanz nicht konvertiert sondern gelöscht werden, so muss in einer der aufgerufenen Änderungsmethoden die Methode delete() aufgerufen werden. Das führt zum Abbruch der Konvertierung der aktuellen Instanz und diese wird nicht übernommen. Nach der Methode delete() muss die Änderungsmethode mit return verlassen werden. Nach delete() dürfen keine weiteren Befehle stehen.
Update-Annotation
Die Update-Annotation gibt an, ob die Konvertierung einer Spalte pro Instanz aufgerufen werden muss oder aber ob alle Instanzen einer Datenbank gleich behandelt werden können. Die Eigenschaft method der Update-Annotation spezifiziert den Aufrufkontext, der im Folgenden „Update-Methode“ genannt wird.
Update-Methode CONSTANT
Bei der Update-Methode CONSTANT wird der ausgezeichneten Methode keine konkrete Instanz eines Business Objects übergeben. Die Methode darf auf keine Objekte in irgendeiner Datenbank zugreifen. Das Ergebnis der Methode wird für eine beliebige Menge von Instanzen auf beliebigen Datenbanken angewendet.
Syntax:
@Update(method=UpdateMethod.CONSTANT)
Update-Methode DATABASE
Bei der Update-Methode DATABASE wird der ausgezeichneten Methode keine konkrete Instanz eines Business Objects übergeben. Die Methode darf generisch auf die Objekte der Datenbank zugreifen. Das Ergebnis der Methode wird für alle Instanzen auf einer Datenbank angewendet.
Syntax:
@Update(method=UpdateMethod.DATABASE)
Update-Methode INSTANCE
Bei der Update-Methode INSTANCE wird der ausgezeichneten Methode eine konkrete Instanz eines Business Objects übergeben. Für diese Instanz muss die Konvertierung berechnet werden. Die Methode darf generisch auf die Objekte der Datenbank zugreifen. Das Ergebnis der Methode wird nur für genau die übergebene Instanz verwendet.
Die Verwendung mindestens einer Methode mit der Update-Methode INSTANCE führt immer zu einer instanzweisen Konvertierung. Beachten Sie, dass die Methode für jede Instanz aufgerufen wird. Wenn Sie komplexe Berechnungen in dieser Methode durchführen, kann die Konvertierung der Daten in Folgesystemen lange dauern.
Syntax:
@Update(method=UpdateMethod.INSTANCE)
Generischer Zugriff auf Business Objects
Die Klasse com.cisag.pgm.datatype.CisGenericObject erlaubt einen generischen Zugriff auf beliebige Versionen eines Business Objects.
Die Spalten eines generischen Business Objects können über den Attributpfad abgefragt werden. Für jeden primitiven Datentyp sowie für die Special Parts existieren geeignete Get- und Set-Methoden.
Beispiel:
Die String-Spalte „description“ können Sie wie folgt abfragen:
object.getString(”description“);
Persistenzdienstzugriffe
In den Update-Klassen kann mit den normalen Business-Object-Mappern nicht auf Business Objects zugegriffen werden. Jeder Persistenzdienstzugriff, z.B. getObject, der ansonsten eine Business-Object-Instanz liefern würde, liefert in den Update-Klassen ein generisches Objekt.
Beim generischen Zugriff auf Business Objects können Sie alle Zugriffsmethoden des Object-Managers verwenden, d.h. getObject, getObjectIterator, getResultSet, usw. Der Object-Manager greift dabei immer auf die auf der Datenbank aktive Version des Business Objects zu. Sie können mit dem generischen Zugriff auf den Zustand der Datenbank vor der Datenmodelländerung zugreifen. Sie können keine Methoden des Object-Managers verwenden, die zum Schreiben auf die Datenbank dienen.
Mit der Methode buildPrimaryKey kann der Primärschlüssel oder der zeitabhängigen Primärschlüssel eines Business Objects erzeugt werden.
Beispiel
Beispiel für die Verwendung eines CisGenericObjects mit dem CisObjectManager:
CisGenericObject item = (CisGenericObject)om.getObject(CisGenericObject.buildPrimaryKey(”com.cisag.app.general.obj.Item“,itemGuid)
oldDescription=item.getString(”description“);
Initialisierung
Für die Konvertierung der Daten eines Business Objects auf einer Datenbank werden eine oder mehrere Instanzen der Update-Klassen erzeugt. Die Update-Klassen konvertieren die Daten der Quellversion, d.h. die auf der Datenbank aktive Version, in eine Zielversion, d.h. für den Entwickler die in einer Entwicklungsaufgabe gesperrte Version.
Das Schema der Quell- und Zielversion kann mit den folgenden Methoden abgefragt werden:
CisObjectSchema getFromSchema()
CisObjectSchema getToSchema()
Die Methode initialize() der Update-Klasse wird vor der Verwendung bei jeder Instanz aufgerufen. Die initialize-Methode kann in der UpdateLogic-Klasse überschrieben werden, um die Instanz der Update-Klasse für die Verwendung auf einer Datenbank zu initialisieren. In dieser Methode können Sie lesend über den generischen Zugriff auf die Business Objects zugreifen (Siehe Abschnitt „„Persistenzdienstzugriffe““).
Prüfen Sie in der initialize-Methode für jede Initialisierung mit der Methode getFromSchema(), ob diese erforderlich ist. Verwenden Sie das Schema der Quellversion, um dies zu prüfen.
Wenn Sie ohne Bedingung komplexe Berechnungen in der initialize-Methode durchführen, werden diese bei jeder Datenmodelländerung ausgeführt und können die Laufzeit zukünftiger Datenmodelländerungen negativ beeinflussen.
Beispiel
Für die Init-Methode der Spalte „myUnit“ wird der Vorschlagswert in der initialize-Methode berechnet. Der Vorschlagswert wird nur dann benötigt, wenn die Spalte „myUnit“ in der Quellversion nicht vorhanden ist.
byte[] myUnitDefault;
void initialize() {
if (getFromSchema().getColumn(“myUnit”)==null) {
myUnitDefault=…
}
}
@override
@Update(method=DATABASE)
byte[] init$myUnit$guid(CisGenericObject object) {
return myUnitDefault;
}
5.1.3 Datenaktualisierung
Es gibt Fälle, wo Update-Programme, die sich immer auf ein Business Object beziehen, nicht ausreichend sind. Dann wird eine spezielle Anwendung vom Typ Datenaktualisierung benötigt, die Daten korrigiert bzw. migriert. Solche Fälle können sein:
- Es werden sehr komplexe Schemaänderungen bei mehreren Business Objects vorgenommen, wo die Daten logisch voneinander abhängen. Eine Konvertierung der Daten eines Business Objects setzt einen definierten Stand aller beteiligten Entwicklungsobjekte voraus, um korrekte Werte zu ermitteln. Man wird in solchen Fällen erst die Schemaänderungen aktivieren und vorher gegebenenfalls für einzelne Business Objects Update-Programme schreiben. Danach führt man die Datenaktualisierung auf dem definierten Klassenstand aus.
- Sollen bereits implementierte Logikfunktionen für die Konvertierung von Daten genutzt werden, muss das in einer Datenaktualisierung geschehen, da in Update-Programmen keine externen Logikklassen benutzt werden dürfen.
- Es müssen reine Logikfehler korrigiert werden und es gab keine Schemaänderungen. Update-Programme werden nur bei Schemaänderungen ausgeführt.
- Der Benutzer muss für die Konvertierung noch manuell Daten erfassen.
Nachteile
Man sollte möglichst immer Update-Programme nutzen, um Daten zu korrigieren, da diese automatisch ausgeführt werden, wenn eine neue Version des Business Objects aktiviert wird. Dabei ist die richtige Reihenfolge bei mehreren Versionsschritten gesichert. Dagegen kann es nötig sein, bestimmte Datenaktualisierungen nach der Installation der Softwareaktualisierung manuell auszuführen. Es besteht die Gefahr des Vergessens, was dazu führen würde, dass Anwendungen mit den nicht korrigierten Daten arbeiten und schlimmstenfalls unbrauchbare Daten erzeugen. Da eine Datenaktualisierung einen definierten Stand voraussetzt, kann es sein, dass diese zu einem späteren Zeitpunkt nicht mehr ausführbar ist, da noch weitere Softwareaktualisierungen eingespielt wurden und dadurch die von der Datenaktualisierung betroffenden Business Objects in anderen Versionen vorliegen. Datenaktualisierungen, die manuelle Eingriff erfordern, bedeuten einen höheren Arbeitsaufwand für den Partner, da er diesen Arbeitsvorgang in jedem System, in dem die Datenaktualisierung eingespielt wurde, ausführen muss.
Datenaktualisierungstypen
Es gibt verschiedene Typen von Datenaktualisierungen, die sich in der Art und Weise der Ausführung unterscheiden und dadurch spezielle Vor- und Nachteile besitzen. Es gibt folgende Typen:
- Dialog-Datenaktualisierung
- Datenaktualisierung, die automatisch bei der Installation der Softwareaktualisierung ausgeführt wird
- Datenaktualisierung, die im Hintergrund ausgeführt wird
Für weitere Informationen wird auf die Dokumentation „Datenaktualisierung schreiben“ verwiesen.
5.2 Lesezugriffe mit dem Objektmanager
Der Objektmanager (Klasse com.cisag.pgm.appserver.CisObjectManager) stellt dem Entwickler die Methoden getObject(), getObjectArray(), getObjectIterator() und getResultSet() für Lesezugriffe zur Verfügung[1]. Er kann am aktuellen Environment abgefragt werden:
CisEnvironment env= CisEnvironment.getInstance();
CisObjectManager om= env.getObjectManager();
5.2.1 Methode getObject()
Die Methode getObject() liest genau eine Business-Object-Instanz zu einem gegebenen Persistenzdienstschlüssel. Gelesen wird immer die gesamte Instanz mit allen Attributen. Die Erzeugung des zur Identifikation der zu ladenden Instanz benötigten Persistenzdienstschlüssels erfolgt mithilfe der entsprechenden statischen Methode der Business-Object-Klasse:
- Zur Generierung des Persistenzdienstschlüssels aus dem Primärschlüssel des Business Objects dient die Methode buildPrimaryKey().
- Zur Generierung des Persistenzdienstschlüssels aus dem fachlichen Schlüssel oder einem anderen Sekundärschlüssel des Business Objects dient die Methode ..Key(), wobei „…“ für den Namen des übergebenen Schlüssels steht.
Die Signatur der Methode ist:
public <T extends CisObject> <T> getObject(byte[] key, int flags)
Der Parameter key ist der generierte Persistenzdienstschlüssel. Der Parameter flags gibt das Zugriffsverhalten an.
Arbeitsweise von getObject()
Als erstes generiert der Entwickler den Persistenzdienstschlüssel über die entsprechende statische Methode build…Key() der Business-Object-Klasse. Dann erfolgt der Aufruf der Methode getObject() am Objektmanager mit dem Persistenzdienstschlüssel und den Zugriffsflags als Parameter. Der Objektmanager versucht, die Business-Object-Instanz aus dem Transaction Cache zu lesen. Wurde sie dort nicht gefunden, wird im Shared Cache gesucht. Wurde sie auch dort nicht gefunden, erfolgt der Zugriff auf die Datenbank. Dabei wird die folgende SQL-Abfrage abgesetzt, die eine oder keine Zeile als Ergebnis liefert:
SELECT * FROM table WHERE keyAttributes=’?’
Der Objektmanager erzeugt eine Instanz der Business-Object-Klasse, mappt das Anfrageergebnis auf die Attribute und liefert die Instanz als CisObject an die Anwendung zurück. Dort hat ein Type-Cast von der allgemeinen Klasse CisObject auf die konkrete Klasse des gelesenen Business Objects zu erfolgen. Wurde kein Ergebnis von der Datenbank geliefert, wird null zurückgegeben.
Beispiel
Der nachstehende Quelltextauszug lädt eine Instanz des Business Objects Item über den Primärschlüssel:
byte[] guid = …;
byte[] primKey = Item.buildPrimaryKey( guid );
Item item = om.getObject(primKey, CisObjectManager.READ);
Der nächste Quelltextauszug lädt eine Instanz des Business Objects Item über den fachlichen Schlüssel number:
String number = …;
byte[] busKey = Item.buildByNumberKey( number );
CisObject obj = om.getObject(busKey, CisObjectManager.READ);
Item item = (Item) obj;
Ist das Business Object zeitabhängig und es soll eine andere Version als die aktive geladen werden, benutzt man die Methode buildTimeDependentKey(). Als Parameter werden die Primärschlüsselattribute und das validFrom-Attribut übergeben:
byte[] guid = …;
Date validFrom = …;
byte[] primKey = Item.buildTimeDependentKey(guid, validFrom);
Item item = om.getObject(primKey, CisObjectManager.READ);
5.2.2 Methode getObjectArray()
Die Methode getObjectArray() liest mehrere Instanzen eines Business Objects zu gegebenen Schlüsseln. Es wird immer die gesamte Instanz mit allen Attributen gelesen. Die Erzeugung der benötigten Persistenzdienstschlüssel erfolgt mithilfe der entsprechenden statischen Methode der Business-Object-Klasse, beispielsweise buildPrimaryKey(), wenn der Primärschlüssel verwendet wird. Es können alle eindeutigen Schlüssel des Business Objects verwendet werden.
Die Signaturen der Methode sind:
CisObject[] getObjectArray(byte[][] persKeys, int flags)
CisObject[] getObjectArray(java.util.List persKeys, int flags)
Der Parameter persKeys ist ein Array oder Liste von generierten Persistenzdienstschlüsseln aus den Schlüsselwerten der zu ladenen Business-Object-Instanzen. Der Parameter flags gibt das Zugriffsverhalten an.
Arbeitsweise von getObjectArray()
Als erstes müssen die Persistenzdienstschlüssel aus den Primärschlüsseln der zu ladenen Business-Object-Instanzen mit der statischen Methode buildPrimaryKey() der Business-Object-Klasse generiert werden. Dann erfolgt der Aufruf der Methode getObjectArray() am Objektmanager, die das Byte-Array mit den Persistenzdienstschlüsseln und die Zugriffsflags als Parameter erhält. Der Objektmanager versucht, die Business-Object-Instanzen aus dem Transaction Cache zu lesen. Nach den Instanzen, die nicht gefunden wurden, wird im Shared Cache gesucht. Für den Rest, der auch dort nicht gefunden wurde, erfolgt der Zugriff auf die Datenbank. Dabei wird die folgende SQL-Abfrage abgesetzt:
SELECT * FROM table WHERE primaryKeyAttributes IN (‘?’,…,’?’)
Der Objektmanager erzeugt die Instanzen der Business-Object-Klasse, mappt das Anfrageergebnis auf die Attribute und liefert die Instanzen als CisObject-Array an die Anwendung zurück. Dort hat ein Type-Cast von der allgemeinen Klasse CisObject auf die konkrete Klasse der gelesenen Business Objects zu erfolgen. Wurde zu einem Schlüssel keine Instanz gefunden, so ist die entsprechende Position im CisObject-Array mit null belegt. Wurde kein Ergebnis gefunden, wird ein leeres CisObject-Array zurückgegeben.
Beispiel
Der nachstehende Quelltextauszug lädt Instanzen des Business Objects Item über die Primärschlüssel:
byte[] primKey1 = Item.buildPrimaryKey( guid1 );
…
byte[] primKeyN = Item.buildPrimaryKey( guidN );
byte[][] primaryKeys = new byte[][]{primKey1, …., primKeyN};
CisObject[] objects = om.getObjectArray(primaryKeys, CisTransactionManager.READ);
for (int i=0; i< objects.length;i++) {
// Position in objects entspricht Position in primaryKeys
Item item = (Item) objects[i];
…
}
5.2.3 Methode getObjectList()
Die Methode getObjectList() liest analog zu getObjectArray() mehrere Instanzen eines Business Objects zu gegebenen Schlüsseln. Die Funktion von getObjectList() und getObjectArray() ist identisch, getObjectList() gibt jedoch eine Liste mit den Business Objects zurück, während getObjectArray() ein Array zurückgibt. Die Größe der Liste entspricht der Anzahl der übergebenen Schlüssel.
Die Signatur der Methode ist:
<T extends CisObject> List<T> getObjectList(CisList persKeys, int flags)
Der Datentyp „List“ als Rückgabe erlaubt einen einfachen Typ-Cast auf eine Liste mit den tatsächlich geladenen Business Objects.
Beispiel
Der nachstehende Quelltextauszug lädt Instanzen des Business Objects „Item“ über die Primärschlüssel:
CisList primaryKeys = new CisArrayList();
primaryKeys.add(Item.buildPrimaryKey( guid1 ));
…
primaryKeys.add(Item.buildPrimaryKey( guidN ));
List<Item> items = om.getObjectArray(primaryKeys,
CisTransactionManager.READ);
for (Item item : items) {
//Position in items entspricht Position in primaryKeys
…
}
5.2.4 Methode getObjectIterator()
Die Methode getObjectIterator() liest mehrere Instanzen eines Business Objects. Es wird jeweils immer die gesamte Instanz mit allen Attributen gelesen. Der Methode wird ein SELECT-Statement in OQL übergeben, welches die zu ladenen Instanzen beschreibt.
Die Signatur der Methode ist:
<T extends CisObject> CisObjectIterator<T>
getObjectIterator(String oqlString, int flags)
Der Parameter oqlString enthält den OQL-String, der Parameter flags gibt das Zugriffsverhalten an.
Arbeitsweise von getObjectIterator()
Als erstes muss der Iterator (CisObjectIterator) erzeugt werden. Dazu wird die Methode getObjectIterator() am Objektmanager aufgerufen und der OQL-String sowie die Zugriffsflags übergeben. Im OQL-String sind Sub-Selects in der WHERE-Klausel erlaubt. Joins werden nicht unterstützt. Die Methode liefert den Iterator zurück, der in der Anwendung zum Laden der Business-Object-Instanzen benutzt wird. Als nächstes werden die OQL-Parameter gesetzt, welche im OQL-String durch den Platzhalter „?“ angegeben wurden. Der erste Aufruf von hasNext() bzw. next() am Iterator löst den Datenbankzugriff aus. Die Methode hasNext() prüft, ob noch Instanzen geliefert werden können. Die Methode next() liefert die nächste Instanz an die Anwendung. Wird das Business Object gecacht, werden als erstes die Primärschlüsselwerte geladen:
SELECT primkeyAttr FROM table [WHERE …]
Die Erzeugung der Business-Object-Instanzen geschieht mithilfe der intern aufgerufenen getObjectArray()-Methode, die über die Primärschlüssel die Instanzen blockweise lädt. Die Standard-Blockgröße ist 16. Ist eine Business-Object-Instanz im aktuellen Transaction Cache als gelöscht markiert, wird sie weg gefiltert und nicht zurückgegeben. Der Object-Iterator gibt nie null zurück. In der Anwendung hat ein Typ-Cast von der allgemeinen Klasse CisObject auf die konkrete Klasse der gelesenen Business Objects zu erfolgen.
Wird das Business Object nicht gecacht, so werden direkt alle Attribute von der Datenbank gelesen und die Instanzen des Business Objects erzeugt.
Beispiel
Der nachstehende Quelltextauszug lädt die Instanzen des Business Objects Item, denen eine bestimmte Maßeinheit (Business Object UnitOfMeasure) zugeordnet ist. Deren Primärschlüssel vom Typ „guid“ wird als Parameter am Iterator über die setGuid()-Methode gesetzt, welche als Parameter die Position im OQL-Statement und der Wert übergeben werden.
Beispiel mit OQL-Parameter:
try (CisObjectIterator<Item> iter = om.getObjectIterator( “SELECT FROM com.cisag.app.general.obj.Item i WHERE i:uom=?”)) {
iter.setGuid(1, uomGuid);
while (iter.hasNext()) {
Item item = iter.next();
…
}
}
Der nachstehende Quelltextauszug lädt die Instanzen des Business Objects SupplierProposalDetail …
Beispiel mit Sub-Select:
String oql = “SELECT FROM com.cisag.app.purchasing.obj.SupplierProposalDetail spd WHERE spd:deliverySupplier <> (SELECT delInfo:supplierData.supplier FROM com.cisag.app.purchasing.obj.PurchaseOrderDeliveryInfo delInfo WHERE delInfo:header=spd:header AND delInfo:detail=spd:guid)”;
try (CisObjectIterator <SupplierProposalDetail> iter = om.getObjectIterator(oql)){
while (iter.hasNext()) {
SupplierProposalDetail spd = iter.next();
…
}
}
Anmerkung
Die Berechnung des Ergebnisses des OQL-Ausdruckes erfolgt direkt auf den Daten in der Datenbank. Im selben Transaktionskontext geänderte Objekte bleiben dadurch unberücksichtigt, da diese Änderungen noch nicht in der Datenbank stehen. Diese werden erst mit Abschluss der Top-Level-Transaktion persistent. Das bedeutet, dass der Objekt-Iterator eventuell falsche Daten liefern kann, wenn von dessen OQL-Statement auch geänderte Objekte betroffen sind. Neu angelegte Objekte kann er aufgrund des Funktionsprinzips nicht berücksichtigen, gelöschte Objekte kann er anhand des Primärschlüssels herausfiltern.
5.2.5 Methode getResultSet()
Die Methode getResultSet() liest ausgewählte Attribute aus einem oder mehreren Business Objects. Die Ergebnisattribute sind über das SELECT-Statement in OQL beliebig zusammenstellbar. Es werden keine kompletten Business-Object-Instanzen von der Datenbank gelesen. Ein Result-Set belegt exklusiv eine Datenbank–verbindung bis zum expliziten Schließen. Deswegen ist es äußerst wichtig, die Zeitdauer, in der ein Result-Set geöffnet ist, zu minimieren, da die Anzahl der verfügbaren Datenbankverbindungen begrenzt sind. Aufwendige oder langwierige Operationen sollten, soweit möglich, erst nach dem Schließen des Result-Sets ausgeführt werden. Wichtig ist auch, dass geschachtelte Result-Sets jeweils eine eigene Datenbankverbindung verwenden, d.h die Anzahl der verfügbaren Datenbankverbindungen begrenzt die Schachtelungstiefe. Aus diesem Grund sollte die Schachtelungstiefe klein gehalten werden.
Arbeitsweise von getResultSet()
Als erstes muss das Result-Set (CisResultSet) erzeugt werden. Dazu wird die Methode getResultSet() am Objektmanager aufgerufen und der OQL-String übergeben. Im OQL-String sind Joins und Sub-Selects erlaubt. Die Methode liefert das Result-Set zurück, das in der Anwendung zum Laden der Attribute benutzt wird. Als nächstes werden die OQL-Parameter gesetzt, welche im OQL-String durch den Platzhalter „?“ angegeben wurden. Der erste Aufruf von next() am Result-Set löst den Datenbankzugriff aus. Die Methode next() liefert den nächsten Satz von Attributen an die Anwendung. Es wird die folgende SQL-Abfrage abgesetzt:
SELECT attr1[, attr2, …] FROM table1 [JOIN
table2 ON … [JOIN …]] [WHERE …]
Die Anwendung muss die Attribute des gelesenen Satzes vom Result-Set mit der jeweilig richtigen Methode (abhängig vom Typ) abholen. Es ist sicherzustellen, dass das Result-Set mit close() explizit geschlossen wird, um die DB-Connection freizugeben, wenn die Dummy-Transaktion zum Lesen verwendet wird. Bei Aufruf von commit() bzw. rollback() für die aktuelle Transaktion werden alle offenen Result-Sets implizit geschlossen. Da das Result-Set „AutoClosable“ ist, wird das Result-Set, wenn des Result-Set im „try“ zugewiesen wurde, auch ohne „finally“ automatisch beim Verlassen des try-Blocks geschlossen.
Beispiel
Der nachstehende Quelltextauszug lädt nur die Attribute „guid“ (Primärschlüssel) und „number“ (fachlicher Schlüssel) der Instanzen des Business Objects Item, denen eine bestimmte Maßeinheit (Business Object UnitOfMeasure) zugeordnet ist. Deren Primärschlüssel vom Typ „guid“ wird als Parameter am Result-Set über die setGuid()-Methode (passend zum Typ) gesetzt, welcher als Parameter die Position im OQL-Statement und der Wert übergeben werden. Der try-catch-finally-Block stellt sicher, dass das Result-Set geschlossen wird, auch wenn eine Exception auftritt.
byte[] uomGuid = … ;
CisResultSet rs = om.getResultSet(“SELECT i:guid, i:number
FROM com.cisag.app.general.obj.Item i
WHERE i:uom=?”);
try {
rs.setGuid(1, uomGuid);
while (rs.next()) {
//read next
byte[] guid = rs.getGuid(1);
String number = rs.getString(2);
…
}
}
finally {
rs.close();
}
5.2.6 Richtlinien zur Benutzung
In diesem Abschnitt werden einige Richtlinien für die Benutzung der vorgestellten Methoden gegeben, um eine gute Performance zu erreichen.
- Die Methode getObjectArray() (ArrayFetch) ist niemals langsamer als n mal die Methode getObject()auszuführen, aber oftmals schneller. Sie benötigt maximal eine Datenbank-Anfrage im Gegensatz zu maximal n Datenbank-Anfragen. Ein weiterer Vorteil ist, dass das Attribut-Mapping zwischen den OQL-, SQL- und Java-Attributen nur einmal ermittelt werden muss.
- Ist für das zu ladene Business Object das Caching aktiviert, lesen die Methoden getObject(), getObjectArray() und getObjectIterator()immer zuerst im Shared Cache.
Sie fügen die von der Datenbank geladenen Instanzen auch in den Cache ein. Für häufig benutzte Business Objects (z.B. Stammdaten) sind diese Methoden zweckmäßig, da sie immer den Cache beim Zugriff beachten. Ohne Verwendung entsprechender Zugriffs-Flags sind sie hingegen schlecht geeignet für Massendatenoperationen auf Bewegungsdaten (z.B. Verfügbarkeitsabfrage über Artikel), da diese möglicherweise andere häufig benutzte Daten aus dem Shared Cache verdrängen, obwohl die neu eingelagerten Daten kaum wieder gebraucht werden. In solchen Fällen sollten die Daten mit dem Zugriffs-Flag IGNORE_SHARED_CACHE gelesen werden. Die Erzeugung vieler nur temporär benötigter Java-Objekte kostet zudem viel Zeit und Hauptspeicherplatz.
Mit der Methode getResultSet() ist es möglich, gezielt nur die Attribute zu laden, die man benötigt und nicht die ganze Business-Object-Instanz mit vielleicht dutzenden Attributen. Das hält den Overhead klein, auch weil weniger Objekte in Java angelegt werden. Bei der Verwendung von getResultSet() hat der Entwickler eine entschieden höhere Verantwortung, da ein offenes Result-Set exklusiv eine Datenbankverbindung belegt. Weiterhin muss sich selbst um das Mapping von den Result-Set-Ergebnissen auf die Java-Attribute kümmern.
5.2.7 Laden über Beziehungen
Sind bei einem Business Object Beziehungen definiert, können die korrespondierenden Instanzen über die Beziehungsmethoden retrieve…() geladen werden. Der Lesezugriff über den Objektmanager (getObject() bzw. getObjectIterator()) erfolgt für Entwickler transparent. Das Ergebnis einer “1 zu 1”-Beziehung ist null, wenn eines der Beziehungsattribute null ist. Das Ergebnis der “1 zu n”-Beziehung ist nicht sortiert. Intern wird ein OQL-Statement benutzt (siehe „Methode getObjectIterator()“).
Beispiel – Laden über „ zu 1“-Beziehung:
Der folgende Quelltextauszug zeigt das Laden der korrespondierenden Instanz des Business Objects Country über die Beziehungsmethode retrieveCountry() des Business Objects Region.
Region region = (Region)om.getObject(primKey);
Country country = region.retrieveCountry();
Beispiel – Laden über „1 zu n“-Beziehung:
Der folgende Quelltextauszug zeigt das Laden der korrespondierenden Instanzen des Business Objects Region über die Beziehungsmethode retrieveRegions() des Business Objects Country ohne Zuweisung im try.
CisObjectIterator<Region> iter = country.retrieveRegions();
try {
while (iter.hasNext()) {
Region region = iter.next();
}
} finally {
iter.close();
}
Das gleiche Beispiel mit Zuweisung im try:
try (CisObjectIterator<Region> iter = country.retrieveRegi-ons()) {
while (iter.hasNext()) {
Region region = iter.next();
}
}
5.3 Schreibzugriffe mit dem Objektmanager
Um eine Business-Object-Instanz zu ändern oder zu löschen, gibt es am Objektmanager die Methoden putObject()und deleteObject().
5.3.1 Methode putObject()
Die Methode putObject() registriert eine Business-Object-Instanz zum Speichern in der Datenbank. Die Instanz wird im Transaction Cache der aktuellen Transaktion abgelegt und erst beim commit() der Transaktion persistent.
Um eine Instanz eines Business Objects anzulegen oder zu ändern, muss zuerst eine Top-Level-Transaktion geöffnet werden. Dann wird die Instanz mit getObject() unter Angabe des gewünschten Zugriffsmodus geladen. Der Zugriffsmodus READ_UPDATE liefert Instanzen, die beliebig geändert oder gelöscht werden dürfen. Der Zugriffsmodus READ_WRITE verhält sich wie READ_UPDATE. Aber: Existiert die Business-Object-Instanz, auf welches zugegriffen werden soll, nicht, wird es neu angelegt. READ_UPDATE würde in diesem Fall null zurück liefern. Die gelesene Business-Object-Instanz ist für andere Transaktionen durch den Persistenzdienst gesperrt. Will eine andere Transaktion dieselbe Business-Object-Instanz lesen, kann es nach einer gewissen Wartezeit zu einem Timeout kommen. Die Änderungen an den Attributen erfolgen über die set…()-Methoden des Business Objects. Der Aufruf von putObject() mit der Business-Object-Instanz als Parameter übernimmt die Änderungen in den aktuellen Transaction Cache. Ein erneutes getObject() innerhalb der Transaktion liefert nun die geänderte Business-Object-Instanz. Beim commit() der Transaktion werden nur die mit putObject() registrierten Änderungen berücksichtigt. Die Ausführung der Datenbankoperation, um die registrierten Änderungen persistent auf der Datenbank zu machen, erfolgt erst beim commit() der Top-Level-Transaktion. Tritt vor oder während des commit() der aktuellen Transaktion ein Fehler auf, muss ein explizites rollback() aufgerufen werden, wenn die Transaktion mit beginNew() und nicht mit createNew() geöffnet wurde.
Beispiel:
Der nachstehende Quelltextauszug lädt eine Instanz des Business Objects UnitOfMeasure von der Datenbank oder legt eine neue Instanz an mit beginNew().
tm.beginNew();
try {
byte[] key = UnitOfMeasure.buildByCodeKey(code);
// Laden der Instanz zum Ändern/Anlegen
UnitOfMeasure unitOfMeasure = (UnitOfMeasure)
om.getObject(key, CisObjectManager.READ_WRITE);
// Setzen der Attribute
unitOfMeasure.setDescription(description);
…
// Registrieren der Änderungen im Transaction-Cache
om.putObject(unitOfMeasure);
// Beenden der Transaktion, Schreiben der Änderungen
tm.commit();
} catch (RuntimeException e) {
// explizites Rollback bei Fehler
tm.rollback();
throw e;
}
Das gleiche Beispiel jedoch mit createNew().
try (CisTransaction txn =tm.createNew()) {
byte[] key = UnitOfMeasure.buildByCode- Key(code);
//Laden der Instanz zum Ändern/Anlegen
UnitOfMeasure unitOfMeasure = (UnitOfMeasure) om.getObject(key, CisObjectMana-ger.READ_WRITE);
//Setzen der Attribute unitOfMeasure.setDescription(description); …
//Registrieren der Änderungen im Transaction-Cache
om.putObject(unitOfMeasure);
//Beenden der Transaktion, Schreiben der Änderungen
txn.commit();
}
5.3.2 Methode deleteObject()
Die Methode deleteObject() löscht eine Business-Object-Instanz von der Datenbank.
Um ein Instanz zu löschen, muss zuerst eine Top-Level-Transaktion geöffnet werden. Dann wird das zu ändernde Business Object mit getObject() unter Angabe des Zugriffsmodus READ_UPDATE geladen. Der Aufruf von deleteObject() mit der Business-Object-Instanz als Parameter markiert die Instanz im aktuellen Transaktion Cache zum Löschen. Die Ausführung der Datenbankoperation erfolgt erst beim commit() der Top-Level-Transaktion. Tritt vor oder während des commit() der aktuellen Transaktion ein Fehler auf, muss ein explizites rollback() aufgerufen werden, wenn die Transaktion mit beginNew() und nicht mit createNew() geöffnet wurde.
Beispiel:
Der nachstehende Quelltextauszug lädt eine Instanz des Business Objects UnitOfMeasure von der Datenbank. Wird die Instanz nicht gefunden, wird null zurück geliefert. Eine gefundene Instanz wird zum Löschen markiert. Beim commit() wird die Instanz gelöscht.
byte[] transGuid= tm.beginNew();
try {
byte[] key = UnitOfMeasure.buildByCodeKey(code);
//Laden der Instanz zum Ändern/Anlegen
UnitOfMeasure unitOfMeasure = (UnitOfMeasure) om.getObject(key,
CisObjectManager.READ_UPDATE);
if (unitOfMeasure==null) {
//nicht gefunden
…
} else {
// Markieren zum Löschen
om.deleteObject(unitOfMeasure);
}
// Beenden der Transaktion, Schreiben der Änderungen
tm.commit(transGuid);
} catch (RuntimeException e) {
// explizites Rollback bei Fehler
tm.rollback(transGuid);
throw e;
}
Das gleiche Beispiel jedoch mit createNew().
try (CisTransaction txn =tm.createNew()) {
byte[] key = UnitOfMeasure.buildByCode- Key(code);
//Laden der Instanz zum Ändern/Anlegen UnitOfMeasure
unitOfMeasure = (UnitOfMeasure) om.getObject(key, CisObjectMan-ager.READ_UPDATE);
if (unitOfMeasure==null) {
//nicht gefunden …
} else {
//Markieren zum Löschen
om.deleteObject(unitOfMeasure);
} //Beenden der Transaktion, Schreiben der Änderungen
txn.commit(transGuid);
}
5.4 Fehlerbehandlung bei Transaktionen
Es muss sichergestellt sein, dass eine mit begin() oder beginNew() geöffnete Transaktion immer mit rollback() abgebrochen oder mit commit() beendet wird. Deswegen ist es unbedingt erforderlich, innerhalb des Transaktionsblockes auftretende Exceptions zu behandeln. Außerdem muss an bestimmten Programmstellen das mögliche Auftreten von Exceptions vermieden werden. Bei geschachtelten Transaktionen muss man aufpassen, das der Transaktions-Stack nicht durcheinander kommt, z.B. wenn aus versehen die übergeordnete Transaktion ebenfalls geschlossen wird. Um die Sicherheit zu erhöhen, dass das Rollback bzw. Commit sich auf die richtige Transaktion bezieht, Kann man die Transaktionsguid benutzen. beginNew()/begin() gibt als Rückgabewert die GUID der aktuellen Transaktion zurück, die man bei rollback() und commit() als Parameter übergeben kann. Stimmt diese bei commit() nicht mit der aktuellen Transaktion überein, kommt es zu einer Exception und es wird nicht die falsche Transaktion persistent gemacht. Bei rollback() wird nur eine Warnung ausgegeben, dass die Transaktionguids nicht übereinstimmen. Jede Top-Level-Transaktion hat eine eigene Transaktionsguid während Sub-Level-Transaktionen die Transaktionsguid der Top-Level-Transaktion liefern. Die Verwendung der Transaktionssguid schützt momentan vor dem unbeabsichtigten Schliessen der falschen Top-Level-Transaktion, aber bei den Subtranskationen sind solche Fehler noch möglich. In einem späteren Release kann dieser Schutzmechanismus auch auf die Subtransaktionen ausgedehnt werden, so dass jede Subtransaktionen eine eigene Transaktionsguid besitzt. Deswegen sollte man auch für die Subtransaktionen die Transaktionsguid verwenden. Transaktionen, die mit create() oder createNew() im „try“ als „AutoClosable“ geöffnet wurden, werden automatisch beim Verlassen des try-Blocks geschlossen. Wenn eine Transaktion als „AutoClosable“ verwendet wird, dann besteht keine Möglichkeit für eine fehlerhafte Verwendung, da das ordnungsgemäße Schließen der Transaktion durch Java sichergestellt wird.
5.4.1 Lesende Transaktion mit begin
Eine lesende Transaktion, die mit begin() oder beginNew() geöffnet wurde, hat immer der nachstehenden Vorlage zu entsprechen. Dabei darf an den mit /*xx*/ markierten Stellen keine Exception ausgelöst werden. Das heißt, dass an diesen Stellen am besten keine Code steht oder nur Anweisungen, die keine Exception auslösen können. Eine ausschließlich lesende Transaktion sollte immer mit rollback() zurückgesetzt werden, da keine Änderungen auf der Datenbank vorgenommen werden. Der try-finally-Block sorgt dafür, dass auf jeden Fall rollback() ausgeführt wird, egal ob innerhalb des Transaktionsblockes eine Exception auftritt oder nicht.
Vorlage für eine lesende Transaktion:
byte[] transGuid= tm.begin…(..);
/*xx*/
try {
…
} finally {
/*xx*/
tm.rollback(transGuid);
…
}
Eine Exception an den markierten Stellen würde dazu führen, dass die Transaktion nicht durch das rollback() zurückgesetzt wird. Die Transaktion bliebe also offen und würde erst beim nächsten auftretenden, für eine andere Transaktion bestimmten commit() oder rollback() geschlossen werden. Der Transaktions-Stack wäre nicht mehr konsistent, das hätte Datenverlust zur Folge.
5.4.2 Lesende Transaktion mit create
Eine lesende Transaktion, die mit create oder createNew geöffnet wurde, hat immer der nachstehenden Vorlage zu entsprechen. Verwenden Sie die erzeugte Transaktion unbedingt im „try“ als „AutoClosable“, da ansonsten der Transaktions-Stack inkonsistent werden kann.
Vorlage für eine lesende Transaktion:
try (CisTransaction txn=tm.create…(..)) {
…
}
5.4.3 Schreibende Transaktion mit begin
Eine schreibende Transaktion, die mit begin() oder beginNew() geöffnet wurde, hat immer der nachstehenden Vorlage zu entsprechen. Dabei darf an den mit /*xx*/ markierten Stellen keine Exception ausgelöst werden. Das heißt, dass an diesen Stellen am besten keine Code steht oder nur Anweisungen, die keine Exception auslösen können. Eine schreibende Transaktion muss entweder mit rollback() abgebrochen oder mit commit() erfolgreich beendet werden. Im Falle eines Abbruches werden alle in der Transaktion bzw. in Subtransaktionen gemachten Änderungen verworfen. Es sind lesende und ändernde Zugriffe über den Persistenzdienst erlaubt. Der try-catch-Block sorgt dafür, dass bei einer Exception im Transaktionsblock die Transaktion explizit mit rollback() abgebrochen wird. Ansonsten wird die Transaktion mit commit() erfolgreich beendet.
Vorlage für eine schreibende Transaktion:
byte[] transGuid= tm.begin…(..);
/*xx1*/
try {
…
tm.commit(transGuid);
/*xx2*/
} catch (RuntimeException ex) {
/*xx3*/
tm.rollback(transGuid);
…
}
Eine Exception an der Stelle /*xx1*/ würde dazu führen, dass die geöffnete Transaktion nicht über eine zum Block gehörende Anweisung geschlossen wird. Eine Exception an der Stelle /*xx2*/ würde dazu führen, dass das rollback() im catch-Block ausgeführt wird, welches die übergeordnete Transaktion abbrechen würde. Die aktuelle Transaktion wurde ja schon mit commit() geschlossen. Eine Exception an der Stelle /*xx3*/ führt dazu, dass das rollback() nicht ausgeführt wird und die Transaktion offen bleibt.
5.4.4 Schreibende Transaktion mit create
Eine schreibende Transaktion, die mit create oder createNew geöffnet wurde, hat immer der nachstehen-den Vorlage zu entsprechen. Dabei darf an den mit /*xx*/ markierten Stellen kein Zugriff auf den Objekt-Manager erfolgen. Nach dem commit sollte daher am besten kein Code stehen.
Vorlage für eine schreibende Transaktion:
try (CisTransaction txn=tm.create…(..)) {
…
txn.commit();
/*xx*/
}
Die mit create geöffnete Transaktion ist an der Stelle /*xx*/ bereits geschlossen. Der Objekt-Manager verwendet an der Stelle
/* xx */ nicht die im „try“ geöffnete Transaktion.
5.4.5 Blockweise schreibende Transaktion mit create
Eine Transaktion sollte immer eine begrenzte Größe haben. Wenn eine unbegrenzte Menge von Daten geschrieben werden soll, dann sollten diese Daten in Blöcken mit begrenzter Größe geschrieben werden. Die Blockgröße kann entweder konstant vorgegeben werden (z. B. 100 Datensätze pro Transaktion) oder aber dynamisch durch den Transaktions-Manager berechnet werden. Dabei darf an den mit /*xx*/ markierten Stellen kein Zugriff auf den Objekt-Manager erfolgen. Nach dem commit sollte daher am besten kein Code stehen.
Vorlage für eine Transaktion mit festen Blockgrößen mit commitBlock():
try (CisTransaction txn=tm.create…(..)) {
int i=0;
while (…) {
…
Om.putObject(o);
if (++i%100=0) {
txn.commitBlock();
}
}
txn.commit();
/*xx*/
}
Vorlage für eine Transaktion mit dynamischer Blockgröße mit commitIfSizeLimitExceeded:
try (CisTransaction txn=tm.create…(..)) {
int i=0;
while (…) {
…
Om.putObject(o);
txn.commitIfSizeLimitExceeded();
}
txn.commit();
/*xx*/
}
Die mit create geöffnete Transaktion ist an der Stelle /*xx*/ bereits geschlossen. Der Objekt-Manager verwendet an der Stelle
/*xx */ nicht die im „try“ geöffnete Transaktion.
5.5 Persistente und transiente Business Objects
Eine Business-Objekt-Instanz besitzt zwei orthogonale Zustände bezüglich der Datenbank und der Zugehörigkeit zu einer Transaktion. Die Zustände können mit denn Methode is_persistent() und is_transient() abgefragt werden.
Zur Feststellung, ob eine Business-Object-Instanz in der Datenbank gespeichert ist, dient die Methode is_persistent(). Liefert sie true, ist die Instanz in der Datenbank vorhanden, bei false nicht. Mit der Methode is_transient() kann festgestellt werden, ob eine Business-Object-Instanz einen Transaktionskontext hat, dass heißt, ist die Instanz im Transaction-Cache der aktuellen Transaktion gespeichert oder nicht. Liefert sie false, hat die Business-Object-Instanz einen Transaktionskontext und ist nur in der aktuellen Transaktion gültig, bei true ist die Instanz nicht an eine Transaktion gebunden. Für neue erzeugte Instanzen, die zwar mit putObject() zum Speichern registriert, aber noch nicht in die Datenbank geschrieben wurden (Top-level-Transaktion wurde noch nicht „commited“), liefert die Methode is_persistent() false. Ob eine neu erzeugte Instanz bereits zum Speichern registriert wurde, lässt sich mit is_newObject() ermitteln. Diese Methode liefert genau dann true, wenn das Objekt weder persistent ist noch mit putObject() registriert wurde.
Beim Arbeiten mit Business Objects müssen diese Flags beachtet werden, um ERP-System-konforme Anwendungen zu entwickeln. Eine nicht transiente Business-Object-Instanz, die z.B. mit getObject(), getObjectArray() oder getObjectIterator() geladen wurde, ist immer an die aktuelle Transaktion gebunden und somit nach dem Ende der Transaktion ungültig. Handelte es sich bei der Transaktion um eine Subtransaktion, werden die nicht transienten Instanzen in den Transaktionskontext der Parent-Transaktion aufgenommen und können dort weiter benutzt werden. Außerhalb der Transaktion kann eine weitere Benutzung der Instanz zu Programmfehlern führen und ist deshalb verboten.
Die Methoden putObject() und deleteObject() erwarten immer nicht transiente Business-Object-Instanzen als Parameter, die über die Methoden getObject(), getObjectIterator() und getObjectArray() unter Angabe der entsprechenden Zugriffs-Flags geladen bzw. erzeugt wurden.
Methode getTransientCopy()
Wurde ein Business-Object-Instanz mit getObject() unter Angabe des Flags READ, oder READ_UPDATE oder READ_WRITE geladen (die zu ladende Instanz existiert auf der Datenbank), so liefert is_persistent() true und is_transient() false. Um die Instanz transaktionsübergreifend zu verwenden, muss mit der Methode getTransientCopy() der Business-Object-Klasse eine neue transiente Kopie erzeugt werden. Neben den Attributwerten wird das persistent-Flag ebenfalls kopiert, das transient-Flag wird auf true gesetzt. Die so erzeugte transiente Business-Object-Instanz ist zu keiner Transaktion gehörig und kann transaktionsübergreifend verwendet werden. Diese Methode ist sehr nützlich, wenn man sich den Inhalt einer Business-Object-Instanz über das Transaktionsende hinaus merken will, z.B. für die GUI-Anzeige.
Methode newTransientInstance()
Die Methode newTransientInstance() einer Business-Object-Klasse erzeugt eine leere transiente, nicht persistente Business-Object-Instanz (is_persistent() liefert false, is_transient() true).
Methode copyTo()
Die Methode copyTo() der Business-Object-Klasse kann benutzt werden, um die Attributwerte einer Instanz in eine andere Instanz der gleichen Business Objects zu kopieren. Die Quelle bzw. das Ziel können beliebig persistente oder transiente Instanzen sein. copyTo() überträgt auf jeden Fall immer die Attributwerte und den Business Key der Quelle zum Ziel. In den folgenden Fällen werden Sonderbehandlungen vorgenommen:
Ist das Ziel eine transiente Instanz, werden zusätzlich das Persistenz-Flag und der Primärschlüssel von der Quelle mit kopiert.
Ist das Ziel eine nicht persistente Instanz wird der Primärschlüssel der Quelle kopiert.
Ist das Ziel transient und ist für die Business-Object-Klasse die Updateinformation erforderlich, wird die gesamte Updateinformation aus der Quelle mit kopiert. Die fachliche Löschmarkierung der Quelle (delete-Flag) wird immer ins Ziel kopiert.
Ist das Ziel transient und zeitabhängig werden die Attribute validFrom und validUntil der Quelle mit kopiert.
Allgemein lässt sich sagen: Auf ein nicht transientes, persistentes Ziel wird kein Attribut oder Flag kopiert, was den Transaktionskontext des Business Objects ändern könnte. Auf ein transientes Ziel wird alles kopiert.
Die folgende Abbildung fasst die möglichen Kombinationen des transient- und persisent-Flags einer Business-Object-Instanz zusammen und gibt die möglichen Methoden an, um Daten zwischen nicht transienten und transienten Instanzen auszutauschen. Die Methode getObject() steht hier stellvertretend für die Methoden getObjectArray() und getObjectIterator(), die intern auf getObject() zurückgreifen.
5.6 Logische Sperren
Logische Sperren dienen der Synchronisation von laufenden Anwendungen, die auf denselben Business-Object-Instanzen arbeiten. Sie haben nichts mit Sperren zu tun, die von Transaktionen auf Business-Object-Instanzen gesetzt werden. Logische Sperren müssen vom Anwendungsentwickler vorgesehen werden.
Normalerweise arbeiten Dialoganwendungen folgendermaßen:
- Die vom Benutzer gewählte Business-Object-Instanz wird über eine lesende Transaktion geladen.
- Der Benutzer ändert die Daten.
- Die Instanz wird über eine schreibende Transaktion gespeichert.
Während der eine Benutzer die Daten ändert, kann ein zweiter Benutzer dieselbe Business-Object-Instanz ebenfalls laden, bearbeiten und etwas eher speichern. Dann würde der erste Benutzer beim Speichern seiner geänderten Instanz die Änderungen des zweiten Nutzers überschreiben, da er noch die alten Daten geladen hatte. Dieses Verhalten ist natürlich nicht wünschenswert. Zur Lösung dieses Konfliktes gibt es zwei Ansätze, das optimistische und das pessimistische Sperren.
5.6.1 Optimistisches Sperren
Für viele Anwendungsfälle reicht eine optimistische Sperre. Dabei wird die zu ändernde Business-Object-Instanz ohne Setzen einer Sperre geladen. Vor dem Speichern wird geprüft, ob die Business-Object-Instanz seit dem letzten Laden auf der Datenbank geändert wurde. Ist das nicht der Fall, wird die Business-Object-Instanz gespeichert. Ansonsten wird normalerweise die folgende Meldung für den Benutzer ausgegeben und die Instanz wird nicht gespeichert:
„Daten wurden zwischenzeitlich von … geändert. Bitte Daten aktualisieren und Änderungen erneut vornehmen.“
Dieses Vorgehen ist sinnvoll, wenn es Daten mit wenig Änderungsaufkommen, wie z. B. Stammdaten, betrifft. Denn dann ist nicht sehr wahrscheinlich, dass dieselbe Business-Object-Instanz von mehreren Benutzern zur gleichen Zeit geändert wird.
5.6.2 Pessimistisches Sperren
Bei komplexen fachlichen Größen, wie z. B. Aufträgen, ist dagegen sinnvoll, möglichst frühzeitig gegen konkurrierende Änderungen zu sperren, da man es dem Benutzer nicht zumuten kann, die Eingaben zu wiederholen. Im ERP-System werden die pessimistischen Sperren über sogenannte Locks realisiert. Diese von Anwendungen gesetzten Locks werden zentral über den Application-Manager verwaltet. Ein Lock wird durch eine Zeichenkette repräsentiert, mit der die Anwendung eine Ressource bezeichnet. Eine Anwendung fordert so eine Sperre mit dem entsprechenden Lock-String beim Application-Manager an. Existiert schon eine Sperre unter diesem Namen, kann der Lock nicht gesetzt werden. Existiert noch keine Sperre unter diesem Namen, wird der Lock-String in die Sperrliste eingetragen und damit der Lock gesetzt. Ab diesem Zeitpunkt kann keine andere Anwendung mit dem gleichen Lock-String eine Sperre vom Application-Manager erhalten. Die Anwendung muss den gesetzten Lock explizit wieder freigeben. Implizit werden alle gesetzten Locks beim Beenden der zugehörigen Session (z .B. Schließen des Browsers) freigegeben. Benutzen mehrere Anwendungen gleiche Lock-Strings für Ressourcen, müssen diese sicherstellen, dass sie dieselbe Ressource meinen. Es sollte deshalb möglichst immer ein eindeutiger String benutzt werden, z.B. durch Hinzufügen eines Schlüsselwertes. Auch sollte immer die GUID der Datenbank, auf der die Business-Object-Instanz geändert wird, mit in den Lock-String aufgenommen werden, um die Eindeutigkeit sicherzustellen, wenn das Business Object auf mehreren Datenbanken existiert.
Um die zentrale Lockverwaltung in der Anwendung zu nutzen, muss zuerst über das Environment der Session der Application-Manager[2] abgefragt werden. Das geschieht über:
CisEnvironment env = CisEnvironment.getInstance();
CisApplicationManager am = env.getApplicationManager();
Der nachstehende Quelltextauszug zeigt die Anforderung eines Locks beim Application-Manager. Es soll ein Lock für eine Instanz des Business Objects PriceList gesetzt werden. Der Lock-String wird dazu aus dem Namen der Business-Object-Klasse und dem Primärschlüssel der Instanz gebildet. Es wird nicht auf Freigabe gewartet, falls der Lock schon gesetzt ist.
//eindeutigen Lock-String erzeugen
String lockString = PriceList.class.getName() + Guid.toHexString(priceListGuid) ;
//Datenbank-Information der aktiven OLTP-DB hinzufügen
lockString = tm.buildDatabaseLock(lockString);
//Sperre anfordern
boolean locked = am.acquireLock(lockString);
if (locked) {
//lock success
…
} else {
//already locked
//User-Information der schon gehaltenen Sperre ermitteln
String user = am.getLockInformation(lockString);
…
}
Die Freigabe des Locks erfolgt in der Anwendung an geeigneter Stelle mit:
am.releaseLock(lockString);
Methode acquireLock()
Mit der Methode acquireLock() am Application Manager können pessimistische Sperren gesetzt werden. Die Methode hat mehrere Signaturen, über die ein oder mehrere Locks gesetzt werden können:
public boolean acquireLock(String value);
public boolean acquireLock(String values[]);
public boolean acquireLock(String value, long timeout);
public boolean acquireLock(String[] values,long timeout);
Über den Parameter timeout ist die maximale Wartezeit angebbar, die auf die Freigabe schon gesetzter Locks gewartet werden soll. Es wird true zurückgegeben, wenn der Lock gesetzt werden konnte, sonst false. Bei Anforderung von Locks für mehrere Ressourcen wird nur true zurückgegeben, wenn alle Locks angefordert werden konnten. Es können durchaus ein Teil der Ressourcen gesperrt worden sein, deshalb müssen diese explizit wieder einzeln freigegeben werden.
Methode releaseLock()
Die Methode releaseLock() gibt einen gegebenenfalls gesetzten Lock wieder frei. Der angegebene Lock-String wird aus der Lock-Tabelle des Application-Managers entfernt. Die Methode hat nachstehende Signatur:
public void releaseLock(String value);
Methode getLockInformation()
Die Methode ermittelt den Nutzernamen für einen bestehenden Lock. Sie gibt diesen Lock implizit wieder frei. Deswegen sollte die Methode nur nach einer fehlgeschlagenen Lock-Anforderung benutzt werden, um Informationen darüber zu erhalten, wer den existierenden Lock gesetzt hat. Die Signatur ist:
public String getLockInformation(String value);
[1]Die nachstehenden Methoden sind veraltet und sollten in neuen Programmen nicht mehr benutzt werden: public CisObjectIterator getObjectIterator(String mapper, String oqlString, int flags) und public CisResultSet getResultSet (String mapper, String oqlString)
[2] Klasse com.cisag.pgm.appserver.CisApplicationManager