Eine überraschende Lösung schwerwiegender Konsistenz-Probleme mit Implementierungs-Beispielen aus IBM InfoSphere DataStage
Einleitung
SAP entwickelt marktführende Enterprise-Resource-Planning-Systeme (kurz: ERP-Systeme), die zur Abbildung und Verwaltung gesamtbetrieblicher Prozesse genutzt werden können. Eines der SAP Module heißt SAP S/4HANA for Financial Products Subleger (kurz: SAP FPSL) und wird für die Buchhaltung komplexer Finanzprodukte von Banken und Versicherungen eingesetzt. Mit InfoSphere DataStage stellt IBM eines der führenden Software-Produkte zur Implementierung von Extract-Transform-Load (kurz: ETL) Prozessen im Kontext von Data Warehousing und Data Integration bereit.
Die Verbindung zwischen der ETL-Welt und SAP FPSL kann über das SAP Business Application Programming Interface (kurz: BAPI) hergestellt werden. Mit Hilfe dieser standardisierten Schnittstelle können externe Komponenten die vorhandenen Daten im SAP FPSL lesen und neue Daten ins SAP FPSL hinzufügen. IBM hat hierfür mit der sogenannten BAPI Stage einen von SAP zertifizierten Konnektor entwickelt, welcher Daten direkt aus einem ETL-Prozess heraus entgegennehmen kann, um diese nach SAP FPSL zu übermitteln. Dabei können insbesondere im Zusammenspiel mit komplexeren Daten schnell Konsistenz-Probleme in SAP FPSL entstehen.
In diesem Beitrag durchleuchten wir, wie diese Inkonsistenzen zustande kommen und präsentieren eine überraschende Lösung, mit welcher wir die Daten stabil übermitteln können. Abschließend möchten wir anhand eines Beispiels eine mögliche Implementierung in DataStage für die Lösung zeigen. Doch zunächst beginnen wir im folgenden Abschnitt mit einigen
Komplexe Datenstrukturen
Datenstrukturen in SAP
In unserem Anwendungsfall möchten wir diverse Transaktionen wie Prämien und Schadenszahlungen aus unterschiedlichen Quellen nach SAP FPSL liefern. Die Quellsysteme sind dabei tabellarisch aufgebaut und liefern für jede Transaktion genau eine Zeile aus der entsprechenden Transaktionstabelle.
Das BAPI hingegen erwartet beim Aufruf einiger Methoden neben normalen Eigenschaftsfeldern auch strukturierte Daten zu jedem übertragenen Datensatz. Diese Substrukturen haben tabellenartige Eigenschaften, die über fremdschlüsselähnliche Konstrukte logisch miteinander verknüpft sein können.
Generell können über die sogenannte BAPI Sequenz Nummer viele Datensatze als gebündeltes Paket nach SAP übertragen werden. Für jede neue Nummer wird ein BAPI Call abgesetzt. Im Folgenden konzentrieren wir uns jedoch auf die Methode zur Übertragung der Business Transactions (vgl. Abbildung 1).
Die Methode für Business Transactions
Über dieses Modul muss für jedes Business Event (hier: eine Transaktion) genau ein BAPI Call mit einer eindeutigen, noch nicht verwendeten BAPI Sequenz Nummer abgesetzt werden. Eine einzige Transaktion kann dabei allerdings gleich mehrere Buchungen auslösen. Diese Buchungen bezeichnet man als Positions oder als Items; SAP fordert sie über eine Tabellenstruktur namens POSITIONS an. Innerhalb dieser Struktur erhalten die Items eine je BAPI Sequenz Nummer hochlaufende Position Nummer sowie Ausprägungen, die das zu buchende Konto (Transaction Type), die Buchungsrichtung (Posting Direction), den zu buchenden Betrag (Amount) und das Buchungsdatum (Posting Date) für dieses Item ausweisen.
Darüber hinaus können im SAP über eine weitere tabellenartige Substruktur POSCHARACTERISTICS zu jeder Position beliebig viele, frei anpassbare Eigenschaften übermittelt werden. Diese auch als Customer Added Characteristics (kurz: CACs) bezeichneten Name-Wert-Paare werden mit einer eigenen Position Nummer versehen, um sie wie ein Fremdschlüssel mit einem Item in Verbindung zu setzen.

Im vorliegenden, konkreten Beispiel hat es zwei Business Events gegeben, welche jeweils einen BAPI Call auslösen. Der erste wird über die BAPI Sequenz Nummer 1001 abgewickelt, der zweite über die Nummer 1002 (vgl. Tab 1.1).

Das Event 1001 soll dabei im SAP zwei Buchungen auslösen: Die erste über den Transaction Type YD5000 mit Buchungsrichtung C und Betrag 300; die zweite über YS7300 mit D und 1500. Das Event 1002 hingegen wird nur eine einzige Buchung über den Transaction Type YD5000 mit Buchungsrichtung D und Betrag 2300 auslösen (vgl. Tab 1.2).

Alle drei Buchungen sollen um jeweils drei CACs erweitert werden. Die Eigenschaften wie auch deren Werte sind in diesem Beispiel für jedes Item dieselben (vgl. Tab 1.3).

Das Problem
Die Daten können mittels des BAPI-Konnektors nicht in einer komplex-strukturierten Form übermittelt werden. Somit müssen wir komplexe SAP-Datentypen wie zum Beispiel Tabellen abflachen, sodass wir eine Übertragung auf Zeilenbasis ausführen können.

Diese Form der Datenbereitstellung hat zwar den Charme, dass wir sie relativ leicht erzeugen können. Allerdings zieht sie Inkonsistenzen im Zielsystem SAP nach sich, sobald zwei tabellenartige Strukturen mit mehreren Werten beliefert werden sollen.
Ein erster Versuch
Die aus SAP-Sicht strukturierten Daten der POSITIONS (in Tab 2 gelb markiert) und der POSCHARACTERISTICS (grün) werden getrennt voneinander und zeilenweise eingelesen. Sobald innerhalb eines BAPI Calls in zwei aufeinanderfolgenden Zeilen dieselben Werte in den POSITIONS stehen, hört der entsprechende Leseprozess für die POSITIONS auf und setzt erst wieder beim nächsten BAPI Call ein. Der Leseprozess für die POSCHARACTERISTICS verläuft analog.
In unserem Beispiel stoppt der Leseprozess der POSITIONS bereits nach der ersten Zeile, weil in der zweiten Zeile dasselbe Item erneut aufgeführt ist. Erst mit einem neuen BAPI Call für das Event mit der Nummer 1002 setzt der Leseprozess wieder ein, sodass das zweite Item der BAPI Sequenz Nummer 1001 nicht übertragen wird (vgl. Tab 3). Eine Fehlermeldung diesbezüglich wird nicht ausgegeben.
Die CACs hingegen werden vollständig eingelesen und ins Zielsystem übertragen, da mit jedem Zeilenwechsel neue Daten erfasst werden. Dies wirft eine Fehlermeldung, da die Customer Added Characteristics mit Item Nummer 2 keinem Item zugeordnet werden können.
Wenn die Datensätze auf andere Weise sortiert sind, als in Tabelle 2 dargestellt, so ist es durchaus möglich, zumindest einen Teil der Daten zufälligerweise korrekt und vollständig zu übertragen. Um eine wirklich stabile Übertragung zu erreichen bedarf es allerdings ab zwei Substrukturen einer anderen Lösung als den hier vorgestellten Inner Join.
Der SAP-Lesealgorithmus
Die aus SAP-Sicht strukturierten Daten der POSITIONS (in Tab 2 gelb markiert) und der POSCHARACTERISTICS (grün) werden getrennt voneinander und zeilenweise eingelesen. Sobald innerhalb eines BAPI Calls in zwei aufeinanderfolgenden Zeilen dieselben Werte in den POSITIONS stehen, hört der entsprechende Leseprozess für die POSITIONS auf und setzt erst wieder beim nächsten BAPI Call ein. Der Leseprozess für die POSCHARACTERISTICS verläuft analog.
In unserem Beispiel stoppt der Leseprozess der POSITIONS bereits nach der ersten Zeile, weil in der zweiten Zeile dasselbe Item erneut aufgeführt ist. Erst mit einem neuen BAPI Call für das Event mit der Nummer 1002 setzt der Leseprozess wieder ein, sodass das zweite Item der BAPI Sequenz Nummer 1001 nicht übertragen wird (vgl. Tab 3). Eine Fehlermeldung diesbezüglich wird nicht ausgegeben.
Die CACs hingegen werden vollständig eingelesen und ins Zielsystem übertragen, da mit jedem Zeilenwechsel neue Daten erfasst werden. Dies wirft eine Fehlermeldung, da die Customer Added Characteristics mit Item Nummer 2 keinem Item zugeordnet werden können.
Wenn die Datensätze auf andere Weise sortiert sind, als in Tabelle 2 dargestellt, so ist es durchaus möglich, zumindest einen Teil der Daten zufälligerweise korrekt und vollständig zu übertragen. Um eine wirklich stabile Übertragung zu erreichen bedarf es allerdings ab zwei Substrukturen einer anderen Lösung als den hier vorgestellten Inner Join.

Die Lösung
Eine konsistente Anlieferung der Daten in das Zielsystem SAP können wir nur dann erreichen, wenn wir die Daten für den BAPI-Konnektor so aufbereiten, dass die Lesealgorithmen sämtliche Datensätze erfassen. Wir beschreiben die Zielstruktur zunächst anhand unseres Beispiels (vgl. Tab 4) und entwickeln darauf aufbauend einen recht abstrakten, dafür aber allgemeingültigen Ansatz.
Die Lösung anhand des Beispiels
Da wir sechs verschiedene Customer Added Characteristics für die BAPI Sequenz Nummer 1001 übermitteln möchten, muss dieser BAPI Call sechs Zeilen erhalten. Die BAPI Sequenz Nummer wird darin stets wiederholt, die CACs erhalten je eine Zeile. Bei den POSITIONS führen wir in der ersten Zeile das erste Item und in allen folgenden fünf Zeilen das zweite Item auf. Für die BAPI Sequenz Nummer 1002 sollen drei CACs gesendet werden. Wir generieren also drei Zeilen, wiederholen stets die BAPI Sequenz Nummer und platzieren je Zeile einen CAC. Da wir für die 1002 nur ein Item schicken, wiederholen wir dieses in jeder der drei Zeilen.

Allgemeingültige Lösung
Um ein solches Zielformat allgemeingültig zu beschreiben, führen wir zuerst einige Variablen ein.
Variable | Definition | Wert im Beispiel* |
i | Anzahl aller Substrukturen für einen BAPI Call | 2 |
m(k), für 1<=k<=i | Die Anzahl der Datensätze, die wir für die k‑te Substruktur übertragen möchten | m(1) = 2 m(2) = 6 |
n | n = max(m(k), mit 1<=k<=i) Die maximale Anzahl an Datensätzen, die wir für einen BAPI Call innerhalb einer Struktur übertrage möchten | 6 |
* für die BAPI Sequenz Nummer 1001; mit Substruktur 1 = POSITIONS und Substruktur 2 = POSCHARACTERISTICS
Mit diesen Definitionen kann das Zielformat für eine BAPI Sequenz Nummer wie folgt aussehen:
- Erstelle n Zeilen
- Wiederhole BAPI Sequenz Nummer sowie alle weiteren einfachen Parameter (in unserem Beispiel das Quellsystem) in jeder Zeile der n Zeilen
- Für Spalten, die im SAP die Substruktur k widerspiegeln, gilt: Trage die Datensätze in die Zeilen 1 bis m(k) Wiederhole den m(k)-ten Datensatz in den Zeilen m(k)+1 bis n
Umsetzung in DataStage
Nachdem wir in Abschnitt 4 gezeigt haben, wie wir die Daten zur konsistenten Belieferung der BAPI Stage strukturieren müssen, möchten wir nun abschließend anhand eines Beispiels demonstrieren, wie wir dieses Datenformat innerhalb eines DataStage Jobs erzeugen können (vgl. Abbildung 2).
Jobdesign
Wir gehen davon aus, dass wir die Business Events aus einer Datenbank einlesen können (SHTabelleLesen) und ihnen in einem zweiten Schritt die gewünschten Items inklusive einer passenden Position Nummer zuweisen können (SHItems). Nach dieser Vorbereitung geschieht der alles entscheidende Trick im dritten Schritt: In dem Shared Container SHCAC generieren wir nicht nur die Customer Added Characteristics, sondern strukturieren ebenfalls die Daten wie in Tabelle 4 beschrieben. Abschließend mappen wir die Daten auf das Zielformat des BAPI-Konnektors, sortieren und schicken sie über die BAPI-Stage nach SAP FPSL.

Shared Container CAC
Wir erzeugen die CACs abseits des regulären Datenstroms. Dabei erhalten sie nicht nur ihre eigene Position Nummer (ITEM_NO POSCHAR), sondern auch die Position Nummer des Items (ITEM_NO POS), mit dem sie zwar nicht zwingend logisch, aber dennoch strukturell über einen Join verbunden werden sollen.

Im Eingang dieses Shared Containers sehen die Daten aus wie in Tabelle 5.1 gezeigt: Für jedes Item gibt es genau einen Datensatz, in dem alle Informationen für dieses Item gespeichert sind.

Der Transformer TRA_Split
Der erste zentrale Baustein ist der Transformer TRA_Split (vgl. Abbildung 4). Über den Link LN_Data_Stream_zu_Join_Left leitet dieser sämtliche Spalten der Daten mittels Runtime Column Propagation (kurz: RCP) an die folgende Join Stage JOI_Join weiter. Über den zweiten Link LN_zu_CAC hingegen leitet der Transformer nur ausgewählte Spalten und auch nur solche Zeilen, die innerhalb einer BAPI Sequenz Nummer die höchste Position Nummer innehaben. Dafür sortieren wir vor dem Transformer mit SOR_Split nach der BAPISequenzNummer und nutzen im Transformer den Filter LastRowInGroup(LN_von_SOR.BAPISequenzNummer). Tabelle 5.2 stellt die Daten an dieser Stelle dar.


Der Transformer TRA_CAC
Im weiteren Verlauf erzeugen wir die CACs über einen Loop in der Transformer Stage TRA_CAC (vgl. Abbildung 5). Zu jedem eintreffenden Datensatz erstellen wir in unserem Beispiel NumChar x MAX_PositionNumber CACs. Dabei ist NumChar = 3 die Anzahl der verschiedenen CACs und MAX_PositionNumber die höchste Position Nummer zur aktuellen BAPI Sequenz Nummer. Wir setzen den Namen (Spalte: CUSTOMER_ADDED_CHARACTERISTICS), den Wert (Spalte: FIELD_VALUE) und berechnen die zugehörige Position Nummer, welche den aktuellen CAC mit einem Item logisch verknüpfen soll (Spalte: PositionNummer_CAC), indem wir bei 1 starten und nach jedem Durchlauf über die gesamte Liste der CACs (dann gilt: Mod(@ITERARION‑1, NumChar) = 0) um 1 erhöhen. So erhalten die ersten drei CACs die Position Nummer 1, die nächsten drei erhalten die 2, usw.
Darüber hinaus ermitteln wir für jeden CAC die Position Nummer des Items, mit welchem der CAC strukturell verbunden werden soll (Spalte: PositionNummer). Hierzu starten wir ebenfalls bei 1, zählen allerdings bei jeder Iteration um 1 hoch bis wir die maximale Position Nummer für die aktuell BAPI Sequenz Nummer erreicht haben und wiederholen diese fortan. Tabelle 5.3 zeigt das Ergebnis des Transformers TRA_CAC.


Der Join JOI_Join
In der abschließenden Join Stage JOI_Join heften wir die erzeugten CACs über einen Inner Join mit den Schlüsseln BAPISequenzNummer und PositionNummer an die ursprünglichen Datensätze des Links LN_Data_Stream_zu_Join_Left (vgl. Abbildung 6) und erhalten so auf allgemeingültige Weise das Format der Tabelle 4.

Mit dieser doch recht speziellen Beladungstechnik stellen wir letztlich sicher, dass der SAP-Lesealgorithmus alle über das BAPI gesendeten Daten erfasst und wie gewünscht miteinander verknüpfen kann. Wir konnten die weiter oben beschriebenen Konsistenzprobleme auflösen.