Jan 272014
 

Im Build-Prozess von Projekten mit Datenbank-Anbindung wird oft davon ausgegangen, dass Datenbank-Verbindungen (also DB-Schema/Nutzer) für Tests, Continuous Integration, Deployment usw. vorhanden sind und über diverse Konfigurationen (URL, Nutzer, Passwörter, Treiber, …) in verschiedenen Schritten des Builds angesprochen werden können. Für einen flexiblen Build und insbesondere für exakt reproduzierbare Tests wäre es allerdings gut, wenn ein Datenbank-Schema im Build erzeugt, modifiziert, gesichert oder auch gelöscht werden kann.

Ein DB-Schema wird durch eine Reihe von DDL- und DCL-Statements bearbeitet. DDL meint Data Definition Language, DCL Data Control Language; beides sind Subsets von SQL. Beispiele für DDL-Statements sind die create-, drop– und alter-Befehle, zur DCL gehören grant und revoke. Der build-Prozess muss also in der Lage sein, sich mit einer Datenbank zu verbinden und SQL-Befehle auszuführen. Das will ich mit Gradle erreichen, einem auf Groovy basierenden Build-Tool, von dem ich mir – mindestens – die Flexibilität von Ant, das Dependency Management von Maven und Ivy, Groovys Scripting-Fähigkeiten und die natürliche und mächtige Ausdruckskraft einer DSL (domain-specific language) erwarte.

Bisher habe ich mit Gradle noch nicht gearbeitet (aber mit Groovy). Die Installationsanweisungen unter http://www.gradle.org/docs/current/userguide/installation.html sind unkompliziert und brauchen hier nicht weiter beschrieben zu werden. Unter http://www.gradle.org/docs/current/userguide/tutorial_using_tasks.html erfahre ich, dass ich mit einem Buildscript namens build.gradle anfangen kann und darin tasks definiere, die meinen Build-Prozess beschreiben. Ich versuche zunächst, mit einer Kombination aus Gradle und Groovy eine Datenbank-Verbindung aufzubauen und eine Query auszuführen. Das Gradle-Buildscript hat die Task callDatabase und sieht so aus:

task callDatabase {
    println 'Connecting to database ...'
    def sql = groovy.sql.Sql.newInstance('jdbc:oracle:thin:@localhost:1521:DBSID', 'user', 'password', 'oracle.jdbc.OracleDriver' )
    println '... connected'
  
    println 'Querying database tables ...'
    sql.eachRow('select * from all_tables') { row -> println "${row.owner}:${row.table_name}" }
    println '... ready'
}

Für die URL, username und password in groovy.sql.Sql.newInstance sind natürlich echte Werte einzusetzen. Im Verzeichnis, in dem diese build.gradle liegt, rufe ich nun gradle auf und bekomme dieses Ergebnis:

heiko@nb:/projects/dbgradle > gradle
Connecting to database ...

FAILURE: Build failed with an exception.

* Where:
Build file '/projects/dbgradle/build.gradle' line: 3

* What went wrong:
A problem occurred evaluating root project 'DatabaseWithGradle'.
> oracle.jdbc.OracleDriver

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 2.035 secs
heiko@nb:/projects/dbgradle > 

Als Java-Entwickler vermutet man schnell (und richtig), dass der Oracle-JDBC-Treiber nicht geladen werden kann (es sei denn, man hat ein entsprechendes jar-Archiv im allgemeinen Java-CLASSPATH). Wenn ich Gradles Vorschlag folge und den Build mit der option --debug starte, wird der Fehler in typischer Java-Manier angezeigt:

heiko@nb:/projects/dbgradle > gradle --debug
18:56:23.007 [INFO] [org.gradle.BuildLogger] Starting Build
18:56:23.011 [DEBUG] [org.gradle.BuildLogger] Gradle user home: /home/heiko/.gradle
18:56:23.015 [DEBUG] [org.gradle.BuildLogger] Current dir: /projects/dbgradle
...
18:56:24.043 [QUIET] [system.out] Connecting to database ...
18:56:24.073 [DEBUG] [org.gradle.configuration.project.BuildScriptProcessor] Timing: Running the build script took 0.129 secs
18:56:24.080 [ERROR] [org.gradle.BuildExceptionReporter] 
18:56:24.082 [ERROR] [org.gradle.BuildExceptionReporter] FAILURE: Build failed with an exception.
18:56:24.084 [ERROR] [org.gradle.BuildExceptionReporter] 
18:56:24.085 [ERROR] [org.gradle.BuildExceptionReporter] * Where:
18:56:24.085 [ERROR] [org.gradle.BuildExceptionReporter] Build file '/projects/dbgradle/build.gradle' line: 3
18:56:24.086 [ERROR] [org.gradle.BuildExceptionReporter] 
18:56:24.087 [ERROR] [org.gradle.BuildExceptionReporter] * What went wrong:
18:56:24.088 [ERROR] [org.gradle.BuildExceptionReporter] A problem occurred evaluating root project 'DatabaseWithGradle'.
18:56:24.088 [ERROR] [org.gradle.BuildExceptionReporter] > oracle.jdbc.OracleDriver
18:56:24.091 [ERROR] [org.gradle.BuildExceptionReporter] 
18:56:24.092 [ERROR] [org.gradle.BuildExceptionReporter] * Exception is:
18:56:24.094 [ERROR] [org.gradle.BuildExceptionReporter] org.gradle.api.GradleScriptException: A problem occurred evaluating root project 'DatabaseWithGradle'.
18:56:24.094 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:54)
18:56:24.095 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:156)
18:56:24.096 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:38)
18:56:24.097 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:25)
18:56:24.098 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:34)
18:56:24.099 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:55)
18:56:24.100 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.project.AbstractProject.evaluate(AbstractProject.java:507)
18:56:24.101 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.project.AbstractProject.evaluate(AbstractProject.java:82)
18:56:24.101 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.configuration.DefaultBuildConfigurer.configure(DefaultBuildConfigurer.java:31)
18:56:24.102 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:142)
18:56:24.103 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:113)
18:56:24.104 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:81)
18:56:24.105 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:64)
18:56:24.106 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:33)
18:56:24.107 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.ExecuteBuildAction.run(ExecuteBuildAction.java:24)
18:56:24.107 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:35)
18:56:24.108 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:26)
18:56:24.109 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.RunBuildAction.run(RunBuildAction.java:50)
18:56:24.110 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.internal.Actions$RunnableActionAdapter.execute(Actions.java:171)
18:56:24.111 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:201)
18:56:24.112 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.CommandLineActionFactory$ParseAndBuildAction.execute(CommandLineActionFactory.java:174)
18:56:24.112 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:170)
18:56:24.113 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.CommandLineActionFactory$WithLogging.execute(CommandLineActionFactory.java:139)
18:56:24.114 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:33)
18:56:24.115 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:22)
18:56:24.116 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.Main.doAction(Main.java:46)
18:56:24.117 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:45)
18:56:24.118 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.Main.main(Main.java:37)
18:56:24.119 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:50)
18:56:24.119 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:32)
18:56:24.120 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.launcher.GradleMain.main(GradleMain.java:23)
18:56:24.121 [ERROR] [org.gradle.BuildExceptionReporter] Caused by: java.lang.ClassNotFoundException: oracle.jdbc.OracleDriver
18:56:24.122 [ERROR] [org.gradle.BuildExceptionReporter] 	at build_km9o03ghb67vf1q533j9a4efl$_run_closure1.doCall(/projects/dbgradle/build.gradle:3)
18:56:24.123 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.ClosureBackedAction.execute(ClosureBackedAction.java:58)
18:56:24.124 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.util.ConfigureUtil.configure(ConfigureUtil.java:130)
18:56:24.125 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.util.ConfigureUtil.configure(ConfigureUtil.java:110)
18:56:24.125 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.AbstractTask.configure(AbstractTask.java:439)
18:56:24.126 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.project.AbstractProject.task(AbstractProject.java:942)
18:56:24.127 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.BeanDynamicObject$MetaClassAdapter.invokeMethod(BeanDynamicObject.java:246)
18:56:24.128 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.BeanDynamicObject.invokeMethod(BeanDynamicObject.java:134)
18:56:24.137 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.api.internal.CompositeDynamicObject.invokeMethod(CompositeDynamicObject.java:147)
18:56:24.138 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.groovy.scripts.BasicScript.methodMissing(BasicScript.java:79)
18:56:24.138 [ERROR] [org.gradle.BuildExceptionReporter] 	at build_km9o03ghb67vf1q533j9a4efl.run(/projects/dbgradle/build.gradle:1)
18:56:24.139 [ERROR] [org.gradle.BuildExceptionReporter] 	at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:52)
18:56:24.140 [ERROR] [org.gradle.BuildExceptionReporter] 	... 30 more
18:56:24.141 [ERROR] [org.gradle.BuildExceptionReporter] 
18:56:24.142 [LIFECYCLE] [org.gradle.BuildResultLogger] 
18:56:24.143 [LIFECYCLE] [org.gradle.BuildResultLogger] BUILD FAILED
18:56:24.144 [LIFECYCLE] [org.gradle.BuildResultLogger] 
18:56:24.145 [LIFECYCLE] [org.gradle.BuildResultLogger] Total time: 2.18 secs
...
heiko@nb:/projects/dbgradle$ 

Im unteren Drittel der Ausgabe findet man "Caused by: java.lang.ClassNotFoundException: oracle.jdbc.OracleDriver". In Java-Anwendungen würde man nun den CLASSPATH erweitern, aber wie kommt gradle zu zusätzlichen Java-Archiven? Eine normale Projekt-Abhängigkeit ist der Treiber jedenfalls nicht, sondern eine Abhängigkeit des Buildscripts selber.

Eine kurze Recherche bringt mich zu einer Diskussion auf stackoverflow. Eine Möglichkeit, den JDBC-Treiber anzuziehen, ist offensichtlich, dem von gradle genutzten Groovy-Classloader das entsprechende jar-File unterzuschieben. Also kopiere ich mir ojdbc7.jar aus einer Oracle 12c-Installation ($ORACLE_HOME/jdbc/lib/ojdbc7.jar) in ein Unterverzeichnis des dbgradle-Projekts:

heiko@nb:/project/dbgradle$ mkdir lib
heiko@nb:/project/dbgradle$ cp $ORACLE_HOME/jdbc/lib/ojdbc7.jar lib

Das Gradle-Buildscript bekommt eine neue Task loadDriver, von der callDatabase abhängt; callDatabase selber gibt zusätzlich die Anzahl der gelieferten Tabellen aus:

task loadDriver {
    URLClassLoader loader = GroovyObject.class.classLoader
    loader.addURL(file('lib/ojdbc7.jar').toURL())
    java.sql.DriverManager.registerDriver(loader.loadClass('oracle.jdbc.OracleDriver').newInstance())
}

task callDatabase(dependsOn: loadDriver) {
    println 'Connecting to database ...'
    def sql = groovy.sql.Sql.newInstance('jdbc:oracle:thin:@localhost:1521:DBSID', 'user', 'password', 'oracle.jdbc.OracleDriver')
    println '... connected'
  
    println 'Querying database tables ...'
    def rowCount = 0
    sql.eachRow('select * from all_tables') { row -> println "${row.owner}:${row.table_name}"; rowCount++ }
    println "... ready selecting $rowCount rows"
}

Und siehe da:

heiko@nb:/projects/dbgradle$ gradle callDatabase
Connecting to database ...
... connected
Querying database tables ...
SYS:IND$
SYS:COL$
SYS:ICOL$
SYS:TAB$
...
SYS:UTL_RECOMP_COMPILED
SYS:WRI$_HEATMAP_TOP_TABLESPACES
DVSYS:CODE_T$_TEMP
SYS:JAVA$MC$
... ready selecting 2316 rows
:loadDriver UP-TO-DATE
:callDatabase UP-TO-DATE

BUILD SUCCESSFUL

Total time: 4.041 secs
heiko@nb:/projects/dbgradle$

Damit habe ich mit etwas Groovy-Vorwissen und ziemlich geringen Aufwand ein recht übersichtliches Gradle-Buildscript, das grundsätzlich erkennen läßt, wie Datenbank-Aktionen im Build ausgeführt werden können. In Teil II dieses Artikels will ich dem Script dann beibringen, SQL-Anweisungen aus externen Dateien auszuführen, die ich außer über den Build auch in grafischen Datenbank-Frontends einsetzen und komfortabler in Editoren mit SQL-Syntax-Unterstützung bearbeiten kann.

 Posted by at 11:16

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)