Von Version zu Version

Neulich wollte ich meine Shopsoftware aktualisieren. Allerdings ging dies nicht mit den vom Hersteller dafür bereitgestellten Plug-In – zumindest nicht mit allen Daten, die ich von der alten in die neue Version überführen wollte. Also musste ich manuell ermitteln, welche Daten der alten Version benötigt werden, damit die neue Version läuft. Damit erhielt ich recht schnell eine lauffähige Version des neuen Systems. Allerdings kam mir dann etwas dazwischen, wodurch zum alten, noch aktiven System wieder neue Kunden und Bestellungen hinzukamen – die Arbeit war also umsonst. Um das Übertragen der Daten beim nächsten Mal einfacher zu gestalten, wollte ich nun das alte und das neue Datenmodell nun automatisch analysieren und die SQL-Befehle erstellen lassen, um diese danach ebenfalls automatisch ausführen zu können. Wie das gelingt, zeigt der vorliegende Beitrag.

Bei der Shopsoftware handelt es sich um ein Produkt namens Shopware. Es verwendet eine MySQL-Datenbank.

Die hier vorgestellten Techniken lassen sich jedoch auch auf andere Systeme übertragen – und auch auf andere Anwendungen.

Auch MySQL als Datenbanksystem kann durch andere Systeme ersetzt werden – Sie müssen dann schlicht den Treiber in der Verbindungszeichenfolge austauschen und gegebenenfalls ein paar Abfragen auf spezifische Eigenarten des jeweiligen Dialekts anpassen. Voraussetzung für die Nutzung der hier vorgestellten Technik ist der Zugriff auf die Datenbank. Ob dies möglich ist, müssen Sie mit dem jeweiligen Provider klären.

Zugriff per ADODB

Damit wir per ODBC auf die Daten der beiden zu vergleichenden MySQL-Datenbanken zugreifen können, fügen wir der Datenbank einen Verweis auf die Bibliothek Microsoft ActiveX Data Objects 2.8 Library zum VBA-Projekt einer neuen Access-Datenbank hinzu (siehe Bild 1).

Verweis auf die ADODB-Bibliothek

Bild 1: Verweis auf die ADODB-Bibliothek

Tabellen abgleichen

Als Erstes wollen wir die Tabellen der beiden Datenbanken abgleichen, also prüfen, ob es in der ersten Datenbank Tabellen gibt, die nicht in der zweiten Datenbank vorkommen und umgekehrt.

Dazu legen wir zwei Konstanten mit den Verbindungszeichenfolgen der beiden Datenbanken in einem neuen Modul namens mdlADODB an:

Const cStrVerbindungszeichenfolgeAlt As String = "DRIVER={MySQL ODBC 5.3 ANSI Driver};SERVER=xxx.xxx.xxx.xxx;DATABASE=<Datenbankname>;UID=<Benutzername>;PWD=<Kennwort>"
Const cStrVerbindungszeichenfolgeNeu As String = "DRIVER={MySQL ODBC 5.3 ANSI Driver};SERVER=xxx.xxx.xxx.xxx;DATABASE=<Datenbankname>;UID=<Benutzername>;PWD=<Kennwort>"

Die Funktion GetConnection erwartet die Angabe der zu verwendenden Verbindungszeichenfolge als Parameter. Sie erstellt ein neues Objekt des Typs ADODB.Connection und öffnet diese unter Angabe der Verbindungszeichenfolge (siehe Listing 1).

Public Function GetConnection(strConnection As String) As ADODB.Connection
     Dim objConnection As ADODB.Connection
     Set objConnection = New ADODB.Connection
     objConnection.Open strConnection
     Set GetConnection = objConnection
End Function

Listing 1: Die Funktion GetConnection

Die Funktion GetRecordset erwartet ein ADODB.Connection-Objekt, das wir zuvor beispielsweise mit der Funktion GetConnection ermitteln, sowie eine SQL-Anweisung mit der zu verwendenden Abfrage (siehe Listing 2). Die Funktion erstellt ein neues ADODB.Recordset-Objekt und stellt seine Eigenschaften ein, darunter auch die SQL-Abfrage für die Eigenschaft Source. Das Ergebnis wird dann als ADODB.Recordset-Objekt zurückgegeben.

Public Function GetRecordset(objConnection As ADODB.Connection, strSQL As String) _
         As ADODB.Recordset
     Dim rst As ADODB.Recordset
     Set rst = New ADODB.Recordset
     With rst
         .ActiveConnection = objConnection
         .CursorLocation = adUseClient
         .CursorType = adOpenDynamic
         .LockType = adLockBatchOptimistic
         .Source = strSQL
         .Open , , , , adCmdText
         Set GetRecordset = rst
     End With
End Function

Listing 2: Die Funktion GetRecordset

Danach nutzen wir diese beiden Funktionen in einer neuen Prozedur namens TabellenAbgleichen (siehe Listing 3). Diese Prozedur verwendet zwei ADODB.Recordset-Objekte namens rstAlt und rstNeu, um die Namen der Tabellen der alten und der neuen Version der Datenbank zu speichern. Diese füllen wir jeweils mit einem Aufruf der Funktion GetRecordset, wobei wir einmal die Verbindungszeichenfolge aus cStrVerbindungszeichenfolgeAlt und einmal die aus cStrVerbindungszeichenfolgeNeu übergeben. In beiden Fällen geben wir als SQL-Anweisung keine einfache SELECT-Anweisung an, sondern die Anweisung Show Tables. Dies ist eine MySQL-spezifische Anweisung, die alle Tabellen einer Datenbank liefert. Danach sortieren wir die Inhalte der beiden Recordsets rstAlt und rstNeu aufsteigend nach dem Inhalt des einzigen Feldes.

Public Sub TabellenAbgleichen()
     Dim rstAlt As ADODB.Recordset
     Dim rstNeu As ADODB.Recordset
     Dim strKriterium As String
     Dim fld As ADODB.Field
     Set rstAlt = GetRecordset(GetConnection(cStrVerbindungszeichenfolgeAlt), "SHOW TABLES")
     Set rstNeu = GetRecordset(GetConnection(cStrVerbindungszeichenfolgeNeu), "SHOW TABLES")
     rstAlt.Sort = rstAlt.Fields(0).Name & " ASC"
     rstNeu.Sort = rstNeu.Fields(0).Name & " ASC"
     Do While Not rstAlt.EOF
         If Not rstNeu.EOF Then
             If rstAlt.Fields(0) = rstNeu.Fields(0) Then
                 Debug.Print rstAlt.Fields(0), rstNeu.Fields(0)
                 rstAlt.MoveNext
                 rstNeu.MoveNext
             ElseIf rstAlt.Fields(0) > rstNeu.Fields(0) Then
                 Debug.Print "----", rstNeu.Fields(0)
                 rstNeu.MoveNext
             ElseIf rstAlt.Fields(0) < rstNeu.Fields(0) Then
                 Debug.Print rstAlt.Fields(0), "----"
                 rstAlt.MoveNext
             End If
         Else
             Debug.Print rstAlt.Fields(0), "----"
             rstAlt.MoveNext
         End If
     Loop
     Do While Not rstNeu.EOF
         Debug.Print "----", rstNeu.Fields(0)
         rstNeu.MoveNext
     Loop
End Sub

Listing 3: Die Prozedur TabellenAbgleichen

Schließlich durchlaufen wir das Recordset rstAlt in einer Do While-Schleife, bis die Datensatzmarkierung des auf EOF steht. In dieser Schleife durchlaufen wir auf bestimmte Weise beide Recordsets, wobei gegebenenfalls noch ein oder mehrere Datensätze im Recordset rstNeu übrig bleiben.

Dabei prüfen wir zunächst, ob rstNeu.EOF wahr ist. Das ist zu Beginn in der Regel noch nicht der Fall – außer, die neue Datenbank enthält gar keine Tabellen. Anderenfalls prüfen wir, ob der aktuelle Datensatz von rstAlt und rstNeu den gleichen Wert aufweisen, was darauf hindeutet, dass die Tabelle in beiden Datenbanken vorhanden ist. In diesem Fall geben wir im Direktbereich den Namen der Tabelle zwei Mal aus und verschieben die Datensatzzeiger beider Recordsets mit MoveNext zur nächsten Position. Ist rstAlt.Fields(0) > rstNeu.Fields(0), dann stimmen die aktuellen Tabellennamen nicht überein und der Tabellenname aus rstNeu liegt im Alphabet vor dem aus rstAlt. In diesem Fall geben wir eine Zeichenkette aus vier Minuszeichen (—-) aus und den Namen der Tabelle aus rstNeu. Außerdem verschieben wir den Datensatzzeiger von rstNeu um eine Position weiter.

Das heißt, wir bleiben bei dem Tabellennamen von rstAlt und wechseln zum nächsten Eintrag von rstNeu. Die nächst ElseIf-Bedingung behandelt den umgekehrten Fall, nämlich dass der Eintrag aus rstAlt im Alphabet vor dem Eintrag aus rstNeu liegt. Dann geben wir den aus rstAlt aus und vier Minuszeichen statt des Eintrags aus rstNeu und verschieben den Datensatzzeiger in rstAlt um eine Position weiter nach hinten.

Wenn also etwa in beiden Datenbanken tblA und tblB vorhanden sind, werden diese so ausgegeben:

tblA tblA
tblB tblB

Wenn in tblA nur in rstNeu vorhanden ist, erhalten wir dieses Ergebnis:

---- tblA
tblB tblB

Ist tblA in beiden Datenbanken vorhanden und tblB nur in der alten, sieht das Ergebnis so aus:

tblA tblA
tblB ----

Nun gibt es drei Fälle für das Ende des Durchlaufens der Datensätze mit den Tabellenamen in der ersten Do While-Schleife:

  • Die letzte Tabelle ist in beiden Datenbanken enthalten. Dann wird die erste Do While-Schleife mit dem If-Teil der inneren If…Then-Bedingung verlassen, weil NOT rstAlt.EOF danach nicht mehr erfüllt ist. rstNeu.EOF ist dann auch True.
  • Die letzte Tabelle beziehungsweise die letzten Tabellen sind nur in rstAlt enthalten. Dann ist die äußere If…Then-Schleife False und der Else-Teil wird ausgeführt, wo der Datensatzzeiger in rstAlt solange weitergeschoben wird, bis die Do While-Bedingung rstAlt.EOF erfüllt ist.
  • Die letzte Tabelle beziehungsweise die letzten Tabellen sind nur in rstNeu enthalten. Dann wird die erste Do While-Schleife verlassen, nachdem der Datensatzzeiger für rstAlt auf EOF verschoben wird.

In allen Fällen wird die Bedingung der zweiten Do While-Schleife noch einmal geprüft, nämlich Not rstNeu.EOF. Sollten also nach dem Durchlaufen von rstAlt noch Datensätze in rstNeu vorhanden sein, durchläuft die Prozedur diese in der zweiten Do While-Schleife. Dort gibt sie für die übrigen Tabellen der neuen Version der Datenbank die Tabellennamen aus, also etwa so:

...
---- tblY
---- tblZ

Interpretation des Ergebnisses

Damit erhalten wir nun eine Gegenüberstellung der Tabellen der alten und der neuen Version der Datenbank, die etwa so aussehen könnte:

tblA tblA
tblB tblB
---- tblC
tblD ----
...
tblY tblY
tblZ tblZ

Die Tabelle tblC kommt nicht in der alten Datenbank vor, aber in der neuen Version. tblD hingegen kommt nur in der alten, aber nicht in der neuen Version vor. Alle anderen Tabellen sind in beiden Versionen der Datenbank vorhanden.

Die Interpretation könnte wie folgt lauten:

  • Eine Tabelle kommt in der alten Version vor, aber nicht mehr in der neuen und umgekehrt: Die Tabelle wurde schlicht unbenannt, daher wird sie nicht mehr als gleiche Tabelle erkannt.
  • Eine Tabelle kommt in der alten Version vor, aber nicht mehr in der neuen und eine Umbenennung ist ausgeschlossen: Die Funktion, für die die Daten der Tabelle benötigt wurden, ist gegebenenfalls weggefallen. Oder, in meinem Beispiel mit der neuen Version des Shops geschehen: Die alte Version des Shops enthielt Plugins, die der neuen Version noch nicht hinzugefügt wurden, daher waren die entsprechenden Tabellen dort auch noch nicht enthalten. Sie können also so gegebenenfalls erkennen, dass Sie noch Plugins hinzufügen müssen, damit der neue Shop wie der alte funktioniert.
  • Eine Tabelle kommt in der neuen Version der Datenbank vor, aber nicht in der alten, und eine Umbenennung ist ausgeschlossen: Dann handelt es sich wohl um eine Tabelle, die im Rahmen einer neuen Funktion der neuen Version der Software benötigt wird.

Felder vergleichen

Wenn wir nun die Daten aus der alten Version der Datenbank in die neue Version übertragen wollen, könnten wir theoretisch für jede Tabelle, die in der alten und in der neuen Version vorhanden ist, folgende Anweisung schreiben (vorausgesetzt, die Datenbanken befinden sich auf dem gleichen MySQL-Server):

INSERT INTO <Neue Datenbank>.<Tabellenname> SELECT * FROM <Alte Datenbank>.<Tabellenname>

Das klappt aber auch nur, wenn die Struktur der Tabelle in der alten und der neuen Version der Datenbank identisch ist, also wenn die Tabellen die gleichen Felder enthalten. Anderenfalls gibt es eine Fehlermeldung.

In diesem Fall müssen wir untersuchen, welche der Felder der Tabelle der alten Version der Datenbank in der Tabelle der neuen Version der Datenbank vorkommen und diese statt des Platzhalters Sternchen (*) in die SELECT…INTO-Anweisung einfügen.

Gemeinsame Felder ermitteln

Um diese gemeinsamen Felder zu ermitteln, fügen wir dem If-Zweig der inneren If…Then-Bedingung den Aufruf der Funktion FelderAbgleichen mit dem Namen der Tabelle als Parameter hinzu. Das Ergebnis der Aufrufe dieser Funktion fügen wir in der Variablen strSQLInsert zusammen. Auf die gleiche Weise fügen wir in der Variablen strSQLDelete jeweils eine DELETE-Anweisung für die betroffenen Tabellen der neuen Version der Datenbank zusammen:

Public Sub TabellenAbgleichen()
     ...
     Dim strSQLDelete As String
     Dim strSQLInsert As String
     ...
     Do While Not rstAlt.EOF
         If Not rstNeu.EOF Then
             If rstAlt.Fields(0) = rstNeu.Fields(0) Then
                 Debug.Print rstAlt.Fields(0),  rstNeu.Fields(0)
                 strSQLDelete = strSQLDelete _
                     & "DELETE FROM " & cStrNeueDatenbank _
                     & "." & rstAlt.Fields(0) & vbCrLf
                 strSQLInsert = strSQLInsert _
                     & FelderAbgleichen(rstAlt.Fields(0)) _
                     & vbCrLf
                 ...

Die Funktion FelderAbgleichen finden Sie in Listing 4. Sie erwartet den Namen der zu untersuchenden Tabelle als Parameter. Sie ist grundsätzlich so aufgebaut wie die Prozedur TabellenAbgleichen, nur dass sie diesmal nicht die Tabellen der beiden Datenbanken, sondern die Felder der beiden Tabellen untersucht.

Public Function FelderAbgleichen(strTabelle As String) As String
     Dim rstAlt As ADODB.Recordset
     Dim rstNeu As ADODB.Recordset
     Dim strKriterium As String
     Dim fld As ADODB.Field
     Dim strFelder As String
     Set rstAlt = GetRecordset(GetConnection(cStrVerbindungszeichenfolgeAlt), "DESCRIBE " & strTabelle)
     Set rstNeu = GetRecordset(GetConnection(cStrVerbindungszeichenfolgeNeu), "DESCRIBE " & strTabelle)
     rstAlt.Sort = rstAlt.Fields("Field").Name & " ASC"
     rstNeu.Sort = rstNeu.Fields("Field").Name & " ASC"
     Do While Not rstAlt.EOF
         If Not rstNeu.EOF Then
             If rstAlt.Fields("Field") = rstNeu.Fields("Field") Then
                 Debug.Print rstAlt.Fields("Field"), rstNeu.Fields("Field")
                 strFelder = strFelder & ", " & rstAlt.Fields("Field")
                 rstAlt.MoveNext
                 rstNeu.MoveNext
             ElseIf rstAlt.Fields("Field") > rstNeu.Fields("Field") Then
                 Debug.Print "----", rstNeu.Fields("Field")
                 rstNeu.MoveNext
             ElseIf rstAlt.Fields("Field") < rstNeu.Fields("Field") Then
                 Debug.Print rstAlt.Fields("Field"), "----"
                 rstAlt.MoveNext
             End If
         Else
             Debug.Print rstAlt.Fields("Field"), "----"
             rstAlt.MoveNext
         End If
     Loop
     Do While Not rstNeu.EOF
         Debug.Print "----", rstNeu.Fields("Field")
         rstNeu.MoveNext
     Loop
     If Len(strFelder) > 0 Then
         strFelder = Mid(strFelder, 3)
     End If
     FelderAbgleichen = "INSERT INTO " & cStrNeueDatenbank & "." & strTabelle & "(" & strFelder & ") SELECT " _
         & strFelder & " FROM " & cStrAlteDatenbank & "." & strTabelleEnd Function

Listing 4: Die Funktion FelderAbgleichen

Daher verwenden wir wieder zwei Recordsets, welche die Verbindungszeichenfolgen aus den Konstanten nutzen. Als Datenquelle verwenden wir diesmal wieder keine klassische SELECT-Abfrage, sondern die Anweisung DESCRIBE mit den Namen der zu beschreibenden Tabelle. Dies liefert die Namen der Felder der angegebenen Tabelle zurück – neben einigen anderen Feldern. Uns interessiert aber nur der Feldname, den wir im Feld Field finden.

Deshalb sortieren wir die Datensätze beider Recordsets zuerst aufsteigend nach dem Inhalt des Feldes Field. Dann durchlaufen wir in einer Do While-Schleife die Datensätze des Recordsets rstAlt. Solange der Datensatzzeiger für rstNeu nun nicht auf EOF steht, was bedeutet, dass wir zwar noch nicht alle Datensätze von rstAlt, aber schon alle Datensätze von rstNeu durchlaufen haben – und dementsprechend im Else-Teil die erste Do While-Schleife verlassen -, bleiben wir in dieser Schleife. Zu Beginn dürfte der Datensatzzeiger von rstNeu allerdings noch am Anfang stehen, weshalb wir erst den If-Teil der Bedingung durchlaufen. Die untergeordnete If…Then-Bedingung ähnelt der aus der Prozedur TabellenAbgleichen.

Sie prüft, ob ein Feld in beiden Tabellen oder nur in einer der beiden Tabellen vorhanden ist und liefert entsprechende Ausgaben im Direktbereich und schiebt den Datensatzzeiger des jeweils betroffenen Recordsets weiter. Im If-Teil haben wir eine Anweisung eingebaut, die der Variablen strFelder ein Komma und den Namen des in beiden Tabellen vorkommenden Feldes hinzufügt.

Dadurch erhalten wir so Schritt für Schritt eine komma-separierte Liste der Feldnamen. In der zweiten Do While-Schleife durchlaufen wir schließlich noch alle Datensätze von rstNeu, die nach dem vollständigen Durchlaufen von rstAlt noch übrig sind.

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar