22. Mar 2013

Einbindung der DB-Entwicklung in den Continuous Integration-Prozess

Exemplarische Darstellung anhand einer MySQL DB und Maven, wie der Abriss und Aufbau des DB-Schemas sowie das Testen der DB Migrationspfade über eine CI-Plattform realisiert werden kann.
1060 x 710 Schubert Stefan

Author

Stefan Schubert

2024 Blogbeitrag Accso

In vielen Java-Projekten erlebe ich immer wieder, dass der Bereich Datenbank eher stiefmütterlich behandelt wird. Bei einer guten Projektinfrastruktur findet man zwar eine CI Umgebung (bamboo/jenkins) vor, in der der aktuelle Entwicklungsstand automatisiert „vertestet" wird und den Entwicklern als Fangnetz dient, jedoch deckt diese meist nur den Java-Teil der Applikation ab. In vielen Fällen wird zudem auf das DB-Backend in der CI Umgebung ganz verzichtet, dabei bedeutet CI doch gerade contineous integration. Der Verzicht hat häufig den Grund, dass man die Einbindung des DB-Backends eher als langläufige Integrationstests ansieht und man lieber schneller Feedback von der CI Umgebung haben möchte, aber auch dafür gibt es Lösungen (z.B. mit einer H2 in memory DB, die den gewünschten DB-Dialekt emuliert).

Betrachtet man das Projekt ganzheitlich, so ist die Datenbankentwicklung ein fester, integraler Bestandteil der Softwareentwicklung. Andernfalls läuft man Gefahr, bei dem zumeist spät in der Projektphase angesetzten Integrationstest oder bei der Auslieferung festzustellen, dass das Deployment abgelehnt wird, weil irgendwelche DB-Skripte fehlerhaft sind. Die Integration der Datenbank-Artefakte in den CI-Prozess sichert eine permanente QA der DDL-Skripte von der Erstauslieferung bis über Migrationspfade hinweg, ganz getreu dem agilen Motto „fail fast, learn quick".

Im ersten Teil stelle ich exemplarisch anhand einer MySQL DB und Maven vor, wie der Abriss und Aufbau des DB-Schemas sowie das Testen der DB Migrationspfade über eine CI-Plattform realisiert werden kann. Der zweite Teil stellt anhand einer Oracle-DB vor, wie sich analog zu JUnit-Tests PL/SQL Code im Rahmen der CI automatisiert testen lässt.


Teil I: CI der DDL und DML Skripte

In manchen „grüne Wiese" Projekten werden je nach eingesetzter Infrastruktur die DB-Artefakte generiert (sei es über den EA oder den ORM). In diesen Fällen kann unter Umständen vollständig auf die DB-CI verzichtet werden. Was ist jedoch mit Projekten, die die „grüne Wiese" schon längst hinter sich gelassen haben und auf die wir viel häufiger stoßen? Hier geht es um Schemaevolution, ggf. Erstellen von zusätzlich notwendigen Datenmigrationsskripten, deren Lauffähigkeit sichergestellt werden muss.

Betrachten wir zunächst folgendes Setup: Jeder Entwickler hat neben seiner Java-IDE lokal eine MySQL Datenbank. Über Maven möchten wir nun für einen Testlauf das bestehende Schema (ProjectDB) aus dem letzten Lauf abreißen, neu aufbauen und hinterlegte Testdaten laden. Dabei hilft uns das codehaus maven-sql-plugin.

Da wir bei der Entwicklung nicht immer mit jedem maven install Lauf auch die Datenbankinfrastruktur neu aufsetzen möchten, sondern nur wenn wir wissen, dass es auch Veränderungen am Domänenmodell (und damit auch dem Datenmodell) gab, spendieren wir für unsere Zwecke ein eigenes Profil:

Ausschnitt aus der pom.xml

<profile>
   <id>setup-mysql</id>
   <activation>               
      <activeByDefault>false</activeByDefault>
   </activation>
   <build>
      <plugins>
          <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>sql-maven-plugin</artifactId>
              <version>1.5</version>
              <dependencies>
                  <!-- specify the dependent jdbc driver here -->
                  <dependency>
                      <groupId>mysql</groupId>
                      <artifactId>mysql-connector-java</artifactId>
                      <version>5.1.20</version>
                  </dependency>
              </dependencies>
               <!-- common configuration shared by all executions -->
              <configuration>
                  <driver>com.mysql.jdbc.Driver</driver>
                  <url>jdbc:mysql://localhost:3306</url>
                  <username>root</username>
                  <password>SHARED_PASSWORD</password>
                  <settingsKey>sensibleKey</settingsKey>
                  <!--all executions are ignored if -Dmaven.test.skip=true -->
                  <skip>${maven.test.skip}</skip>
              </configuration>
               <executions>
                  <execution>
                      <id>drop-db</id>
                      <phase>process-test-resources</phase>
                      <goals>
                          <goal>execute</goal>
                      </goals>
                      <configuration>
                          <autocommit>true</autocommit>
                          <sqlCommand>drop database ProjectDB;</sqlCommand>
                          <!-- ignore error when database is not avaiable -->
                          <onError>continue</onError>
                      </configuration>
                 </execution>
                   <execution>
                      <id>create-db</id>
                      <phase>process-test-resources</phase>
                      <goals>
                          <goal>execute</goal>
                      </goals>
                      <configuration>
                          <autocommit>true</autocommit>
                         <sqlCommand>create database ProjectDB;</sqlCommand>
                      </configuration>
                  </execution>
                   <execution>
                      <id>create-schema</id>
                      <phase>process-test-resources</phase>
                      <goals>
                          <goal>execute</goal>
                      </goals>
                      <configuration>
                          <url>jdbc:mysql://localhost:3306/ProjectDB</url>
                          <autocommit>true</autocommit>
                          <srcFiles>
                 <srcFile>src/main/schema/TablesOfComponentA.sql</srcFile>
                 <srcFile>src/main/schema/TablesOfComponentB.sql</srcFile>
                          </srcFiles>
                      </configuration>
                  </execution>

                  <execution>
                      <id>create-data</id>
                      <phase>process-test-resources</phase>
                      <goals>
                          <goal>execute</goal>
                      </goals>
                      <configuration>
                          <url>jdbc:mysql://localhost:3306/ProjectDB</url>
                          <autocommit>true</autocommit>
                          <orderFile>ascending</orderFile>
                          <fileset>
                              <basedir>${basedir}</basedir>
                              <includes>
                                  <include>src/main/data/basicdata.sql</include>
                              </includes>
                          </fileset>
                      </configuration>
                  </execution>
                   <execution>
                      <id>create-appuser</id>
                      <phase>process-test-resources</phase>
                      <goals>
                         <goal>execute</goal>
                      </goals>
                      <configuration>
                          <autocommit>true</autocommit>
                          <sqlCommand>
                             grant all on ProjectDB.* to appUser@localhost
                             identified by 'secret';
                             flush privileges;
                          </sqlCommand>
                      </configuration>
                  </execution>
               </executions>
         </plugin>
      </plugins>
   </build>
   <dependencies>
       <dependency>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>sql-maven-plugin</artifactId>
           <version>1.5</version>
       </dependency>
   </dependencies>
</profile>

In der lokalen Entwicklungsumgebung lässt sich das Schema nun mit

mvn -Psetup-mysql install

erstellen.

Im obigen Fall habe ich die Ausführung der DB-Artefakte in den maven lifecycle process-test-resources gesetzt, weil ich für die Ausführung von Integrationstests (d.h. Junit-Tests, die auf die Persistenzschicht zurückgreifen) ein jungfräuliches DB-Setup haben möchte. Dies ist besonders dann sinnvoll, sollten die Integrationstests eher „unsauber" sein, d.h., dass sie Ihre Testdaten nicht selbst verwalten. Je nach Bedarf kann man sich hier an einen anderen lifecycle hängen. Durch die Verwendung des Profils haben wir in jedem Falle die Freiheit, den DB-Part beim Testlauf mitzunehmen oder auf dem alten Schema zu operieren.
In Projekten habe ich es häufig als effizienter empfunden, die Testdaten über generierte SQL Skripte bereitzustellen, als diese über die Business Services anzulegen. Gerade bei entsprechenden Mengengerüsten ist dies wesentlich performanter. Im obigen Beispiel übernimmt der execution Abschnitt create-data diesen Schritt.

Der ein oder andere mag sich inzwischen gefragt haben, wieso ich nicht auf DBUnit ( http://dbunit.sourceforge.net) setze. Die Antwort ist einfach: DBUnit arbeitet mit XML-basierten Datenfiles. Das empfinde ich zum einen als Medienbruch (meine DBTools, die ich einsetze, können damit nichts anfangen), und zum anderen gehören die SQL-Skripte (welche in der Regel Stammdaten und Testdaten umfassen) zum Teil zu den auslieferpflichtigen Artefakten. Ein DBA würde niemals XML von DBUnit akzeptieren. Ein weiterer Grund liegt darin, dass DBUnit nicht weit genug geht. Der Ansatz behandelt nur DML, aber keine DDL. D.h., mit DBUnit kann ich keine Migrationspfade absichern.

Zum Schluss möchte ich in Teil I noch auf drei Punkte eingehen:

  1. Testen von Migrationspfaden
  2. Anpassung des Maven Profiles für die CI Umgebung
  3. Verwendung einer InMemory vs. „echter" DB.

Testen von Migrationspfaden

Hat man ein Projekt, welches in Milestones (oder ganz agil in kurzen Iterationen) ausgeliefert werden soll, dann ergeben sich zwischendurch häufig Änderungen an den zugrunde liegenden Datenstrukturen. Die entsprechenden Java-Artefakte zur Verfügung zu stellen ist keine Kunst, aber wie sieht es mit dem Schema-Delta aus? Welche Änderungen gehören nun zum Release, und sind die Skripte, die man dem DBA da in die Hand drücken will, überhaupt lauffähig (Thema Abhängigkeiten)? Zwar gibt es Tools, die einem das Diff zwischen zwei DB-Schemata ermitteln und das Delta-Skript (inkl. Auflösung von Abhängigkeiten) erstellen, dennoch muss meist ein anderer Weg eingeschlagen werden. Gründe hierfür sind häufig, dass das Tool im Projekt schlicht nicht zur Verfügung gestellt wird, oder (für mich der eigentliche Hauptgrund) die Tools nehmen einem nicht die Arbeit ab, Datenmigrationsskripte zu erstellen (z.B. beim Refactoring des ORM-Mappings oder dem Einbau von referenzierten Stammdaten aus dem Altsystem).

Zur Lösung bediene ich mich in der Regel eines simplen Verfahrens auf Basis obiger Skripte:

Die Skripte für jedes Release werden hierzu in ein separates Verzeichnis gepackt. Zusätzlich werden die Skripte in der Reihenfolge der Releases und ihrer Abhängigkeiten untereinander ausgeführt, in dem man obiges Maven-Profil um entsprechende Execution-Steps mitwachsen lässt. Wurde ein Release ausgeliefert, dürfen an den Skripten in den zugehörigen Verzeichnissen keine Änderungen mehr durchgeführt werden. Drakonische Strafmaßnahmen des Technischen Projektleiters wären die Folge (ok, ein SVN-Hook würde es auch tun 😉 ) Dies erfordert eine gewisse Disziplin, der Aufwand hält sich jedoch in Grenzen, und in Verbindung mit der QS über die CI hat man bei der Zusammenstellung der Artefakte bei der Auslieferung keine Bedenken mehr, dass hier was schief gehen kann.

Hat man die Infrastruktur einmal soweit aufgebaut, kann man auch darüber nachdenken, Rollback-Skripte, welche man u.U. bereitstellen muss, ebenfalls über diesen Mechanismus abzusichern.

Anpassung des Maven-Profils für die CI-Umgebung

Als nächstes wollen wir obiges Verfahren in die CI-Umgebung einbauen. Je nach Bedarf sieht die CI-Umgebung in Projekten immer etwas anders aus. Für unser Beispiel gehe ich einmal von folgendem Szenario aus, welches ich gerne einsetze:

20130322 DB Entwicklung

Wer jetzt aufmerksam mitgezählt hat, stellt fest, dass ich in diesem Beispiel davon ausgehe, dass auf dem CI-Server mindestens zwei Datenbanken laufen und wir eine remote Datenbank auf dem Testsystem ansprechen wollen (Auf dem CI-Server wählen wir eher zwei Schemata anstelle ganzer Datenbank-Instanzen)

Wie lässt sich nun per build-plan die entsprechende Datenbank dediziert ansprechen?

Hierzu erweitern wir die pom.xml um je ein Profil für die verschiedenen DB-Umgebungen. Um Redundanzen in diesem Blog einzusparen, stelle ich exemplarisch nur ein Profil dar:

Zusätzliches Profil für die Skriptkonfiguration

<profile>
   <id>db_ci_trunk</id>
   <activation>
        <activeByDefault>false</activeByDefault>
   </activation>
   <properties>
       <!-- CI trunk-db-build database connection secrets -->
       <db.mysql.schema>ci_ProjectDB</db.mysql.schema>
       <db.mysql.server.name>localhost</db.mysql.server.name>
                      <!-- DB resides on same host as CI -->
       <db.mysql.server.port>3306</db.mysql.server.port>
       <db.mysql.server.app.username>user_ci</db.mysql.server.app.username>
       <db.mysql.server.app.password>somesecret</db.mysql.server.app.password>
       <db.mysql.server.root.username>root</db.mysql.server.root.username>

       <db.mysql.server.root.password>1.FC-Kölle</db.mysql.server.root.password>
    </properties>
</profile>

Als nächstes bauen wir die properties in unser db-setup Profil ein. Auch dies werde ich hier nur exemplarisch andeuten, das Verfahren sollte damit klar sein:

Ausschnitte aus der modifizierten pom.xml

...
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>${mysql-connector-java.version}</version>
</dependency>
...
 
<configuration>
  <driver>org.gjt.mm.mysql.Driver</driver>
  <url>
jdbc:mysql://${db.mysql.server.name}:${db.mysql.server.port}/${db.mysql.schema}
  </url>
  <username>${db.mysql.server.app.username}</username>
  <password>${db.mysql.server.app.password}</password>
</configuration>
 
...
 
<execution>
   <id>drop-data-base</id>
   <phase>process-test-resources</phase>
   <goals>
       <goal>execute</goal>
   </goals>
   <configuration>
       <username>${db.mysql.server.root.username}</username>
       <password>${db.mysql.server.root.password}</password>
       <url>jdbc:mysql://${db.mysql.server.name}:${db.mysql.server.port}</url>
       <onError>continue</onError>
       <sqlCommand>drop database ${db.mysql.schema};</sqlCommand>
   </configuration>
</execution>
 
...
 
<execution>
    <id>create-appuser</id>
    <phase>process-test-resources</phase>
    <goals>
        <goal>execute</goal>
    </goals>
    <configuration>
       <autocommit>true</autocommit>
       <sqlCommand>
          grant all on ${db.mysql.schema}.* to
          '${db.mysql.server.app.username}'@'${db.mysql.server.name}'
          identified by  '${db.mysql.server.app.password}';
          flush privileges;
       </sqlCommand>
    </configuration>
</execution>

Nun lassen sich die Profile für den build-plan entsprechend kombinieren und aufrufen:

mvn -Pdb_ci_trunk,setup-mysql install

Variante für eingeschränkten DB-Zugriff:

Bei manchen Kunden hat das Entwicklerteam keinen direkten Zugriff auf die CI-Umgebung, geschweige denn auf die Root Accounts der Datenbanken (welche für das Abreißen und Aufbauen der Datenbank per Skript jedoch benötigt werden). In diesem Fällen können die properties in die settings.xml der Maven-Installation auf dem CI-Server ausgelagert werden, und dem Entwicklungsteam bleiben die DB-Secrets verborgen.


Verwendung einer InMemory vs. „echter" DB

Wenn die JUnit-Tests die Persistenzschicht benötigen, mag die Ausführung mit der konkreten DB-Instanz unter Umständen zu lange Laufzeiten aufweisen. Für diese Fälle lässt sich die Projektkonfiguration noch weiter verfeinern, indem man z.B. eine H2 DB „in Memory“ betreibt und dabei den benötigten DB-Dialekt (hier MySQL) einstellt. Für die CI sicherlich eine gute Wahl. Ich persönlich (da sehr DB-affiin), bevorzuge trotzdem immer eine „echte" DB – da ich zum Debuggen auch im Nachhinein gerne mal einen Blick auf die tatsächlich erzeugten Daten werfe oder zur Einsparung von Roundtrip-Zeiten auch gerne mal Daten per Hand manipuliere möchte.

Hinzu kommt, dass dem Einsatz einer H2 bzgl. der intendierten Integration der DB-Entwicklung Grenzen gesetzt sind. Benötigen wir die Kompatibilität zu einer ganz bestimmten älteren Datenbankversion oder haben wir Logik in der Datenbank (z.B. per Trigger, PL/SQL Code), kommen wir mit der H2 nicht mehr aus. Der gewinnbringende Einsatz einer H2 ist je nach Projekt also abzuwägen. Da der Konfigurationsaufwand jedoch gering ist, kann man damit gerne beginnen. Dies ist auch häufig die erste Wahl, wenn man dem ORM gestattet, das Schema zu generieren, ein Thema, was allein einen weiteren BLOG Eintrag füllen könnte.


Ausblick

Teil II: Testen der DB-Logik

  • Ausgehend von einer Oracle-DB / PL/SQL Logik
  • Etwas zur Historie: Toolüberblick (Feuerstein) / Kostenaspekt
  • Vorstellung utPLSQL
  • Setup zur Verwendung in JUnit Test
  • Maven-Beispiel

Update 5.11.2013: Teil II ist inzwischen erschienen, siehe: http://www.accso.de/wp/2013/11/einbindung-von-plsql-code-in-den-continuous-integration-prozess/