今回はGCP(Google Cloud Platform)で提供されているDataformというETL(Extract / Transform / Load)サービスについてお話します。
DataformではGCSやBigQueryなどのデータソースに格納されているデータを読み込み、SQLX(SQLの拡張)もしくはJavaScriptを使って変換して、データストアに保存することができます。
Web上の記事ではSQLXでの実装が多い印象です。SQLXだけでもパワフルですが、JavaScriptを使えばより柔軟にロジックを記述できます。本記事ではJavaScriptでの実装例をご紹介します。
SQLXとJavaScriptでのデータ変換の違い
SQLXで記述する場合とJavaScriptで記述する場合の例を以下に示します。
config {
type: "table"
}
SELECT id, name FROM project_id.dataform.sample1
↑SQLXの場合(sample1テーブルのみを定義)
const tables = ["sample1", "sample2", "sample3"];
tables.forEach(table => {
const table_id = `project_id.dataform.${table}`;
publish( `${table}_extract`, {
type: "table",
}).query(ctx => {
return `SELECT id, name FROM ${table_id}`;
});
})
↑JavaScriptの場合(forEachでsample1~3までをまとめて定義)
見てわかる通り、SQLXは静的に定義を行うのに対して、JavaScriptでは変数を使って動的に定義を行えます。
SQLXは単一の大きなフローを定義するようなケースに向いていて、JavaScriptは同じ構造を持つデータセットを複数繰り返し処理するのに向いている印象です。
JavaScriptでのDataform定義の書き方
前提としてサービスアカウントに対するIAMの設定やDataformのリポジトリ、ワークスペースの作成は完了しているものとします。
JavaScriptで定義を書く場合にはdefinitionsディレクトリ配下に<filename>.jsのファイル名でファイルを作成します。
基本的には一般的なJavaScriptを記述するのと同じように、変数やロジックを記載していきます。
declareやpublishなどのDataform用の関数が用意されていて、それを利用してデータの定義やクエリ実行を行う部分がポイントとなります。

よく利用する関数は以下です。より細かな仕様は以下ドキュメントを参照ください。
【Dataform コアのリファレンス | Google Cloud】
https://cloud.google.com/dataform/docs/reference/dataform-core-reference?hl=ja
declare
データ変換では既に存在するデータソースを参照する必要があります。この時、既存のデータソースを定義するのがdeclare関数です。以下のように記述することでdataformデータセット内のsampleテーブルをデータソースとして指定できます。
declare({
schema: "dataform",
name: "sample"
});
ここで指定したデータソースはDataformの中でref関数によって参照できます(次のpublish関数を参照)。
また、declareしたデータソースはCOMPILED GRAPHタブで可視化されます。
publish
テーブルやビューを作成する際に利用します。例えば以下のように記述することでdeclareで定義したデータソースsampleからid, nameの列を抜き出して、BigQuery内にsample_extractテーブルを作ります。
publishではテーブルやビューの属性などを指定し、SQL自体はquery関数を使って記述します。
FROM句で、ref関数を使ってdeclareで定義したデータソースを指定しています。
publish( `sample_extract`, {
type: "table",
}).query(ctx => {
return `SELECT id, name FROM ${ctx.ref("sample")}`;
});
operate
任意のSQLを実行できます。publishでテーブルやビューのSELECTを書くことになるので、operateはINSERTやPARTITION BY 付きのCREATE TABLEを行うときなどに使っています。
※operateの場合はpublishと違ってqueryではなくqueriesである点に注意。
operate({
name: "some_operation",
description: "xxを実行"
}).queries( `INSERT INTO dataform.sample (
column1,
column2
) VALUES (
"xxx",
"yyy"
)`
);
columns
columnsではテーブルやビューの生成時にカラムの説明を記載できます。先のpublishの例だと以下のようになります。
publish( `sample_extract`, {
type: "table",
}).query(ctx => {
return `SELECT id, name FROM ${ctx.ref("sample")}`;
}).columns({
id: "顧客IDを表す",
name: "顧客名を表す"
});
また、operateを使ってCREATE TABLEをした場合など、SQLによってテーブルが生成される場合は、テーブルやビューを出力したことを伝えるhasOutput関数と組み合わせると、columnsを利用できます。
operate({
name: "create_table",
description: "テーブルを生成"
}).queries(`
CREATE TABLE dataform.sample (
column1 STRING,
column2 STRING,
date DATE
) PARTITION BY date;
`)
.hasOutput(true)
.columns({
column1: "カラム1",
column2: "カラム2",
date: "日付"
});
dependencies
前述したrefを使うことで、そのSQLはrefで参照しているテーブルやビューに依存していることをDataformに伝えることができます。このような場合はわざわざ依存関係を定義する必要はありません。
しかし、operateで指定したINSERTをしてから処理してほしい、など手動で依存関係を指定したい場合があります。この時使えるのがdependenciesです。
publish( `sample_extract`, {
type: "table",
}).query(ctx => {
return `SELECT id, name FROM ${ctx.ref("sample")}`;
}).columns({
id: "顧客IDを表す",
name: "顧客名を表す"
}).dependencies([
"insert_query1",
"insert_query2"
])
dependenciesは文字列でも、配列でも指定できます。operate関数の実行時に引数に与えたnameを引数に渡します。
assert
assertは異常検知に役立つ関数です。SELECT文を実行して結果が1行でも返ってきた場合は異常とみなし、エラーログを残すことができます。
assert("update_delayed_assertion").query(
(ctx) => `
SELECT *
FROM ${ctx.ref('sample_extract')}
WHERE id < 0`
);
上記ならidが負の数のものが1件でもあればエラーとして処理を停止します。

エラーログは上記のように管理画面でも確認できますし、GCPのログエクスプローラからも確認できます。アラート設定しておけば、定時実行するクエリで異常が無いか検知するのに役立ちます。
まとめ
簡単な例として、上記のdeclare関数とpublish関数を組み合わせると以下のようにして、複数のテーブルからデータを読み込んで、加工、それぞれの新しいテーブルに書き出すということができます。
const tables = ["sample1", "sample2", "sample3"];
tables.forEach(table => {
declare({
schema: "dataform",
name: table
});
publish( `${table}_extract`, {
type: "table",
}).query(ctx => {
return `SELECT id, name FROM ${ctx.ref(table)}`;
});
});
これをCOMPILED GRAPHでみると以下のようになります。

この例では単純ですが、複数のテーブルからUNIONやJOINで結合するようなケースがあると可視化で分かりやすくなりそうですね。
今回はDataformでJavaScriptを使って、複数テーブルを処理する例をご説明しました。
JavaScriptが使えることで動的にSQLを組み立てることができて便利ですね。GCPにはいろんなサービスがあるので、GCSのファイルをデータソースにして読み込んだり、スケジューリングの機能と連携したり用途が広がりそうです。
ぜひ、Dataformを試してみてください!

株式会社GLASS CTO。2010年、株式会社日立製作所にてIT新技術の調査・検証、Project Management Office、システム開発、マーケティング業務に携わる。2017年、株式会社Webios 取締役副社長 / CTO に就任。2018年、GLASS開発チームに参画。
AWS,GCPでのクラウドシステム開発、Ruby,PHPでのスクラッチ開発、CMS,ECパッケージでのサイト構築(WordPress / WooCommerce, MovableType, EC-CUBEなど)、kintoneでのシステム開発などに携わっている。