automatisierte Datenversorgung mit Pseudo-APIs

APIs oder vollständig ausgesprochen Application Programming Interfaces sind schon eine wirklich feine Sache. Sie können relativ einfach in bestehende Programme eingebunden werden und stellen über eine vordefinierte und stabile Schnittstelle die Datenversorgung zu einem bestimmten fachlichen Sachverhalt sicher. Ebenso vorteilhaft ist, dass APIs ihre Antworten einem nicht in Form von undefinierbarem Datenbrei vor die Füße werfen, der erstmal aufwändig geparst werden muss, sondern fein säuberlich als einfach verdauliche XML / JSON Datenstrukturen.

Doch was tun wir, wenn es in der fachlichen Domäne, in der wir uns bewegen, keine bestehende API gibt? Müssen wir in solchen Fällen auf automatisierte Datenversorgung verzichten? Nicht immer. Je nach Sachverhalt gibt es Möglichkeiten, sich eine eigene Pseudo-API zu bauen.

Lasst uns das Thema an einem konkreten Beispiel aus meiner Praxis anschauen. In einem Kundenprojekt gibt es nämlich die Notwendigkeit, Fusionen von Krankenkassen zeitnah mitzubekommen. Liegt eine Krankenkassenfusion vor, muss in diesem Fall eine bestimmte fachliche Anpassung am zentralen Schlüsselverzeichnis der Anwendung vorgenommen werden. Die Anpassung an sich ist nicht das Problem. Doch wie stellt man sicher, dass man jede Krankenkassenfusion überhaupt mitbekommt? Natürlich kann man in regelmäßigen Abständen eine kurze Internetrecherche betreiben – Krankenkassenfusionen erfolgen schließlich nicht von heute auf morgen, sondern haben einigen zeitlichen Vorlauf. Doch jeder, der mit Softwareentwicklern zu tun hat, weiß eigentlich schon vorab, dass so eine Lösung nicht in Frage kommt. Softwareentwickler haben nun mal eine gesunde Abneigung gegen monotone und wiederkehrende Tätigkeiten – auch dann, wenn sie nicht einen selbst betreffen.

Datengrundlage

Eine Zeitlang fand ich für diese Problematik jedoch keine wirklich zufriedenstellende Lösung. So hatte ich mit meinem Kunden die Vereinbarung getroffen, dass er das Thema Fusionen selbst im Blick behält und mich im Fusionsfall informiert. Doch eines Tages fiel mir eine einfach zu realisierende Lösung plötzlich ein. Eine der Aufgaben in meiner hauptberuflichen Tätigkeit darin besteht, regelmäßig Beitragsdaten von Krankenkassen in SAP HCM zu importieren, um deren Beitragsdaten für Gehaltsabrechnungsläufe aktuell zu halten. Diese Daten werden regelmäßig vom Unternehmen namens ITSG GmbH veröffentlicht, das IT-Dienstleistungen für Krankenkassen erbringt. Die Analyse dieser XML-Dateien hat ergeben, dass neben den Beitrags- und Konto- und Stammdaten aller Krankenkassen, dort mit etwas XPath-Akrobatik herausgefunden werden kann, welche Krankenkasse wann mit welcher Kasse fusioniert hat.

Das würde bedeuten, dass wenn ich eine Kasse aufbaue, die selbstständig die oben genannte Datei herunterlädt, sie entzippt, einliest, ein XPath-Ausdruck darauf anwendet und das Ergebnis zurückgibt, ich mehr oder weniger eine Pseudo-API habe.

Warum Pseudo-API?

  • weil die nicht webbasiert, sondern ausschließlich lokal aufruf- und ausführbar ist.
  • weil sie keine eigene Datenhaltung hat, sondern die Daten vorab aus dem öffentlich verfügbaren Download bezieht.

Warum Pseudo-API?

  • weil sie die Komplexität der fachlichen Logik wegabstrahiert und eine einfach zu nutzende Schnittstelle bereitstellt.

Implementierung

Schauen wir uns mal die Implementierung der Klasse an und gehen die Methode für Methode durch.

Konstruktor-Methode (Class_Initialize())

Die Konstruktormethode wird automatisch aufgerufen, wenn ein Objekt der API-Klasse erzeugt wird. Sie prüft als erstes, ob das Arbeitsverzeichnis der Klasse, das sich unterhalb der AddIn-Verzeichnisses befindet, wirklich existiert. Denn ohne dieses Arbeitsverzeichnis kann die Klasse ihren Dienst nicht verrichten.

Private c_strArbeitsverzeichnisPfad     As String
Private c_strPfadZipArchiv              As String
Private c_objXmlBeitragsdatei           As MSXML2.DOMDocument
Private c_objBrowser                    As MSXML2.ServerXMLHTTP
Private c_objShell                      As Object

Private Const c_strURL                  As String = "https://stammdatendatei.gkv-ag.de/Stammdatendatei.zip"
Private Const c_strVerzeichnisName      As String = "api"
Private Const c_strZipArchivDateiname   As String = "Archiv.zip"


Private Sub Class_Initialize()
    
    c_strArbeitsverzeichnisPfad = ThisWorkbook.Path & "\" & c_strVerzeichnisName & "\"
    
    If Dir(c_strArbeitsverzeichnisPfad, vbDirectory) = "" Then
        Err.Raise 512, TypeName(Me), "Das Arbeitsverzeichnis " & c_strArbeitsverzeichnisPfad & " existiert nicht!"
    End If
    
    c_strPfadZipArchiv = c_strArbeitsverzeichnisPfad & c_strZipArchivDateiname
    
    Call arbeitsverzeichnisBereinigen()
    Call beitragsdatendateiHerunterladen()
    
End Sub

Aufräum-Methode (arbeitsverzeichnisBereinigen())

Der Name dieser Methode verrät schon, was diese tut. Bevor die neue Beitragsdatendatei heruntergeladen und entzippt wird, wird das Verzeichnis erstmal bereinigt, indem alte zip-Archive und alte Beitragsdatendatei gelöscht werden.

Private Sub arbeitsverzeichnisBereinigen()
    
    Dim strXmlDateiName As String
    
    On Error GoTo fehlerBeimLoeschen
    
    If Dir(c_strPfadZipArchiv) <> "" Then
        Kill c_strPfadZipArchiv
    End If
    
    Do While getXmlDateiName() <> ""
        Kill c_strArbeitsverzeichnisPfad & getXmlDateiName()
    Loop
        
    Exit Sub
    
fehlerBeimLoeschen:
    Err.Raise 512, TypeName(Me), "Beim Bereinigen des Arbeitsverzeichnisses ist ein Fehler aufgetreten!" & vbCrLf & Err.Description
    
End Sub

Herunterladen-Methode (beitragsdatendateiHerunterladen())

Diese Methode ist das Herzstück der Klasse und übernimmt die tatsächliche Datenbeschaffung. Dies erfolgt, indem ein Browserobjekt den Request an die Download-URL von ITSG absetzt. Die vom Server empfangenen Daten (das Zip-Archiv), die in Form eines Bytestroms vorliegen, werden im Arbeitsverzeichnis abgelegt. Anschließend wird ein Shellobjekt aufgebaut, welches mit Hilfe eines Powershell-Befehls das heruntergeladene Archiv entzippt und somit die XML-Datei im Arbeitsverzeichnis ablegt.

Am Ende wartet die Methode eine Sekunde, bevor die XML-Datei ins DomObjekt eingelesen wird. Ab diesem Zeitpunkt befindet sich die Beitragsdatendatei im Hauptspeicher, sodass darauf tatsächliche Selektionen in Form von XPath-Ausdrücken abgesetzt werden können.

Private Sub beitragsdatendateiHerunterladen()
    
    Dim bytDateiinhalt() As Byte
    Dim strAufrufparameter As String
    
    
    Set c_objBrowser = New MSXML2.ServerXMLHTTP
    
    c_objBrowser.Open "GET", c_strURL, False
    c_objBrowser.send
    
    If c_objBrowser.Status <> 200 Then
        Err.Raise 512, TypeName(Me), "Beim Herunterladen des Archivs ist ein Problem aufgetreten"
    End If


    bytDateiinhalt = c_objBrowser.responseBody

    Open c_strPfadZipArchiv For Binary As #1
        Put #1, , bytDateiinhalt()
    Close #1

    Set c_objShell = CreateObject("WScript.Shell")
    strAufrufparameter = "powershell.exe Expand-Archive -Path '" & c_strPfadZipArchiv & "' -DestinationPath '" & c_strArbeitsverzeichnisPfad & "'"
    c_objShell.Run strAufrufparameter, 0

    Application.Wait Now + TimeSerial(0, 0, 1)


    Set c_objXmlBeitragsdatei = New MSXML2.DOMDocument
    c_objXmlBeitragsdatei.Load (c_strArbeitsverzeichnisPfad & getXmlDateiName())
    If c_objXmlBeitragsdatei.parseError <> 0 Then
        Err.Raise 512, TypeName(Me), "Beim Laden der Beitragsdatei ist ein Fehler aufgetreten!" & vbCrLf & Err.Description
    End If
        
End Sub

Private Function getXmlDateiName() As String
    getXmlDateiName = Dir(c_strArbeitsverzeichnisPfad & "*.xml")
End Function

Fusionserkennungsmethode (LiegtFusionVor())

Aktuell ist es die einzige fachliche Methode dieser API, die nur die Aufgabe hat, die Info zurückzugeben, ob zur übergebenen Betriebsnummer eine Fusion bekannt. ist. Die liegt dann vor, wenn es zur gegebenen Betriebsnummer mindestens einen Knoten Einzugsstelle gibt, der ein Attribut nachfolge_bn aufweist.

Da sich die komplette Beitragsdatendatei im Speicher befindet, lassen sich natürlich auch komplexere Selektionen durchführen. So wäre es beispielsweise möglich, zur gegebenen Betriebsnummer die Nachfolgekasse zu ermitteln. Dabei wäre jedoch zu beachten, dass es zur übergebenen Betriebsnummer zwar eine Nachfolgekrankenkasse geben kann, die jedoch wiederum selbst bereits eine Fusion hinter sich haben kann. Am einfachsten kann diese Problematik mit Hilfe von rekursiven Funktionsaufrufen gelöst werden, indem die Beitragsdatendatei anhand der Betriebsnummer solange gelesen wird, bis der Knoten /Stammdatendatei/Einzugsstelle gefunden wird, der kein Attribut nachfolge_bn aufweist.

Public Function LiegtFusionVor(betriebsnummer As String) As Boolean
    
    Dim nodKasse As MSXML2.IXMLDOMNode
            
    'nach dieser Betriebsnummer wird dann in der ITSGXML gesucht
    Set nodKasse = c_objXmlBeitragsdatei.SelectSingleNode("/Stammdatendatei/Einzugsstelle[@bbnr=" & Chr(39) & betriebsnummer & Chr(39) & "]")
    If nodKasse Is Nothing Then
        LiegtFusionVor = False
        Exit Function
    End If

    If nodKasse.SelectSingleNode("@nachfolge_bn") Is Nothing Then
        LiegtFusionVor = False
        Exit Function
    End If

    LiegtFusionVor = True

End Function

Nutzung der Pseudo-API

Die Einfachheit der API-Nutzung erkennt man an diesem Codeschnipsel. Damit erstellt man eine Instanz der API und ruft übergibt der Methode LiegtFusionVor() die entsprechende Betriebsnummer. Daraufhin erhält man die Antwort, ob eine Fusion vorliegt. Spoileralarm – der Methodenaufruf liefert TRUE zurück – die Kasse hat das hat sie und zwar Ende 2022. Da ist nämlich die BKK Stadt Augsburg in Audi BKK aufgegangen.

Dim clsAPI As klasseKrankenkassenfusionInfoAPI.KrankenkassenfusionInfoAPI
    
Set clsAPI = klasseKrankenkassenfusionInfoAPI.getKlasseninstanz()
    
MsgBox clsAPI.LiegtFusionVor("81211334")

In meinem konkreten Anwendungsfall sieht die Nutzung der Pseudo-API natürlich nicht auf der Einzelsatzebene aus. Stattdessen wird beim Start der Hauptanwendung das zentrale Schlüsselverzeichnis gelesen und Kasse für Kasse durchiteriert. Für jede Kasse wird die Methode LiegtFusionVor() mit der Betriebsnummer der Kasse aufgerufen. Alle Kassen, für die eine Fusion vorliegt, werden protokolliert und als ToDo angezeigt.

Fazit

Wie man sieht, ist der Aufbau einer Pseudo-API recht einfach und ermöglicht eine automatisierte Datenversorgung. Die Voraussetzung hierfür natürlich ist, dass es entsprechende öffentliche Downloadmöglichkeiten gibt, idealerweise natürlich in strukturierter Form.

Die vorgestellte API ist noch nicht ganz optimal realisiert – so wird der Donwload / das Enzippen immer durchgeführt, wenn eine neue Instanz der Klasse erstellt wird. Dies kostet natürlich Laufzeit und Datenverkehr – zumal die ITSG die Datei, soweit ich es mitbekommen habe, ca. 2 Mal im Monat aktualisiert. So werde ich bei der nächsten Anpassung die Logik etwas aufbohren, indem ich zuerst prüfe, ob die aktuell heruntergeladene Datei mit der zuletzt herunterladenden übereinstimmt. Ist es der Fall, dann kann die bisherige Datei nach wie vor verwendet werden. Dies werde ich entweder realisieren, indem ich den Verhalt anhand der Hashwerte durchführe oder anhand des im Archiv hinterlegten Namen der XML-Datei, an dem man seinen Erstellungszeitpunkt ohne Weiteres erkennen kann.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert