MSDN Magazin > Home > Ausgaben > 2001 > August >  Bringen Sie Ihre Datenformate und XML unter ein...
Bringen Sie Ihre Datenformate und XML unter einen Hut
Veröffentlicht: 27. Jan 2002 | Aktualisiert: 15. Jun 2004
Von Johnny Papa

Der DataManager ist eine Klasse, die angeforderte Daten in einem beliebigen Format liefert. Hierdurch können über eine gemeinsame Komponente unterschiedlichste Clients ihre Daten erhalten. Und ganz nebenbei haben Sie auch XML-Daten in Ihrem Fundus.

Auf dieser Seite

Die Arbeit mit dem DataManager Die Arbeit mit dem DataManager
Im DataManager Im DataManager
Herstellung der Verbindung Herstellung der Verbindung
Ausführung des SQL-Befehls Ausführung des SQL-Befehls
XML-Formatierung XML-Formatierung
Fazit Fazit

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild03



Stellen Sie sich vor, Ihr Boss kommt zur Tür herein und sagt: "Wir müssen dafür sorgen, dass unsere Anwendung XML spricht." Nun ist XML zwar ein solides Datenformat, aber was ist mit den vielen verschiedenen Datenformaten, die in Ihrer Firma bereits benutzt werden? Die wichtigsten Anwendungen haben vermutlich eine Datenzugriffsschicht, die Daten in einer Datenbank abspeichert oder aus der Datenbank ausliest und an die Schicht mit der Geschäftslogik zurückschickt. In welcher Form liegen diese Daten vor? Wenn Sie mit ADO-Recordsets arbeiten (ActiveX Data Objects), dann wird Ihre Geschäftskomponente auf die ADOR-Objektbibliothek angewiesen sein. Vielleicht ist das im speziellen Fall wünschenswert, vielleicht aber auch nicht. Außerdem haben Sie vielleicht schon in ASP-Code investiert, der die Daten als Array erwartet, damit die Geschäftskomponente von ADO unabhängig sein kann und die Daten schneller durchlaufen kann.
Außerdem möchten Sie noch XML einsetzen, damit die Geschäftskomponente die Daten leichter an andere Systeme auf anderen Plattformen übermitteln kann. Offensichtlich müssen die Daten aus verschiedenen Gründen verschiedene Formen annehmen. Wäre es nicht schön, wenn es eine Datenzugriffskomponente gäbe, die sich um die Verwaltung der Daten kümmert und die Daten in den gewünschten Formaten liefern könnte?
Ich möchte Ihnen nun zeigen, wie Sie genau das erreichen können - nämlich durch die Entwicklung einer COM+-Komponente, die sich von jeder Windows-Anwendung aus benutzen lässt und Daten in verschiedenen Formaten liefert, zum Beispiel als ADO-Recordset, als zweidimensionales Array oder als XML. Die DataManager-Komponente, die ich im Folgenden beschreiben möchte, lässt sich auch in mehreren Geschäftskomponenten benutzen. Ich beginne mit der Demonstration, wie der DataManager die Daten ermittelt. Dann greife ich einige wichtige Teile des DataManager-Codes heraus, damit Sie sich anschauen können, wie er funktioniert. Falls Sie den Code schon nebenbei ausprobieren möchten, während Sie diesen Artikel lesen, finden Sie die DataManager-Komponente samt Quelltext und einer Testschnittstelle auf der Begleit-CD dieses Hefts. Der Code wurde unter Windows NT 4.0 entwickelt und benutzt daher den MTS (Microsoft Transaction Server) und den SPM (Shared Property Manager). Der Code funktioniert zwar unter Windows 2000, diese beiden Komponenten werden dort aber durch COM+-Dienste ersetzt.

Die Arbeit mit dem DataManager

Lassen Sie uns mit einem Blick auf eine einfache Anwendung beginnen, die ich zum Test der DataManager-Komponente in Visual Basic geschrieben habe. Das Testprogramm bietet eine Schnittstelle an, in der man die Daten aus den drei Formaten betrachten kann. Wenn die Daten als ADO-Recordset oder als Array geliefert werden, so werden sie in ein ganz normales Tabellen-Steuerelement (Grid) geladen. Treffen sie dagegen in XML-Form ein, werden sie in ein WebBrowser-Steuerelement geladen. In diesem Beispiel gibt es auch eine Geschäftskomponente, die zwischen der Schnittstelle und der Datenzugriffskomponente ihren Platz findet. Insgesamt ist der Umgang mit den Daten wesentlich einfacher, wenn man sie in einem bekannten und gängigen Format anzeigen kann.
Im Testprogramm gibt es ein Kombinationsfeld, in dem man festlegen kann, in welcher Form die Daten geliefert werden sollen. Die Daten werden dann auf der ersten Seite in einer Tabelle (Grid) oder auf der zweiten Seite in einem WebBrowser-Steuerelement geladen. Alle Steuerelemente dieser Anwendung sind im Lieferumfang von Visual Basic enthalten, so dass sich aus deren Verfügbarkeit keine Probleme ergeben sollten. Etwaige Fehlernachrichten werden im Textfeld der dritten Seite angezeigt. Bild B1 zeigt Daten aus der pubs-Datenbank vom SQL Server, die als ADO-Recordset geliefert wurden.

Bild01

B1 So zeigt das Beispielprogramm ADO-Recordsets an.

Wenn Sie die Daten aus einem Array auslesen möchten, wählen Sie im Kombinationsfeld 2-D Array und klicken dann OK an. Sie werden dieselben Daten wie in Bild B1 sehen. Während in Bild B1 aber noch die verschiedenen Überschriften der Tabellenspalten zu sehen sind (die Feldnamen ProductID, ProductName und so weiter), werden die Daten aus dem zweidimensionalen Array ohne diese Spaltenüberschriften angezeigt. Wenn Sie ein Array anfordern, erhalten Sie nicht die Metadaten, die für einen ADO-Recordset zur Verfügung stehen. Daher können Sie auch nicht die Feldnamen als Spaltenüberschriften sehen.
Natürlich können Sie diesen Lösungsansatz so abändern, dass auch die Array-Form Metadaten enthält. Tatsächlich können Sie ja den DataManager so erweitern, dass er jedes gewünschte Datenformat beherrscht. An welcher Stelle Sie das tun können, werde ich bei der Besprechung des Codes noch erwähnen.
Sofern Sie die Daten in einem XML-Format anfordern, sieht das Ergebnis so aus wie in Bild B2. Die XML-Daten umfassen auch die Feldnamen, mit denen die einzelnen Spalten beschrieben werden. Auch die eigentlichen Nutzdaten sind dabei (notfalls blättern Sie etwas in der Darstellung).

Bild02

B2 Die Daten in XML-Form.

Damit steht also fest, wie die Daten im Beispielprogramm angezeigt werden. Lassen Sie uns nun untersuchen, wie der DataManager in einer einzigen COM+-Komponente untergebracht wird, die alle drei Datenformate liefern kann.

Im DataManager

Die DataManager-Komponente hat eine öffentliche Klasse, die C_DataManager heißt. Zur Ausführung eines SQL-Befehls rufen Sie die Methode ExecuteSQL von C_DataManager auf. ExecuteSQL kümmert sich um die Aufnahme der Verbindung zur entsprechenden Datenbank, leitet den SQL-Befehl weiter, nimmt die Daten an, formatiert sie nach Wunsch und leitet sie an die aufrufende Anwendung weiter.
Die Methode ExecuteSQL hat vier Parameter:

  • sSQL, der auszuführende SQL-Befehl

  • sINIAppName, der Name der Geschäftskomponente, die den DataManager aufruft

  • sINIFullPath, der vollständig qualifizierte Pfad der Geschäftskomponente, die den DataManager aufruft

  • lDataReturnType, das Format, in dem die Daten ausgeliefert werden sollen

Der DataManager geht davon aus, dass die Geschäftskomponente eine INI-Datei benutzt, in der die spezielle Datenbankkonfiguration der Anwendung zu finden ist. Immerhin können mehrere verschiedene Geschäftsanwendungen denselben DataManager benutzen und es muss die Möglichkeit geben, den DataManager darüber zu informieren, welche Datenbankwerte er benutzen soll. Ich benutze hier eine Methode, bei der für die Geschäftskomponente eine INI-Datei mit den erforderlichen Angaben über die Datenbank angelegt wird. In der INI-Datei wird zum Beispiel der Datenbanktyp angegeben, der Name des Datenbankservers, der Name der Datenbank, die User-ID der Anwendung und das Kennwort - im Prinzip die Angaben aus einem ADO-Verbindungsstring. Diese INI-Datei namens TestBL.ini ist ebenfalls auf der Begleit-CD dieses Hefts zu finden.
Für die Parameter sINIAppName und sINIFullPath geben Sie den Namen der INI-Datei an (ohne die Endung .ini) und ihren vollständigen Pfad. Für den Parameter sSQL geben Sie den auszuführenden SQL-Befehl an. Und mit dem Parameter lDataReturnType legen Sie das Format fest, in dem die Daten geliefert werden sollen. Damit das möglichst einfach wird, definiert der DataManager die entsprechenden Werte, die von der Geschäftskomponente an ExecuteSQL übergeben werden können:

Public Enum enumDataReturnType 
    geDataReturnType_2DArray 
    geDataReturnType_ADOR 
    geDataReturnType_XML 
End Enum

Herstellung der Verbindung

Die Methode ExecuteSQL ruft die Routine SetConnectionParameters auf (Listing L1), die wiederum die INI-Datei der Geschäftskomponente ausliest. Die Routine erfährt aus der INI-Datei die Angaben zur Datenbank und bringt diese in einem lokalen Visual Basic-Typ unter, damit Sie später die Verbindung zur Datenbank aufnehmen können.

L1 SetConnectionParameters

Private Sub SetConnectionParameters(Optional sINIAppName As Variant, _ 
                                    Optional sINIFullPath As Variant) 
... 
    With m.udtConnectionParameters 
        ' -  Datenquelle 
        .sDataSource = GetINIValue(m.oError, .sINIConnectionSection, _    
            "DataSource", m.sINIAppName, m.sINIFullPath) 
        ' -  Datenbankname 
            .sDatabaseName = GetINIValue(m.oError, _    
            .sINIConnectionSection, "DatabaseName", m.sINIAppName, _ 
            m.sINIFullPath) 
        ' -  Datenbankanbieter 
        .eDatabaseProvider = CInt(GetINIValue(m.oError, _  
            .sINIConnectionSection, "DatabaseProvider", _ 
            m.sINIAppName, m.sINIFullPath)) 
        ' -  Anwendungs-UserID 
        .sUserID = GetINIValue(m.oError, .sINIConnectionSection, _ 
            "AppID", m.sINIAppName, m.sINIFullPath) 
        ' -  Anwendungskennwort 
        .sPassword = GetINIValue(m.oError, .sINIConnectionSection, _ 
            "AppPassword", m.sINIAppName, m.sINIFullPath) 
...

Die Funktion GetINIValue (Listing L2) überprüft zuerst, ob die Werte im MTS SPM verfügbar sind. Ist dies der Fall, liest GetINIValue die Werte dort aus, ohne die INI-Datei zu öffnen. Andernfalls liest GetINIValue die INI-Datei, schließt die INI-Datei wieder, speichert die Werte im SPM ab und übergibt die Werte auch als Rückgabewert an den Aufrufer. Der Rückgriff auf den SPM verhindert, dass bei jedem Aufruf von ExecuteSQL die entsprechende INI-Datei ausgewertet werden muss.

L2 GetINIValue

Public Function GetINIValue(oError As C_ErrorManager, _ 
    ByVal sSection As String, ByVal sKey As String, ByVal sINIAppName _ 
    As String, Optional sINIFullPath As Variant) As Variant 
    Const sPROC_NAME As String = msOBJ_NAME & ".GetINIValue" 
    On Error GoTo Err 
    Const sPROPERTY_MANAGER As String = _ 
        "MTxSpm.SharedPropertyGroupManager.1" 
    Dim oINI As C_INIManager 
    Dim oSharedPropertyGroupManager As SharedPropertyGroupManager 
    Dim oSharedPropertyGroup As SharedPropertyGroup 
    Dim oSharedProperty As SharedProperty 
    Dim bGroupAlreadyExists As Boolean 
    Dim bPropertyAlreadyExists As Boolean 
    Dim vValue As Variant 
    ' -  Propertygruppe ermitteln 
    Set oSharedPropertyGroupManager = CreateObject(sPROPERTY_MANAGER) 
    Set oSharedPropertyGroup = _ 
        oSharedPropertyGroupManager.CreatePropertyGroup(sINIAppName & "." 
        & "INIValues", LockSetGet, Process, bGroupAlreadyExists) 
    Set oSharedProperty = oSharedPropertyGroup.CreateProperty _ 
        (sSection & "." & sKey, bPropertyAlreadyExists) 
    If bPropertyAlreadyExists Then 
        GetINIValue = oSharedProperty.Value 
    Else 
        Set oINI = New C_INIManager 
        ' -  Standardpfad durch angegebenen INI-Pfad 
        '    ersetzen, sofern angegeben. 
        If HasValue(sINIFullPath) Then 
            oINI.FilePath = sINIFullPath & "\" & sINIAppName & ".ini" 
        End If 
        vValue = oINI.Value(sSection, sKey) 
        oSharedProperty.Value = vValue 
        GetINIValue = vValue 
    End If 
    ' -  Aufräumen 
    Set oINI = Nothing 
    Set oSharedProperty = Nothing 
    Set oSharedPropertyGroup = Nothing 
    Set oSharedProperty = Nothing 
Exit Function 
Err: 
    oError.Push Err, Err.Source, Error, sPROC_NAME 
...

Ausführung des SQL-Befehls

Sobald die Routine SetConnectionParameters die Datenbankangaben ermittelt hat, legt ExecuteSQL ein ADO-Verbindungsobjekt an und öffnet es. Dann legt sie einen ADO-Recordset an und öffnet ihn unter Angabe des offenen Connection-Objekts. Der Recordset wird als clientseitiger Recordset geöffnet und nach Gebrauch vom Connection-Objekt getrennt, wie aus Listing L3 hervorgeht.

L3 Auszug aus ExecuteSQL

' -  Erstelle einen unverbundenen Recordset 
Set oRs = New ADODB.Recordset 
With oRs 
    ' - Erstelle einen unverbunden Recordset, da er im 
    ' - Gegensatz zum regulären Recordset die vollstän- 
    ' - digen Metadaten-Properties enthält. 
    Set .ActiveConnection = oCn 
    .CursorLocation = adUseClient 
    .CursorType = adOpenStatic 
    .LockType = adLockBatchOptimistic 
    ' -  Öffne den Recordset 
    .Open Source:=sSQL, Options:=adCmdText 
    ' -  Trenne die Verbindung 
    Set .ActiveConnection = Nothing 
End With 
' ************************************************** 
' -  Setze den Recordset als Rückgabewert 
' ************************************************** 
If lDataReturnType = geDataReturnType_ADOR Then 
    Set ExecuteSQL = oRs 
Else 
    ExecuteSQL = ConstructReturnData(oRs, lDataReturnType) 
End If

Zum Abschluss werden die Daten, wie ebenfalls in Listing L3 ersichtlich, noch in die gewünschte Form gebracht. Fordert die aufrufende Anwendung einen ADO-Recordset an, gibt man einfach den Recordset zurück. Andernfalls wird zur Konvertierung noch die Funktion ConstructReturnData aufgerufen (Listing L4). Sie formatiert die Daten und gibt das Ergebnis an den Aufrufer zurück.
Die Funktion ConstructReturnData überprüft einfach, welches Datenformat angefordert wird, und liefert entweder mit Hilfe der GetRows-Methode des Recordsets ein zweidimensionales Array ab oder bringt die Daten mit BuildXML in XML-Form. Die Funktion BuildXML ruft die Methode BuildXML der Klasse C_XML auf, die den ADO-Recordset in XML-Form konvertiert. Im Prinzip geht sie über die Felder des ADO-Recordsets, um die erforderlichen Metadaten zu ermitteln, und läuft dann in einer Schleife über die Zeilen und Felder, um die eigentlichen Nutzdaten auszulesen und umzuwandeln. Dabei erstellt sie die XML-Form der Daten mit Hilfe des MSXML.DOMDocument-Objekts.

L4 ConstructReturnData

Private Function ConstructReturnData(ByRef oRs As ADODB.Recordset, _ 
    ByRef lDataReturnType As enumDataReturnType) As Variant _ 
    Const sPROC_NAME As String = msOBJ_NAME & ".ConstructReturnData" 
    On Error GoTo Err 
    ' -  Überprüfe, in welcher Form der Aufrufer die  
    ' -  Daten angefordert hat 
    If IsObject(oRs) Then 
        If oRs.State = adStateOpen Then 
            Select Case lDataReturnType 
                Case geDataReturnType_2DArray 
                    If Not oRs.EOF Then 
                        ConstructReturnData = oRs.GetRows 
                    End If 
                Case geDataReturnType_XML 
                    ConstructReturnData = BuildXML(oRs) 
            End Select 
        End If 
    End If 
Exit Function 
Err: 
    m.oError.Push Err, Err.Source, Error, sPROC_NAME 
    m.oError.RaiseLast 
End Function

XML-Formatierung

Zur Formatierung der XML-Daten legt die Methode BuildXML zuerst das <xml>-Wurzeletikett an. Dann folgt das <metadata>-Etikett, das die gesamten Feldattribute aufnimmt. Innerhalb des <metadata>-Etiketts wird ein <fields>-Etikett angelegt, das die <field>-Etiketten aufnimmt:

' -  Lege ein XML-Dokumentelement an 
Set m.oXML.documentElement = m.oXML.createElement("xml") 
' -  With <xml> 
With m.oXML.documentElement 
    ' ******************************************* 
    ' -  Lege die Metadaten an 
    ' ******************************************* 
    ' -  Lege den Metadatenknoten an 
    .appendChild CreateNode("metadata") 
    ' -  Lege die Metadaten-Feldknoten an 
    .childNodes(lMetaDataTag).appendChild CreateNode("fields")

Pro Feld aus dem Recordset gibt es ein <field>-Etikett. Dieses Etikett hat Attribute wie Name, Type, IsNullable und andere. Listing L5 zeigt, wie diese Attribute angelegt und an das entsprechende <field>-Etikett angehängt werden.

L5 So werden die Metadaten zusammengestellt.

' -  With <fields> 
With .childNodes(lMetaDataTag).childNodes(lMetaDataFieldsTag) 
    ' -  Schleife durch die Recordset-Felder, zur 
    ' -  Erstellung der Metadaten-Feldknoten 
    For lCol = 0 To oRs.Fields.Count - 1 
' -  Lege den field-Knoten an 
.appendChild CreateNode("field") 
' -  Lege alle Metadaten-Attribute an 
' -  With <field>-attributes 
With .childNodes(lCol).Attributes 
    ' -  Lege Indexattribut an 
    .setNamedItem CreateAttribute("Index", lCol) 
    ' -  Lege Namensattribut an 
    .setNamedItem CreateAttribute("Name", oRs.Fields(lCol).Name) 
...

Daran schließt sich ein <data>-Etikett an, das ein <rows>-Etikett enthält. Das <rows>-Etikett enthält für jede Datenzeile ein <row>-Etikett. Jedem <row>-Etikett wird ein Indexattribut zugeordnet, damit sich die betreffende Zeile eindeutig identifizieren lässt. In <row> ist auch ein <fields>-Etikett zu finden, das für jedes entsprechende Feld aus dem ADO-Recordset ein <field>-Element enthält. Listing L6 zeigt, wie die Daten im XML-Dokument zusammengestellt werden.

L6 So werden die Daten ins XML-Dokument aufgenommen.

' -  Lege den Datenknoten an 
.appendChild CreateNode("data") 
' -  Lege den Datenzeilenknoten an 
.childNodes(lDataTag).appendChild CreateNode("rows") 
oRs.MoveFirst 
' -  With <rows> 
With .childNodes(lDataTag).childNodes(lDataRowsTag) 
    Do While Not oRs.EOF 
        lRow = oRs.AbsolutePosition - 1 
        ' -  Lege den Zeilenknoten an 
        .appendChild CreateNode("row") 
        ' -  Lege das Indexattribut an 
        .childNodes(lRow).Attributes.setNamedItem _ 
            CreateAttribute("Index", lRow) 
        ' -  Lege den Felderknoten an 
        .childNodes(lRow).appendChild CreateNode("fields") 
        ' -  With <fields> 
        With .childNodes(lRow).childNodes(lDataRowsRowFieldsTag) 
            ' - Gehe in einer Schleife über die Felder 
            ' - des Datensatzes und erstelle die 
            ' - Datenfeldknoten 
            For lCol = 0 To oRs.Fields.Count - 1 
                ' - Lege einen Feldknoten an 
               .appendChild CreateNode("field") 
                ' -  With <field> 
                With .childNodes(lCol) 
                    ' -  Lege ein Indexattribut an 
                    .Attributes.setNamedItem CreateAttribute _ 
                        ("Index", lCol) 
                    ' -  Lege ein Namensattribut an 
                    .Attributes.setNamedItem CreateAttribute _ 
                        ("Name", oRs.Fields(lCol).Name) 
                    ' -  Felddaten nicht vergessen 
                    .Text = oRs.Fields(lCol).Value & "" 
                End With 
            Next 
        End With 
        oRs.MoveNext 
    Loop 
End With 
End With 
sXML = m.oXML.xml 
Set oRs = Nothing 
BuildXML = sXML

Sie können die XML-Daten in praktisch jedem Format zurückgeben, das Sie für angemessen halten. Ich habe die Metadaten und die Daten im XML-Code voneinander getrennt. Man könnte die <metadata>-Etiketten auch weglassen, indem man die Metadatenattribute in den geschachtelten Feldelementen des <data>-Etiketts unterbringt. Oder Sie lassen die Metadaten einfach vollständig weg, falls diese Angaben nicht gebraucht werden. Das ist das Schöne an XML - wie Sie nämlich die Daten formatieren, bleibt Ihnen überlassen.

Fazit

Ziel der Entwicklung ist eine Datenzugriffskomponente, die von den aufrufenden Anwendungen unabhängig ist. Daher können mehrere verschiedene Geschäftskomponenten den DataManager benutzen, wobei zum Beispiel jede einen anderen Datenbankserver anspricht und jede die Daten in einem anderen Format anfordert. Es bietet sich zum Beispiel an, den DataManager so zu erweitern, dass sich das gewünschte XML-Format dynamisch festlegen lässt. Dann kann man dem DataManager in jedem Einzelfall genau mitteilen, wie der XML-Code strukturiert sein soll.
Falls Sie den Code ausprobieren möchten, schauen Sie bitte auf der Begleit-CD dieses Hefts nach. Dort finden Sie den Code vom DataManager und das Testprogramm. Zusätzlich brauchen Sie noch Visual Basic und die pubs-Datenbank vom SQL Server. Natürlich können Sie den Code auch so abändern, dass er mit anderen Datenbanken funktioniert.

Page view tracker