DataformでJavaScriptを使ってBigQueryのデータを変換する方法

今回は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で一緒に働いてみませんか?