Donnerstag, 28. Oktober 2010

Eine Erfolgsgeschichte mit Simple Transformations

Simple Transformations (ST) sind eine wunderbare und effiziente Möglichkeit, ABAP-Daten in XML-Dokumente und zurück zu transformieren. Sie sind vom Konzept her bidirektional, somit ideal für die Serialisierung und Deserialisierung geeignet – und ausserdem sind sie, wie wir beobachten konnten, hocheffizient. Wenn man sich einmal in das Konzept eingearbeitet hat, sind sie darüberhinaus wirklich simple, wie es der Name verheisst, also einfach in der Implementierung.

Dank der Simple Transformations ist es uns gelungen, eine unzuverlässige Komponente unseres XI-Systems (den JDBC-Adapter) durch einen stabil laufenden, in ABAP programmierten Adapter zu ersetzen, der über seine Arbeit im Joblog genau Rechenschaft gibt und obendrein einen deutlich besseren Durchsatz hat als die bisherige Lösung.

Wir haben eine entfernte Datenbank, in die im Laufe des Tages aus verschiedenen Quellen immer wieder neue "Meldungen" eingespeist werden (Wert- und Warenflüsse). Um diese einzulesen und den Java-Mappingklassen zur weiteren Verarbeitung zuzuführen, haben wir bislang den von SAP mitgelieferten sogenannten JDBC-Adapter verwendet, der gemäss Beschreibung genau für eine solche Aufgabe geeignet wäre. Leider hat dieser Adapter (in unserem System) eine Schwachstelle, die sich trotz intensiver Bemühungen nicht beheben liess – er leidet gewissermassen unter Narkolepsie: Ganz unvermittelt stellt er immer wieder mal seine Arbeit komplett ein – ohne noch irgendein Lebenszeichen von sich zu geben, aber auch ohne dass der Prozess abbricht, geschieht mitten in einer Verarbeitung einfach nichts mehr.

Da die eingehenden Meldungen zeitkritisch sind, führte dies immer wieder zu extra Wartungsaufwand. Es mussten Überwachungsprogramme geschrieben werden, um die fristgerechte Abarbeitung der Meldungen zu garantieren. Zur Unzeit (so etwas passiert immer zur Unzeit!) wurde unser Support mit Alarm-SMS aus dem Bett geholt, weil es wieder einmal passiert war. Der manuelle Workaround, um den JDBC-Adapter wieder zum Laufen zu bekommen, bestand dann darin, einen zweiten, identisch konfigurierten Kanal zu definieren und auf diesen zu wechseln.

Der JDBC-Adapter ist für uns leider eine "Black Box". Das ist das Problematische an den von SAP ausgelieferten Java-Komponenten (nicht an der Sprache Java selbst – die Sprache Java ist grossartig). Wir können die SAP-Java-Komponenten nicht wie ein ABAP-Programm studieren, um unsere eigenen Schlussfolgerungen zu ziehen.[1] Wir können auch keine Fangschaltung einbauen, um diesen spontan auftretenden Fehler näher zu lokalisieren. Wir sind gezwungen, jedes Problem an den Support von SAP zu addressieren. SAP selbst hatte keine Antwort auf unser Problem und musste die OSS-Meldungen schliessen, da das Problem ja nicht reproduzierbar ist.

Wir haben uns daher dazu entschlossen, diesen JDBC-Adapter aus dem XI-Eingangsprozess zu eliminieren und durch eine in ABAP programmierte Komponente zu ersetzen, die letztlich den sogenannten Plain Adapter verwendet. Das ist ein vom ABAP-seitigen ICF angesprochener REST-artiger HTTP-Service (zu erreichen unter /sap/xi/adapter_plain). Im Body der HTTP-Anfrage erwartet der Plain Adapter das XML-Dokument in der Form, wie es schliesslich vom XI-Mapping verarbeitet wird. Er reicht dieses Dokument dann an die XI-Mappingschicht weiter.

Die eigenprogrammierte Komponente ist ein in regelmässigen Abständen gestarteter Job, der

  1. die Daten aus der entfernten Datenbank in interne Tabellen einliest,
  2. die ABAP-Daten in ein XML-Dokument transformiert – dies ist der Ort, an dem die Simple Transformations zum Einsatz kommen – und
  3. das XML-Dokument schliesslich über einen "internen" HTTP-Request an den Plain Adapter weiterleitet.[2]


In einem (typischen) Joblauf wurden 403 Meldungen mit insgesamt 125.254 Meldungspositionen in 123 Sekunden verarbeitet. Das bedeutet einen Durchsatz von rund 1 Millisekunde pro Position. Hierin enthalten ist das Lesen der Position von der Datenbank, die Abbildung der Meldungen in ein XML-Dokument und schliesslich die Verarbeitung im "Plain Adapter". Das sind wunderbare Laufzeiten!

Die folgende Graphik zeigt, wie sich die Laufzeit für einen typischen Joblauf auf diese Schritte verteilt:



Für das Marshalling, also die Konvertierung der ABAP-Daten in ein XML-Dokument, wurden nur 12 der 125 Sekunden benötigt. Die XML-Konvertierung, einschliesslich eines Prepare-Schritts in ABAP, in dem die Daten für die Transformation passend aufbereitet werden, schafft also 10 Detailsätze pro Millisekunde. Das ist mehr als zufriedenstellend.

Es ist sinnvoll, vor dem eigentlichen Aufruf der ST einen vorbereitenden Schritt zu implementieren, in dem die Daten für den Zugriff der ST passend aufbereitet werden. Simple Transformations sind nicht nur einfach zu verstehen, sie sollten auch einfach konzipiert werden. Denn je simpler eine Simple Transformation gerät, desto effizienter wird sie auch. Im vorliegenden Fall erzeugt die ST eine Sequenz von Elementen ("Zeilen"), die jeweils Daten aus Kopf und Position enthalten. Die passende Datenstruktur hierfür ist eine interne Tabelle mit einem tiefen Zeilentyp: Jede Zeile steht für eine Meldung, und die Komponente detail der Zeile ist selbst eine interene Tabelle, die die Positionen enthält. Bei geschachtelten Strukturen ist auf den erhöhten Speicherbedarf zu achten. Eine überschlägige Rechnung zeigte uns aber, dass der Hauptspeicher für das benötigte Datenvolumen auch für sehr grosse Meldungen noch ausreicht. Ausserdem handelt es sich ja nur um eine Hilfstabelle mit extrem kurzer Lebensdauer - sie lebt als lokales Feld "auf dem Stack" und wird genau in der Methode auf- und abgebaut, die die Simple Transformation durchführt: [3]
* Header/Detail in Tabelle mit tiefer Zeilenstruktur abbilden
loop at it_header assigning <ls_header>.

clear ls_meldung.
move-corresponding <ls_header> to ls_meldung.

loop at it_detail assigning <ls_detail>
where absender = <ls_header>-absender and
meldungs_id = <ls_header>-meldungs_id and
meldungs_datum = <ls_header>-meldungs_datum and
partition_knoten = <ls_header>-partition_knoten and
partition_tag = <ls_header>-partition_tag.
insert <ls_detail> into table ls_meldung-detail.
endloop.

insert ls_meldung into table lt_meldungen.

endloop.


Der Aufruf der Transformation ist dann sehr einfach:

* Meldungsspezifisch in XML transformieren
call transformation (gv_transformation)
source meldungen = lt_meldungen
result xml ev_xml.


Die Transformation wird dynamisch aufgerufen, wir haben für jeden Meldungstyp eine andere Simple Transformation. Die zuvor aufbereitete Meldungstabelle wird nun als source übergeben - das Resultatdokument wird im Parameter ev_xml vom Typ xstring entgegengenommen (es könnte auch ein string oder ein Objekt vom Typ if_ixml_document sein - die Transformation erkennt selbständig den erwarteten Typ).

Die Simple Transformation selbst ist nun wirklich einfach: Nach Festlegung des Referenz-Datenobjekts mittels <tt:root> werden die Meldungen mit einer geschachtelten <tt:loop> abgearbeitet. In der inneren Loop wird schliesslich die Ergebniszeile hergestellt, indem die entsprechenden Quellfelder aus Meldungskopf oder Meldungsposition in das durch das Mapping gewünschte Zielfeld übernommen werden.

Hier ein typisches Beispiel (wir haben rund ein Dutzend solcher Transformationen, und sie sehen alle ähnlich aus):

<?sap.transform.simple?>
<!-- Automatisch generierte Transformation.
Bitte Anpassungen nur an der Vorlage machen
(XSLT-Transformation ZGDBW_TO_XI_CREATE)-->
<tt:transform xmlns:tt="http://www.sap.com/transformation-templates">
<tt:root name="MELDUNGEN"/>
<tt:template>
<ns:MT_MVN_DESADV2500 xmlns:ns="http://migros.ch/xi/DESADV2500">
<tt:loop ref=".MELDUNGEN" name="header">
<tt:loop ref="DETAIL">
<row>
<ABSENDER>
<tt:value ref="$header.ABSENDER"/>
</ABSENDER>
<MELDUNGS_ID>
<tt:value ref="$header.MELDUNGS_ID"/>
</MELDUNGS_ID>
(... weitere Kopffelder ...)
<FELD_1 >
<tt:value ref="FELD_1 "/>
</FELD_1 >
(... weitere Detailfelder ...)
</row>
</tt:loop>
</tt:loop>
</ns:MT_MVN_DESADV2500>
</tt:template>
</tt:transform>


Was heisst hier "automatisch generiert"? Ein weiterer starker Vorteil der Simple Transformations (ebenso wie der XSLT-Transformationen) ist, dass es ein einfach zu bedienendes API zum Erstellen einer Transformation im Repository gibt - die Klasse CL_O2_API_XSLTDESC. Dies kam uns in unserer Situation sehr entgegen: Dinge wie der Namensraum des Zieldokuments und die tatsächlich zu extrahierenden Felder variieren nämlich je nach Meldungstyp. Wir haben daher eine simple Customizingtabelle vorgesehen, in der wir pro Meldungstyp diese Unterschiede festlegen. Ein Report Z_REGENERATE_ST baut aus dieser Customizingtabelle alle vorgesehenen ST-Transformationen auf (wobei er sich selbst einer XSLT-Transformation bedient). Diese werden dann dynamisch aufgerufen. Dieses Vorgehen hat den Vorteil, dass die Transformationen möglichst einfach und sehr schnell werden. Ausserdem hat eine im Repository gepflegte Transformation den Vorteil, dass sie bei Gebrauch im Hauptspeicher des Servers gepuffert wird - was ihre Ausführung noch mehr beschleunigt.


[1] Zwar gibt es neben dem Disassembler javap hervorragende Java-Decompiler mit begeisternden Benutzerschnittstellen, wie z.B. jd-gui. Es ist aber umständlich, ein Programm erst decompilieren zu müssen, um seine Logik zu verstehen und ggf. zu modifizieren. Bei der in der Branche zunehmenden Kleingeistigkeit ist ausserdem vermehrt mit dem Einsatz von Bytecode-Verdunklern (Obfuskatoren) zu rechnen, die die Lesbarkeit des decompilierten Codes weiter erschweren.

[2] Das geschieht mittels eines internen HTTP-Requests - d.h. der Baustein HTTP_DISPATCH_REQUEST erscheint im eigenen Callstack, der Request wird nicht in einem separaten Task bearbeitet. Insbesondere ist die Verarbeitung der Requests somit notwendigerweise synchron: Wenn der Job durchgelaufen ist, haben sämtliche Meldungen den Plain Adapter passiert.

[3] Wir haben hier eine Loop über eine Tabelle mit N Eintragen, die eine Loop über eine zweite Tabelle mit N*M Einträgen enthält. Das könnte ein Problem ergeben. Damit hier keine quadratischen Effekte auftreten, darf die Positionstabelle keine Standardtabelle sein. In unserem Fall sind beide Tabellen sortiert. Die innere Loop mit Where-Bedingung nutzt implizit die Sortierordnung gemäss Tabellenschlüssel aus, so dass kein Performanceproblem entsteht. Eine - noch etwas effizientere - Alternative wäre gewesen, mit der loop at it_detail zu beginnen (ohne Where-Bedingung) und mittels at new die Kopfdaten beim Wechsel zu einer neuen Meldung nachzulesen. Aber, wie man an den Laufzeiten sieht, ist der "Marshalling Prepare" Schritt auch in dieser Form unkritisch.

Kommentare :

Holgi hat gesagt…

Hi Rüdiger,
Dein Beispiel hat bei mir wesentlich dazu beigetragen, ST zu verstehen. Was mich aber abschreckte war die Tatsache die Transformationen per Hand zu schreiben. Im ECC 6.0 kann man diese generieren:
http://sapblog.rmtiwari.com/2009/02/discovering-hidden-gem-generate-simple.html
... dass ist schon 'ne "coole" Sache :-)
Gruß Holgi

Rüdiger Plantiko hat gesagt…

Hallo Holgi,

natürlich sind Programmgeneratoren eine coole Sache: "Programs that write programs are the happiest programs in the world." (Andrew Hume)

Sicher ist Dir beim Lesen meines Blogs nicht entgangen, dass auch in unserem Fall die Simple Transformations mit einem Generator aus dem Customizing erzeugt werden.

Dieser Generator ist allerdings handgeschrieben - und massgeschneidert auf unser Meldungscustomizing.

Während ich den selbstgeschriebenen Generatoren natürlich vollstes Vertrauen entgegenbringe ;-) , bin ich bei allgemeinen Generator-Tools immer skeptisch und habe Angst, dass sie wegen ihrer Allgemeingültigkeit unnötigen Overhead produzieren. Inwieweit das bei dem von Dir beschriebenen Generator der Fall ist, habe ich nicht überprüft.

Gruss,
Rüdiger

rheckly hat gesagt…

Hoi Rüdiger
Hoffe, Du erinnerst Dich noch an mich ;-)
Ich schlage mich zur Zeit auch etwas mit XML herum - um Daten aus dem SharePoint ins ABAP zu laden. Ich bin schon recht weit gekommen und würde eben gerne den erhaltenen XML String via einer Transformation in eine interne Tabelle laden.
Dazu habe ich auf SDN folgende Frage gestellt:
http://scn.sap.com/thread/3917964

Falls Du da mal Zeit und Lust hast, das anzuschauen und mir etwas Hilfe zu geben, wäre ich Dir sehr dankbar!
Gruss von nebenan
Roger Heckly

Rüdiger Plantiko hat gesagt…

Hallo Roger,

siehe http://pastebin.com/EXkhLxuc

Gruss,
Rüdiger

rheckly hat gesagt…

Lieber Rüdiger
Du hast meinen Tag gerettet. Habe letzte Woche einiges recherchiert (und bin eben auch auf deinen Blog hier gestossen) - leider fehlte mir immer ein lauffähiges Beispiel.
Und so bin ich eben an Punkten gestolpert, bei denen ich dann nicht mehr wusste, woran es denn nun liegt (Transformation, Table Type, etc.).
Aber nun funktioniert es einwandfrei - ganz herzlichen Dank!
Musste bei meinem Beispiel noch einen bei den Attributen einbauen, da das Plant nicht immer im XML zurück kommt.
Gruss
Roger

rheckly hat gesagt…

Hoi Rüdiger
Du hast oben etwas von "Automatisch Erstellen von ST" geschrieben - mir schwebt auch so eine Customizing Tabelle vor, in der die Logik steht.
Ist es demzufolge möglich, mit der Klasse CL_O2_API_XSLTDESC zur Laufzeit eine ST zu erstellen und diese dann aufzurufen?
Wenn ja, hast Du ein Beispiel dafür, welches Du mir zustellen könntest?
Gruss Roger

Rüdiger Plantiko hat gesagt…

Hallo Roger,

ja, das hast Du richtig gelesen. Wir haben eine ganze Gruppe von ST-Transformationen, die sich im Detail nur leicht unterscheiden. Die Unterschiede werden in einer Customizingtabelle gepflegt, und ein Report Z_REGENERATE_ST generiert die Simple Transformations aus diesem Customizing mithilfe einer XSLT-Transformation. Die XSLT ist also ein Generater, tatsächlich werden vom XI-Plain-Adapter dann nur die generierten Simple Transformations verwendet.

Wenn ich Deine E-Mail-Adresse noch irgendwo auftreiben kann, schicke ich Dir ein ZIP-File mit einer Dokumentation und Quelltexten.

Gruss,
Rüdiger