Caching ist eine Form des Zwischenspeicherns von Daten, sodass Daten, die bereits von einer Datenbank geladen worden sind, nicht erneut geladen und verarbeitet werden müssen. Dieses Konzept existiert bei SAP. Hauptanwendungsbereich ist dabei das Cachen von Stammdaten. Allerdings existiert für Daten, die nicht zu den Stammdaten zählen, kein Cache. Zudem kann ich diesen Cache durch verschiedene Methoden aushebeln, z. B. durch die Verwendung von Datenbank-JOINs. Daher ist es Aufgabe des Programmierers, die Anwendungslogik so zu gestalten, dass die Datenbankabfragen performant sind. Dennoch sind in der Praxis oftmals die Datenbankabfragen bzw. deren Häufigkeit an Aufrufen ein großes Performance-Problem.
Um dieses Problem zu schließen, haben wir als Acando einen generischen Cache implementiert, welcher für beliebige Daten verwendet werden kann. Die Implementierung der Cache-Klasse sowie die dazugehörigen Unit-Tests sind als Textdatei im Anhang zu finden.
1 Cache-Aufbau
Der generische Cache beinhaltet die nachfolgenden Methoden, welche die dargestellten Attribute und Typen benötigt.
Der wesentliche Bestandteil eines Caches sind dessen Einträge, weswegen ich nachfolgend näher darauf eingehe.
Typdefinitionen
Um klar festzulegen, welchen Typen der Schlüssel eines Eintrages hat, wird der Schlüssel mit dem Typen T_KEY festgelegt. T_KEY entspricht dabei dem Typen String.
Ein Cache-Eintrag (definiert als Typ TS_CACHE) besteht aus folgenden Komponenten:
- Key: Schlüssel (Typ T_KEY)
- Type: Typ des Eintrags (Typ I)
- Value: Wert des Eintrags (Typ XSTRING)
- Size: Größe des Eintrages (Typ I)
- Timestamp: Zeitpunkt, an dem der Eintrag geschrieben wurde (Typ TIMESTAMPL)
Anmerkung: Der Typ TT_CACHE entspricht dem Tabellentypen der Struktur TS_CACHE.
Man erkennt, dass die Typen generisch sind, was bedeutet, dass der Cache für jeden beliebigen Typ einsetzbar ist. Um unterscheiden zu können, von welchem Typ der Eintrag ist, gebe ich den Typ bei der Abfrage eine Eintrags bzw. beim Schreiben mit an.
Im optimalen Fall erstelle ich dazu ein Konstanten-Interface, bei dem ich eine eindeutige Bezeichnung und eine Nummer hinterlege (analog zu einer Enumeration in Sprachen wie Java, C#, etc.). Diesen Typen gibt man beim Lesen als auch beim Schreiben eines Cache-Eintrages an.
Durch die Verwendung des Typs XSTRING des Eintrags-Wertes ist die Generizität des Caches gewährleistet. Das zeigt nachfolgende Abbildung.
2 Cache-Benutzung
2.1 Schreiben von Cache-Einträgen (SET_ENTRY)
Damit ich einen Cache-Eintrag schreiben kann, benötige ich zuallererst die Daten, die ich schreiben möchte. Diese lese ich meistens aus der Datenbank oder ich benutze bereits berechnete Daten, etc.
Da ich für einen Eintrag die Bestandteile Key, Type und Value benötige, müssen diese vor dem Setzen eines Eintrages feststehen. D. h. ich habe die Daten beschafft, den zugehörige Schlüssel festgelegt (ggf. berechnet) und einen Typ festgelegt.
2.2 Lesen von Cache-Einträgen (GET_ENTRY)
Das Lesen eines Cache-Eintrages bedeutet, dass mithilfe eines Schlüssels und eines Typs, die Werte, ohne erneutes Lesen aus der Datenbank, zurückgegeben werden. Zusätzlich setzt das System die Variable ev_not_found, welche bestimmt, ob das System einen Eintrag gefunden hat oder nicht. Falls dies nicht der Fall ist, muss das System den Eintrag schreiben.
2.3 Erstellen des Schlüssels
Bei der Erstellung des Schlüssels kann man sowohl einfache Variablen/Strukturen als auch Tabellen angeben. Ein häufiger Fall ist die Bildung eines Schlüssels über Variablen/Strukturen und Tabellen zugleich.
Allerdings besteht bei Tabellen die Herausforderung, jede kleinste Änderung in der Tabelle zu erkennen.
2.3.1 Ohne Hashberechnung
Bei einfachen Variablen (Typ String, I, etc.) oder Strukturen können diese direkt als Schlüssel des Eintrages angegeben werden. Lediglich tiefe Strukturen sollten vermieden werden.
2.3.2 Hash-Berechnung (CALC_HASH)
Die oben genannte Herausforderung, Änderungen in komplexen Datentypen festzustellen, löst man durch die Berechnung eines Hashs über die gesamte komplexe Struktur hinweg. Mit der statischen Methode calculate_hash_for_raw der Klasse cl_abap_message_digest lässt sich dieser unter Verwendung des SHA1-Algorithmus berechnen. Als Ergebnis erhält man einen String der Länge 40.
2.4 Leeren des Caches (RESET)
Da die Daten des Caches nach einer gewissen Zeit veraltet sind, empfiehlt es sich, ihn gelegentlich zu leeren. Die dafür implementierte Methode RESET erledigt das. Dabei besteht die Möglichkeit einen Typen als Parameter mit anzugeben, welcher die Einträge des Typs aus dem Cache entfernt. Falls ich den Typ nicht angegebe, leert die Methode den Cache komplett.
2.5 Verkleinern des Caches (CLEANUP)
Da der Cache aufgrund der Anzahl der Einträge zu groß für den Speicher werden kann, ist es notwendig, diesen beim Setzen von neuen Einträgen aufzuräumen. Das heißt, dass, wenn der Wert der Konstante GC_MAX_SIZE überschreitet, die Methode CLEANUP die Cache-Tabelle um den Wert der Konstanten GC_CLEANUP_SIZE verkleinert. Das erfolgt durch das Löschen von Einträgen, welche am ältesten sind (Überprüfung realisiert durch den Timestamp).
2.6 Aktivieren/Deaktivieren des Caches (SET_ENABLED)
Da man für manche Testzwecke eine Überprüfung ohne als auch mit Cache benötigt, ist es sinnvoll, den Cache aktivieren und deaktivieren zu können.
3 Cache-Beispiele
3.1 Test einfacher Schlüssel (TEST_METHOD_SIMPLE)
Das Beispiel zeigt eine kurze Beispiel-Implementierung des Caches. Dabei selektiere ich spezifisch aus den Flugdaten der SAP Demo-Daten – in diesem Fall nach einem Abflugsland. Zuerst überprüfe ich, ob der Eintrag vorhanden ist. Anfangs ist er das nicht, weshalb das System einen Cache-Eintrag schreibt. Daraufhin versuche ich nochmals den Eintrag zu lesen, was dieses Mal erfolgreich ist. Die darauffolgende Überprüfung stellt sicher, dass die Daten dieselben sind.
DATA:lt_spfli_exp TYPE TABLE OF spfli, ls_spfli TYPE spfli, lv_country TYPE land1, lv_not_found TYPE abap_bool, lt_spfli_act TYPE TABLE OF spfli. *& Initialisierung: Schlüssel für den Flugplan ist das Land lo_cache = zcl_cache=>get_instance( ). lv_country = 'DE'. *& Eintrag vorhanden? lo_cache->get_entry( EXPORTING iv_type = zcl_cache=>gc_enum_type-flight_schedule iv_key = lv_country IMPORTING ev_value = lt_spfli_act ev_not_found = lv_not_found ). *& Wenn Eintrag nicht gefunden wurde, beschaffe die Daten und setze einen Cache-Eintrag. IF lv_not_found EQ abap_true. SELECT * FROM spfli INTO TABLE lt_spfli_exp WHERE countryfr = lv_country. lo_cache->set_entry( EXPORTING iv_type = zcl_cache=>gc_enum_type-flight_schedule iv_key = lv_country iv_value = lt_spfli_exp ). ENDIF. *& Eintrag lesen lo_cache->get_entry( EXPORTING iv_type = zcl_cache=>gc_enum_type-flight_schedule iv_key = lv_country IMPORTING ev_value = lt_spfli_act ev_not_found = lv_not_found ). *& Die beiden internen Tabellen müssen gleich sein cl_abap_unit_assert=>assert_equals( act = lt_spfli_act exp = lt_spfli_exp ).
3.2 Test von GUIDs (TEST_METHOD_GUID)
Bei Verwendung von GUIDs als Schlüssel stößt der Cache anfangs an seine Grenzen. Nachfolgend habe ich ein Beispiel implementiert, worin ich GUIDs verwende und welche ich anfangs nicht kompilieren kann, da der Typ für den Schlüssel (RAW(16)) nicht mit dem Typen des Cache-Schlüssels (CLIKE) übereinstimmt. Lösung des Problems ist die Konvertierung in den Typ Char(32).
CONSTANTS lc_key_type TYPE i VALUE '3'. DATA lt_bp TYPE STANDARD TABLE OF snwd_bpa WITH DEFAULT KEY. DATA ls_bp TYPE snwd_bpa. DATA lv_not_found TYPE abap_bool. * Typ Raw Länge 16 DATA ls_cache_key_temp TYPE snwd_node_key. DATA ls_cache_key(32) TYPE c. * Hier wäre ein der Typ inkompatibel mit der Schnittstelle des Caches. ls_cache_key_temp = '000C296828551EE99F9629ACFDCB1537'. * Deshalb wird hier in einen Char(32) konvertiert. ls_cache_key = ls_cache_key_temp. SELECT * FROM snwd_bpa INTO TABLE lt_bp ORDER BY node_key. lo_cache = zcl_cache=>get_instance( ). lo_cache->get_entry( EXPORTING iv_type = lc_key_type iv_key = ls_cache_key IMPORTING ev_value = ls_bp ev_not_found = lv_not_found ). IF lv_not_found EQ abap_true. READ TABLE lt_bp INTO ls_bp WITH KEY node_key = ls_cache_key BINARY SEARCH. IF sy-subrc = 0. lo_cache->set_entry( EXPORTING iv_type = lc_key_type iv_key = ls_cache_key iv_value = ls_bp ). ENDIF. ENDIF.
3.3 Test von komplexen Schlüsseln/Einträgen (TEST_METHOD_KOMPLEX)
Nachfolgendes Beispiel zeigt die Verwendung des Caches mit einem komplexen Schlüssel als auch komplexen Einträgen. Wie zu erkennen ist, besteht der Schlüssel aus zwei Ranges. Aus diesen erzeuge ich mittels der Methode CALC_HASH den Schlüssel für den Eintrag. Weiterhin existiert der Typ ts_cache_value, welcher beide Werte des Cache-Eintrags (bpheaderdata und bpcontactdata) hält.
TYPES: * Komplexer Cache Value bestehend aus 2 internen Tabellen BEGIN OF ts_cache_value, bpheaderdata TYPE STANDARD TABLE OF bapi_epm_bp_header WITH DEFAULT KEY, bpcontactdata TYPE STANDARD TABLE OF bapi_epm_bp_contact WITH DEFAULT KEY, END OF ts_cache_value. DATA lt_rng_bpid TYPE if_epm_bp_header=>tt_sel_par_bp_ids. DATA ls_rng_bpid LIKE LINE OF lt_rng_bpid. DATA lt_rng_companyname TYPE if_epm_bp_header=>tt_sel_par_company_names. DATA ls_rng_companyname LIKE LINE OF lt_rng_companyname. DATA lv_not_found TYPE abap_bool. DATA lv_cache_key TYPE string. DATA ls_cache_value TYPE ts_cache_value. * Ranges für Selektion aufbauen ls_rng_bpid-sign = 'I'. ls_rng_bpid-option = 'BT'. ls_rng_bpid-low = '0100000005'. ls_rng_bpid-high = '0100000020'. APPEND ls_rng_bpid TO lt_rng_bpid. ls_rng_companyname-sign = 'I'. ls_rng_companyname-option = 'CP'. ls_rng_companyname-low = 'A%'. APPEND ls_rng_companyname TO lt_rng_companyname. lo_cache = zcl_cache=>get_instance( ). * Hashwert für den komplexen Cachekey erzeugen lo_cache->calc_hash( EXPORTING iv_data = lt_rng_bpid iv_data2 = lt_rng_companyname IMPORTING ev_hash = lv_cache_key ). lo_cache->get_entry( EXPORTING iv_type = zcl_cache=>gc_enum_type-business_partner_list iv_key = lv_cache_key IMPORTING ev_value = ls_cache_value ev_not_found = lv_not_found ). IF lv_not_found EQ abap_true. CALL FUNCTION 'BAPI_EPM_BP_GET_LIST' TABLES selparambpid = lt_rng_bpid selparamcompanyname = lt_rng_companyname bpheaderdata = ls_cache_value-bpheaderdata bpcontactdata = ls_cache_value-bpcontactdata. lo_cache->set_entry( EXPORTING iv_type = zcl_cache=>gc_enum_type-business_partner_list iv_key = lv_cache_key iv_value = ls_cache_value ). ENDIF.
Anschließend kann über die Variable ls_cache_value auf die jeweiligen gecachten Einträge zugriffen werden. In diesem Fall wäre das mit ls_cache_value-bpheaderdata und ls_cache_value-bpcontactdata.
4 Fazit des generischen Caches
Mithilfe des generischen Caches ist es möglich, Bewegungsdaten und Daten, die am SAP-Cache vorbei gehen (z. B. durch JOINs), im Speicher zu halten. Durch die Implementierung auf Basis des Singleton-Patterns ist ein Zugriff an jeder Programmstelle realisiert.
Weiterhin schließt der generische Cache die Lücke zum Standard-SAP Cache, welcher durch einige Möglichkeiten umgangen werden kann und wird.
Die generelle Schwierigkeit, dass die Daten im Cache aktuell sind, liegt in Verantwortung des Programmierers. Deshalb sollte vor längeren Verarbeitungsblöcken der Methodenaufruf RESET erfolgen.
Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion oder schreibt mir direkt an michael.krause@cgi.com
Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.