この記事は、Java Advent Calendar 2016 の25日目の記事です。

Java Advent Calendar 2016
http://qiita.com/advent-calendar/2016/java

前日は、@newtaさんの「モダンなJavaの書き方。Immutable Java、Null安全を考えてみる」 でした。
http://qiita.com/newta/items/ccf9116909dc61ed7123

私自身groovyはgradleで「build.gradle」を書く以外にほぼほぼ使ったことが無いのですが。
機械学習でのFeatureEngineeringなどの用途で、
ETL的なことを行うバッチを書くなら、以下の理由でgroovyが適切な気がしたので、

  • 各種DBなどの接続ライブラリが充実しているJVMで動く
  • 修正がカジュアルに出来るスクリプト言語
  • プログラミング言語の、基本的なロジックが書ける内部DSLが使える

このエントリでは、
勉強ついでにETLというか、SQLを投げるツールを作ってみることにしました。

以下のチュートリアルを参考にしました。

Domain-Specific Languages | groovy-lang.org
http://groovy-lang.org/dsls.html

作るツールの説明

作るツールは、
以下のようなファイルでジョブを定義して、実行するツールです。

sample-etl.dsl

job "job1", {
    execute {
        db.execute "truncate table table_a"
        db.execute "insert into table_a (id) values (1)"
    }
    valid {
        def size = db.rows "select count(*) from table_a"
        assert 1 == size[0][0]
    }
}

run "job1"

このファイルでは、以下のようにジョブを定義しています。

  • ジョブの定義(ジョブのIDは「job1」)
    • ジョブで実行する処理の定義
      • table_aをtruncate
      • table_aに1行insert
    • ジョブ実行後のチェック処理の定義
      • table_aのレコード数が1かどうかをチェック
  • ジョブ「job1」の実行

このファイルを実行すると、以下の処理が順に流れることになります。

  • table_aをtrauncate
  • table_aに1行insert
  • table_aのレコード数をチェック

環境構築

gradleはセットアップ済みの前提で、以下の「build.gradle」を作成します。
DBはMySQLを想定、DB接続にはgroovy-sqlを使用します。

build.gradle

apply plugin: 'groovy'
apply plugin: 'application'

mainClassName = 'GroovyEtlMain'

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.codehaus.groovy', name: 'groovy', version: '2.4.7'
    compile group: 'org.codehaus.groovy', name: 'groovy-sql', version: '2.4.7'
    compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.5'
}

メインクラスのスクリプトファイルを置いて、
「gradle run」して成功すれば、準備完了です。

$ mkdir -p src/main/groovy
$ touch src/main/groovy/GroovyEtlMain.groovy
$ gradle run
※省略※
BUILD SUCCESSFUL

メインクラスの実装

メインクラスは以下のように実装しました。

src/main/groovy/GroovyEtlMain.groovy

import org.codehaus.groovy.control.*
import org.codehaus.groovy.control.customizers.*
import groovy.sql.*

class JobSpec {
    Sql db = null
    Closure executeCl = {}
    Closure validCl = {}
    void execute(Closure cl) { executeCl = cl }
    void valid(Closure cl) { validCl = cl }
    void run() {
        db = Sql.newInstance("jdbc:mysql://※MySQLホスト名※/※MySQLのDB名※", "※MySQLユーザ名※", "※MySQLパスワード※", "com.mysql.cj.jdbc.Driver")
        executeCl()
        validCl()
        db.close()
    }
}

@Singleton
class EtlUtil {
    static jobMap = new HashMap()
    def job(String key, Closure cl) {
        def jobspec = new JobSpec()
        def code = cl.rehydrate(jobspec, this, this)
        code.resolveStrategy = Closure.DELEGATE_ONLY
        code()
        jobMap.put(key, jobspec)
    }
    def run(String key) {
        def jobspec = jobMap.get(key)
        jobspec.run()
    }
}

def etlUtil = EtlUtil.instance
binding = new Binding([
        job: etlUtil.&job,
        run: etlUtil.&run
])

def importCustomizer = new ImportCustomizer()
def config = new CompilerConfiguration()
config.addCompilationCustomizers importCustomizer

def shell = new GroovyShell(binding, config)
shell.evaluate(new File("sample-etl.dsl"))

ざっくり全体を見ると。

  • JobSpecクラスで、job定義の型を定義
    • executeとvalidを持ち、DSLで処理を定義
    • runメソッドで、executeとvalidを実行(前後でDBへの接続・切断を実行)
  • EtlUtilクラスで、job定義のMap作成と実行処理を定義
    • jobメソッドで、job定義をMapに登録
    • runメソッドで、Mapに登録されたjobを実行
  • メイン部で、必要なクラスのbindingとDSLの評価を実行

これを実行すると、以下のようにMySQLのテーブルにレコードが追加されます。

$ gradle run
$ mysql ※接続情報※
MariaDB [test]> select * from table_a;
+------+
| id   |
+------+
|    1 |
+------+
1 row in set (0.00 sec)

設定をべた書きしていたりと、いろいろ突っ込みどころはあるのですが。
ヘルパークラスとかいろいろと用意したりなど、やることはまだまだありますが。
groovyの内部DSLを使うと、
そこそこ見やすく&柔軟なETL処理を書けるのではないかなと、思いました。

そんな感じで。みなさま Merry Christmas!!