- 岸本卓也
- 約 3,500 文字
- 1,100 View
目次
はじめに
こんにちは、TIGの岸本卓也です。夏の自由研究連載2024 シリーズです。
これまでPostgreSQLで手続き型処理を実装することにがっつりと向き合うことがなかったため手続き型処理の実装言語はPL/pgSQL一択だと思いこんでいたのですが、実は複数の選択肢がありました。PostgreSQL標準で提供されている選択肢はPL/pgSQL、PL/Tcl、PL/Perl、PL/Pythonですが、サードパーティ提供も含めると多数の言語が使えるます。
その中でもJavaScriptで実装できるPLV8は手馴染みが良さそうで興味を惹かれたので試してみることにしました。
DBの準備
適当にPostgreSQLデータベースとサンプルDBを作成しておきます。
DBインスタンス
Amazon RDS for PostgreSQLを利用してPostgreSQL 16.3のデータベースを作成しました。大手のクラウドベンダーではマネージドサービスのDBでもPLV8に対応しています。
cf. クラウドベンダーのマネージドPostgreSQLデータベースにおける対応拡張機能
PLV8拡張機能をインストールします。(cf. Installing PLV8)
-- 利用可能な拡張機能を確認 |
サンプルDB
今回は PostgreSQL wiki に掲載されている Pagila を利用しました。
DO ブロックでの実行
PLV8は DO による無名コードブロック実行にも対応しているため、手始めに DO ブロックで試してみます。
素朴なSQL実行の例
do $$ |
実行結果は以下です。特に問題なく期待通りの結果が得られました。
pagila=> do $$ |
SELECT 結果をカーソルで取得する例
plv8.execute ではSQL実行結果を一度に取得しますが、カーソルを使って逐次取得できます。カーソルで取得する方法を試してみます。
do $$ |
実行結果は以下です。こちらも特に問題なく期待通りの結果が得られました。
実行結果
pagila=> do $$ |
関数として作成して実行
CREATE FUNCTION 文を手作成
カーソルで取得する例のSQLを関数にして実行してみます。DO ブロックの代わりに CREATE FUNCTION にするだけなので、ついでに閾値を関数の引数で渡すように変更してみます。
create function print_top_customers(frequency integer) returns void as $$ |
実行結果
pagila=> create function print_top_customers(frequency integer) returns void as $$ |
DO ブロックでの実行と同じ結果が得られました。
JavaScript実装をバンドルして CREATE FUNCTION SQL を生成
前の例では CREATE FUNCTION 文を手作成しました。そのSQLにおいて1行目と最終行以外はJSの実装です。であれば、JS部分は独立して開発して最後に CREATE FUNCTION 文を生成できれば色々捗りそうです。それをやってくれるツールであるPLV8ifyが公式ドキュメントで紹介されていますので、ここからはPLV8ifyを使った関数の開発を試してみます。
PLV8関数開発環境の構築
適当なNode.js環境をインストールしておきます。今回はv20.16.0のNode.jsを利用しました。
開発用のフォルダで以下のコマンドによりプロジェクトを作成し、利用するパッケージをインストールします。PLV8ifyによる変換はTypeScriptしか対応していないのでTypeScriptの開発環境を整えています。
npm init -y |
上記でインストールされたパッケージのバージョンは以下の通り (npm list --depth=0 の出力) です。
パッケージバージョン
+-- @eslint/js@9.9.1 |
なお、 plv8ify は実際には上記でインストールされたバージョンそのものではなく、バグと思われる挙動や利便性向上をローカルで修正したものを使用しました。このため、ここより後の例ではTypeScriptで実装した関数に対して生成される CREATE FUNCTION 文の関数定義では関数名と引数名が snake_case 化されるようにしています。
PLV8関数のTS実装例
前の手作成 CREATE FUNCTION の例をTS実装にし、ついでに検索結果をログではなく戻り値として返却するように変更したものがこちらです。
/** |
plv8ify で CREATE FUNCTION SQL を生成し、
npx plv8ify generate --input-file src/fetch_top_customers.ts |
関数作成後にSQLを実行しました。
pagila=> \i plv8ify-dist/fetch_top_customers.plv8.sql |
手作成 CREATE FUNCTION の例と同じ結果が得られました。
実践的な例
より実践的な例としてPL/pgSQLの関数をPLV8関数に実装し直してみます。対象はサンプルDBにある rewards_report 関数です。ただし素の rewards_report 関数は CURRENT_DATE が使われていてテストしにくいため、 CURRENT_DATE 部分を引数で指定できるように変更した以下の関数を対象とします。
引数追加版 rewards_report 関数
CREATE FUNCTION public.rewards_report(min_monthly_purchases integer, min_dollar_amount_purchased numeric, today date) RETURNS SETOF public.customer |
上記PL/pgSQL関数を以下のようにTS実装しました。
引数追加版 rewards_report のTS実装
/** |
前の例と同様に CREATE FUNCTION SQL を生成し関数を作成してSQLを実行しました。
pagila=> select customer_id, first_name |
PLV8版の関数でもPL/pgSQL版の関数と同じ結果が得られました。
外部ライブラリの利用
rewards_report 関数は LAST_DAY 関数を呼び出していますが、前の例では既存の関数をSQLで呼び出していました。PLV8関数からPLV8関数を呼び出す場合はSQL実行よりも簡単に呼び出す手段があるため、 LAST_DAY 関数もPLV8化してみます。この関数は日付を扱うため、日時を扱う外部ライブラリの利用も試してみます。今回はDay.jsを利用し、以下のようにTS実装しました。
import dayjs from 'dayjs'; |
このTS実装から生成した CREATE FUNCTION SQL を確認すると、インポートしている dayjs はインライン化されることが分かります。
このPLV8関数版を使うように変更した rewards_report 関数のTS実装は以下です。 LAST_DAY 関数のPLV8化と同様にDay.jsを利用する変更を加えていますが、大部分は前掲の関数実装と同じなため差分だけ掲載します。
--- v8_rewards_report.ts 2024-08-29 13:01:54.774358100 +0900 |
このTS実装から生成した CREATE FUNCTION SQL を確認すると、 v8_last_day2.ts と同様に dayjs はインライン化されることが分かります。
これらの関数を作成してSQLを実行しました。
実行結果
pagila=> select customer_id, first_name |
PL/pgSQL版の関数と同じ結果が得られました。
Unit test
PostgreSQL関数を問題なくTS実装できることが確認できたので、次はTS実装のテストを試します。今回はテストフレームワークとモック作成にVitestを利用しました。シンプルな例として、サンプルDBにある inventory_in_stock 関数をPLV8化した以下のTS実装を使います。
/** |
CREATE FUNCTION SQL を生成し関数を作成してSQLを実行すると、PLV8版とPL/pgSQL版で同じ結果が得られます。
実行結果
pagila=> select |
この v8InventoryInStock 関数をテストする以下のテストスクリプトを作成しました。このテストではグローバルな plv8 オブジェクトはモックを作成してテストできるようにしました。
import { expect, test, vi } from 'vitest'; |
このテストを実行すると以下の通り成功することが確認できました。
✓ src/v8_inventory_in_stock.test.js (1) |
まとめ
JavaScriptでPostgreSQLの手続き型処理を実装できる拡張機能PLV8を試した結果の感想は以下です。
良い点
- Node.jsのエコシステム、各種ツールが使える。エディタ、Linter、ライブラリ、テスト、など
- フロントエンドの開発スキルが流用できる。
懸念点
- PLV8ifyの場合はインポートしたモジュールがPostgreSQL関数ごとにインライン化される。
- 関数定義が肥大化しそう。
- モジュールの変更時はそれに依存する関数を生成&作成し直す必要があるため、どのような単位でPostgreSQL関数化するかは要配慮と考えられる。
- 今回試した範囲ではまったく性能的な懸念はなかったが、PL/pgSQLに対して性能的にどうなのかは分からない。
- SQL実行やPostgreSQL組み込み関数の呼び出しはPL/pgSQLの方が簡単に書ける。
- PLV8ifyの場合はインポートしたモジュールがPostgreSQL関数ごとにインライン化される。
JavaScript (TypeScript) で開発できるというのは私は楽しく楽に実装できました。これは以下からくるものだと思います。
- エディタなどのJS開発支援機能による効率的な実装
- Linterに指摘してもらえる安心感
- テストしやすさからくる安心感
注意すべき点はありそうなものの十分実用できそうな印象です。いざとなればPL/pgSQLに切り替えるという選択肢も取れるので、チャンスがあれば実際のPJでも導入してみたいと思います。