groovyの内部DSLの勉強がてらにETLツールもどきを作ってみた
この記事は、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!!