Skip to content
 

Datenbankmigration mit Liquibase und DbUnit

Wir mussten mehrere Microsoft SQL Server Datenbanken nach MySQL auf einem Linux Server zügeln. Zu diesem Zweck setzten wir die Java Libraries Liquibase und DbUnit ein.

Folgende Libraries werden benötigt.

Liquibase 2.0 RC1
DbUnit 2.4.7
JTDS 1.2.4 JDBC Treiber für MSSQL Server
MySQL Connector 5.1.10 JDBC Treiber für MySQL
SL4J 1.5.8
Logback 0.9.17

Es werden keine zusätzlichen, selber erstellten Programme benötigt. Alle Aufgaben können mit Ant erledigt werden.

Zu Beginn werden die Angaben für die Datenbankverbindungen in der Datei build.properties definiert.

db1.driver=net.sourceforge.jtds.jdbc.Driver
db1.url=jdbc:jtds:sqlserver://localhost/sourcedb
db1.user=user
db1.password=password

db2.driver=com.mysql.jdbc.Driver
db2.url=jdbc:mysql://linuxserver/targetdb
db2.user=user
db2.password=password

In der Ant Build Datei build.xml wird build.properties eingelesen und die Tasks für DbUnit und Liquibase definiert.

<project name="migration">
  <property file="${basedir}/build.properties" />
  <property name="lib.dir" value="${basedir}/lib" />

  <path id="tool.classpath">
    <path location="${lib.dir}/dbunit-2.4.7.jar" />
    <path location="${lib.dir}/jtds-1.2.4.jar" />
    <path location="${lib.dir}/liquibase.jar" />
    <path location="${lib.dir}/mysql-connector-java-5.1.10-bin.jar" />
    <path location="${lib.dir}/jcl-over-slf4j-1.5.8.jar" />
    <path location="${lib.dir}/slf4j-api-1.5.8.jar" />
    <path location="${lib.dir}/logback-classic-0.9.17.jar" />
    <path location="${lib.dir}/logback-core-0.9.17.jar" />
    <pathelement location="${basedir}"/>
  </path>  

  <taskdef resource="liquibasetasks.properties" classpathref="tool.classpath"/>
  <taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask" classpathref="tool.classpath"/>  

...

</project>

Als nächster Schritt wird das Schema der Quelldatenbank exportiert. Dazu wird der Liquibase Task <generateChangeLog> eingesetzt.

<project name="migration">
...
  <target name="generate.changelog">
    <generateChangeLog
            outputFile="${basedir}/ddl.xml"
            driver="${db1.driver}"
            url="${db1.url}"
            username="${db1.user}"
            password="${db1.password}"
            classpathref="tool.classpath" />
    <replace file="${basedir}/ddl.xml" token="int identity" value="int"/>
    <replace file="${basedir}/ddl.xml" token="baseTableSchemaName="dbo" " value=""/>
    <replace file="${basedir}/ddl.xml" token="schemaName="dbo" " value=""/>
    <replace file="${basedir}/ddl.xml" token="image" value="longtext"/>
  </target>
...
</project>

Da der Output noch SQL Server spezifische Dinge enthält muss die Datei ddl.xml angepasst werden. Der <replace> Task von Ant erledigt dies. Hier werden alle schemaName und baseTableSchemaName Attribute aus dem XML gelöscht. Zusätzlich werden alle Datentypen “int identity” durch “int” ersetzt. Bei den int identity Feldern ist das autoIncrement Attribute auf true gesetzt so dass in der MySQL Datenbank auch Autonummerfelder erzeugt werden.
Der Datentyp image für Blob Felder muss auch angepasst werden. Hier muss von Fall zu Fall enschieden werden, da es in MySQL vier verschiedene Typen (Grössen) für Blobs gibt. Das Beispiel ersetzt image durch longtext. http://dev.mysql.com/doc/refman/5.0/en/blob.html

Das erstellt Schema (ddl.xml) wird nun mit dem Liquibase Task <updateDatabase> in die Zieldatenbank eingefügt.

<project name="migration">
...
  <target name="update.db">
    <updateDatabase
            changeLogFile="${basedir}/ddl.xml"
            driver="${db2.driver}"
            url="${db2.url}"
            username="${db2.user}"
            password="${db2.password}"
            promptOnNonLocalDatabase="false"
            dropFirst="false"
            classpathref="tool.classpath"/>
  </target>
...
</project>

Falls es Fehler beim Erstellen des Schemas gibt muss die Datei ddl.xml angepasst werden.

Mit dem nächsten Schritt werden alle Daten aus der Quelldatenbank exportiert. Der <export> Task von DbUnit übernimmt diese Aufgabe.

<project name="migration">
...
  <target name="export.data">
    <dbunit driver="${db1.driver}"
        url="${db1.url}"
        userid="${db1.user}"
        password="${db1.password}">

      <dbconfig>
        <property name="datatypeFactory" value="org.dbunit.ext.mssql.MsSqlDataTypeFactory" />
      </dbconfig>      

      <export dest="${basedir}/dataexport.xml" />

    </dbunit>
  </target>
...
</project>

Die exportierten Daten werden in einer XML Datei (dataexport.xml) abgespeichert. Diese Datei kann SQL Server spezifische Daten enthalten, wie zum Beispiel dtproperties oder sysdiagrams. Diese Daten müssen aus der Datei gelöscht werden ansonsten funktioniert der anschliessende Import nicht. Mit dem <table> Parameter ist es möglich dem <export> Task anzugeben welche Tabellen exportiert werden sollen.

<export dest="...">
  <table name="TABLE1"/>
  <table name="TABLE2"/>
</export>

Weitere Informationen findet man auf der DbUnit Seite.

Im letzten Schritt werden die Daten in die Zieldatenbank importiert. Für diese Aufgabe wird der <operation> Task von DbUnit verwendet. Als Typ wird hier CLEAN_INSERT verwendet, was bedeutet das vor dem Insert alle Rows in der Tabelle gelöscht werden. Eine Auflistung aller möglichen Typen findet man in der DbUnit Dokumentation.

<project name="migration">
...
  <target name="import.data">
    <dbunit driver="${db2.driver}"
        url="${db2.url}?sessionVariables=FOREIGN_KEY_CHECKS=0"
        userid="${db2.user}"
        password="${db2.password}">

      <dbconfig>
        <property name="datatypeFactory" value="org.dbunit.ext.mysql.MySqlDataTypeFactory" />
        <property name="escapePattern" value="`?`"/>
      </dbconfig>

      <operation type="CLEAN_INSERT" transaction="true" src="${basedir}/dataexport.xml"/>

    </dbunit>
  </target>
...
</project>

Für den Import wird die Foreign Key Prüfung temporär ausgeschaltet (?sessionVariables=FOREIGN_KEY_CHECKS=0). Damit ist es möglich die Daten in einer beliebigen Reihenfolge einzufügen.

Alternativ ist es möglich die Daten bereits in der richtigen Reihenfolge zu exportieren, so dass beim Import keine Foreign Key Verletzungen auftreten. Dazu wird das Attribute ordered beim <export> Task angegeben: <export dest=”${basedir}/dataexport.xml” ordered=”true” /> Dies funktioniert allerdings nicht in allen Fällen. Wenn zum Beispiel Tabellen mit Referenzen auf sich selber (Abbildung von Hierarchien) eingesetzt werden.

Wenn für Tabellen und Felder Namen verwendet werden die in MySQL Schlüsselwörter sind dann müssen diese beim Import mit einem ` umschlossen werden. Dies wird mit der Zeile <property name="escapePattern" value="`?`"/> konfiguriert. Das Fragezeichen wird mit dem Namen der Tabelle oder Feld ersetzt.

One Comment

  1. Benjamin says:

    Interessanter Beitrag, hätte mir vor nem Monat geholfen ;D

    Ich habe eine Bitte: Da ich zurzeit an einem Projekt arbeite, das eine ähnliche Zielgruppe wie deine ansprechen soll, wollte ich dich fragen, ob du mir zwecks Browseroptimierung die Verteilung der verwendeten Browser deiner Besucher zukommen lassen könntest.

    Liebe Grüße
    Benjamin

Leave a Reply