SnowflakeのデータをWebに届ける・SQL APIとWordPressで始めるデータ可視化

Snowflakeにたまっているデータを、BIツール(QuickSight、Looker など)を使わずに、手元のWordPressサイトでさっと表やグラフにして見たい!という検証をしました。本記事では、WordPressプラグインの実装をできる限り薄くする考え方と、実際に書いたコードのポイントを紹介します。

全体の構成

今回は、WordPressからSnowflake SQL APIを呼び出し、取得したデータをWebページ上で表示します。構成を複雑にしないため、WordPress側ではデータの取得までを担当し、表やグラフなどの表示処理はブラウザ側に任せることにしました。そのために、両者の間ではSnowflakeから取得したデータをJSONとして受け渡します。

Snowflake・WordPress・ブラウザをつなぐ設計

今回の実装でポイントになるのが、<script id="sfq-data" type="application/json"> というタグです。このタグを、WordPress側とブラウザ側の責務を分ける境界として使います。

  • WordPress(PHP)側: Snowflakeに問い合わせて、結果をJSON文字列としてタグの中に差し込む
  • ブラウザ(JavaScript)側: JSONを読み込み、表やグラフとして描画する

この境界を決めておけば、表からグラフに変更したり、利用するJavaScriptライブラリを変更したりしても、PHP側のコードには手を入れる必要がありません。逆にWordPress側は、「Snowflakeにクエリを投げ、結果をJSONとして渡す」ことだけに集中できます。この役割分担によって、プラグイン側の実装もシンプルに保てます。

プラグイン構成

今回作ったプラグイン(snowflake-token-query-viewer)の構成です。認証方式はSnowflakeのPersonal Access Token(PAT、トークン方式)を使っています。

snowflake-token-query-viewer/
├── snowflake-token-query-viewer.php   … プラグイン本体(初期化)
├── src/
│   ├── Admin/
│   │   └── PageController.php         … 管理画面(接続設定・動作確認クエリ)
│   ├── Snowflake/
│   │   ├── SettingsRepository.php     … 接続設定の保存/取得
│   │   ├── QueryClient.php            … Snowflake SQL API v2への問い合わせ
│   │   ├── QueryResult.php            … 結果の入れ物
│   │   └── SnowflakeApiException.php  … 例外
│   └── Frontend/
│       └── InquiryDataShortcode.php   … [sfq_inquiry_data] ショートコード(今回の主役)
└── templates/
    └── page.php                       … 管理画面のビュー

ファイル数は少なく、それぞれの役割もはっきり分かれています。

WP側プラグイン

ツールでの設定(Snowflake接続情報)

接続情報(アカウント、ウェアハウス、データベース、スキーマ、ロール、トークン)は、wp-config.phpを編集するのではなく、「ツール」メニュー配下の設定画面から登録できるようにしました。保存先はWordPressのwp_optionsテーブルです。


Snowflake Token Query Viewerの管理画面。接続設定(Account/Warehouse/Database/Schema/Role/Personal Access Token)の入力フォームとRun queryのSQL入力欄が表示されている

src/Snowflake/SettingsRepository.php

class SettingsRepository {

	private const OPTION_PREFIX = 'sftq_snowflake_';
	private const TEXT_FIELDS   = array( 'account', 'warehouse', 'database', 'schema', 'role' );

	public function get(): array {
		$data = array();
		foreach ( self::TEXT_FIELDS as $field ) {
			$data[ $field ] = (string) get_option( self::OPTION_PREFIX . $field, '' );
		}
		$data['token'] = (string) get_option( self::OPTION_PREFIX . 'token', '' );
		return $data;
	}

	public function save( array $input ): void {
		foreach ( self::TEXT_FIELDS as $field ) {
			update_option( self::OPTION_PREFIX . $field, sanitize_text_field( $input[ $field ] ?? '' ), false );
		}

		// トークン欄が空なら上書きしない。既存のトークンをそのまま維持する
		$token = trim( sanitize_text_field( $input['token'] ?? '' ) );
		if ( '' !== $token ) {
			update_option( self::OPTION_PREFIX . 'token', $token, false );
		}
	}
}

ショートコードの実装要点

ショートコード名はsfq_inquiry_data、引数は実行するSQLを渡すsql属性の1つだけです。データ取得・出力の本体はこのクラス1つだけです。役割は本当に「Snowflakeに問い合わせて、JSON文字列を返す」だけで、HTMLもCSSも一切生成しません。

src/Frontend/InquiryDataShortcode.php

class InquiryDataShortcode {

	public function register(): void {
		add_shortcode( 'sfq_inquiry_data', array( $this, 'render' ) );
	}

	public function render( $atts = array() ): string {
		$atts = shortcode_atts( array( 'sql' => self::DEFAULT_SQL ), (array) $atts, 'sfq_inquiry_data' );
		$sql  = (string) $atts['sql'];

		if ( ! $this->settings->isComplete() ) {
			error_log( 'SnowflakeTokenQueryViewer: 接続設定が未保存です。' );
			return '[]';
		}

		try {
			$result = $this->client->run( $sql );
		} catch ( \Throwable $e ) {
			// 失敗時も画面を壊さないよう空配列を返し、詳細はログにのみ残す
			error_log( 'SnowflakeTokenQueryViewer: クエリ失敗: ' . $e->getMessage() );
			return '[]';
		}

		$rows = array_map( array( $this, 'mapRow' ), $result->rows );

		return (string) wp_json_encode( $rows, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG );
	}
}

失敗した場合(接続未設定・Snowflake側のエラーなど)も、ページを壊さないよう常に有効なJSON(空配列 [])を返します。詳細なエラーはerror_log()にのみ記録し、フロント側の出力には一切現れません。

クエリを引数にすることで汎用化

sql属性で外から渡せるようにしました。これで同じショートコード1つを使い回して、いろいろなクエリの結果を表示できます。

[sfq_inquiry_data sql='SELECT INQUIRY_ID, TO_VARCHAR(RECEIVED_AT, ''YYYY-MM-DD HH24:MI:SS'') AS RECEIVED_AT,
  PRODUCT, CATEGORY, CHANNEL, STATUS, PRIORITY, RESOLUTION_HOURS
  FROM "WP_DB"."SAMPLE"."INQUIRY_LOG" ORDER BY RECEIVED_AT']

補足: クエリはQueryClient側でSELECT/WITH文以外を弾くガードが入っているため、sql属性からDELETE文などを渡しても実行されません。ただしこれはあくまで簡易的なクライアント側チェックであり、本来の安全策はSnowflake側で読み取り専用ロールを使うことです。

参照のみなのでHTTPS/WebAPIでシンプル

SnowflakeにはPHP PDO Driverもあります。ただし今回は参照(SELECT)のみのシンプルな用途なので、SQL API v2を使いました。WordPress標準のwp_remote_post()でJSONをPOSTするだけで、ドライバのインストールも常時接続の管理も不要になります。

認証もPAT(トークン)をAuthorization: Bearerヘッダーに載せるだけなので、鍵ペア(JWT署名)方式に比べて実装はかなり薄くなります。

src/Snowflake/QueryClient.php

$response = wp_remote_post(
	'https://' . $account . '.snowflakecomputing.com/api/v2/statements',
	array(
		'timeout'    => 30,
		'user-agent' => 'SnowflakeTokenQueryViewer/' . SFTQ_PLUGIN_VERSION, // 既定のUAは391903エラーで拒否される
		'headers'    => array(
			'Authorization'                       => 'Bearer ' . $settings['token'],
			'X-Snowflake-Authorization-Token-Type' => 'PROGRAMMATIC_ACCESS_TOKEN',
			'Content-Type'                         => 'application/json',
		),
		'body'       => wp_json_encode( array(
			'statement' => $sql,
			'warehouse' => $settings['warehouse'],
			'database'  => $settings['database'],
			'schema'    => $settings['schema'],
			'role'      => $settings['role'],
		) ),
	)
);

画面描画はHTML/JS

PHP側が返すのはJSON文字列だけです。これを<script id="sfq-data" type="application/json">の中にそのまま差し込みます。

<script id="sfq-data" type="application/json">
[sfq_inquiry_data sql='SELECT INQUIRY_ID, TO_VARCHAR(RECEIVED_AT, 'YYYY-MM-DD HH24:MI:SS') AS RECEIVED_AT,
  PRODUCT, CATEGORY, CHANNEL, STATUS, PRIORITY, RESOLUTION_HOURS
  FROM "WP_DB"."SAMPLE"."INQUIRY_LOG" ORDER BY RECEIVED_AT']
</script>

JavaScriptでこのJSONを読み込んで<table>の行を組み立てるだけです。最小構成だとこれだけで動きます。

<table border="1">
  <thead>
    <tr>
      <th>ID</th><th>受付日時</th><th>製品</th><th>カテゴリ</th>
      <th>チャネル</th><th>ステータス</th><th>優先度</th><th>対応時間(h)</th>
    </tr>
  </thead>
  <tbody id="sfq-tbody"></tbody>
</table>

<script id="sfq-data" type="application/json">
[sfq_inquiry_data sql='SELECT INQUIRY_ID, TO_VARCHAR(RECEIVED_AT, 'YYYY-MM-DD HH24:MI:SS') AS RECEIVED_AT,
  PRODUCT, CATEGORY, CHANNEL, STATUS, PRIORITY, RESOLUTION_HOURS
  FROM "WP_DB"."SAMPLE"."INQUIRY_LOG" ORDER BY RECEIVED_AT']
</script>

<script>
// JSONを読み込んで<tbody>に1行ずつ追加するだけ
var rows = JSON.parse(document.getElementById('sfq-data').textContent);
var tbody = document.getElementById('sfq-tbody');
rows.forEach(function (r) {
  var tr = document.createElement('tr');
  tr.innerHTML =
    '<td>' + r.inquiryId + '</td>' +
    '<td>' + r.receivedAt + '</td>' +
    '<td>' + r.product + '</td>' +
    '<td>' + r.category + '</td>' +
    '<td>' + r.channel + '</td>' +
    '<td>' + r.status + '</td>' +
    '<td>' + r.priority + '</td>' +
    '<td>' + (r.resolutionHours === null ? '' : r.resolutionHours) + '</td>';
  tbody.appendChild(tr);
});
</script>

実際にWordPressの固定ページに設置した結果です。


WordPressの固定ページ上に表示された、Snowflakeから取得した問い合わせログの一覧テーブル(ID・受付日時・製品・カテゴリ・チャネル・ステータス・優先度・対応時間の8列、10件分)

この構成の良いところは、表示側をどれだけ作り込んでも(あるいは作り直しても)、PHP側・プラグイン側には一切手を入れなくていい点です。

発展: Chart.js でグラフ化

同じJSONをそのまま使って、<table>の代わりに<canvas>を置きChart.jsに渡せば、棒グラフ表示に切り替えられます。

<canvas id="sfq-chart"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.4/chart.umd.min.js"></script>
<script>
var rows = JSON.parse(document.getElementById('sfq-data').textContent);

// 受付日ごとに件数を集計
var countsByDay = {};
rows.forEach(function (r) {
  var day = r.receivedAt.slice(0, 10);
  countsByDay[day] = (countsByDay[day] || 0) + 1;
});

new Chart(document.getElementById('sfq-chart'), {
  type: 'bar',
  data: {
    labels: Object.keys(countsByDay),
    datasets: [{ label: '問い合わせ件数', data: Object.values(countsByDay) }]
  }
});
</script>

まとめ

表とグラフを出すだけなら、Snowflake連携用のWordPressプラグインは

  • 接続情報を管理画面から登録する
  • 決まったクエリ(または引数で渡されたクエリ)を実行してJSONを返す

の2つだけで十分でした。表示側の作り込みは全部JavaScriptに任せることで、プラグイン本体のコード量を最小限に抑えられます。BIツールを導入するほどでもない「ちょっとしたデータをサイトに出したい」場面では、この構成が手軽です。

×
タイトルとURLをコピーしました