まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:postgresql

自作サービスを開発している途中で調査した内容のアウトプットです。

Node.js を使って PostgreSQL データベースを操作する場合、pg(node-postgres) ライブラリを使うのが定番だと思っています。実際これまで何度も使ってきているし、データの読み書き更新削除といった作業で特に困ったことはありませんでした。

しかし今回ちょっとしたことで詰まってしまいました。結果的には解決できたのですが、データベース内に定義されたテーブルの、列の定義情報を調べたいと思った時に「これどうやるんだろ?」となってしまいました。

もう少し具体的に説明します。例えば以下のような SQL を使って items テーブルを作成したとします:
CREATE TABLE items( id varchar(50) primary key, name varchar(100) not null, price int default 0, body text, image bytea, datetime timestamp );

このようにして作成した items テーブルの各列の定義情報(上の例の青字部分)を取り出す方法が分からなかったのでした(列名だけであれば select 文の実行結果の中に含まれるので、1行でもレコードが登録されていればそこから分かる、ということは知っていました。が、レコードが1件も登録されていないケースだったり、列名以外の型の情報まで必要な場合の取得方法が分かっていませんでした)。ちなみにこの情報は psql コマンドを使った場合はログイン後に
# \d items

というコマンドを実行することで取得できることは知っていました("items" の部分に知りたいテーブル名を指定して実行します):
db=# \d items
                          Table "public.items"
  Column  |            Type             | Collation | Nullable | Default
----------+-----------------------------+-----------+----------+---------
 id       | character varying(50)       |           | not null |
 name     | character varying(100)      |           | not null |
 price    | integer                     |           |          | 0
 body     | text                        |           |          |
 image    | bytea                       |           |          |
 datetime | timestamp without time zone |           |          |
Indexes:
    "items_pkey" PRIMARY KEY, btree (id)

この方法を知っていたので、これまであまり気にすることもありませんでした。ところがこれはあくまで psql コマンドを利用する際のコマンドであって、これをそのまま SQL として pg を使って実行すると(SQL ではないので当然ですが)エラーとなってしまいます。ではいったいどうすれば pg でこの情報をプログラムのコード内で取り出すことができるのだろうか・・・ というのが今回のブログエントリのテーマです。


結論として分かったのは、こんな感じでした:
・SQL としては実行結果にすべての列が含まれるような SELECT 文(例: "select * from items")を実行する
・実行結果からレコードを取り出す場合は result.rows を参照するが、実行結果の列情報は result.fields と result._types._types.builtins を参照することで取り出すことができる
・実行結果のレコードが0件でも(1件もレコードが登録されていなくても)、上の方法で列情報を取り出すことはできる

具体的なコードとしてはこのような感じです:
var PG = require( 'pg' );
var pg = new PG.Pool({
  connectionString: "postgres://user:pass@hostname:5432/db",
  idleTimeoutMillis: ( 3 * 86400 * 1000 )
});

  :
  :

if( pg ){
  var conn = await pg.connect();
  if( conn ){
    try{
      var sql = 'select * from items';
      var query = { text: sql, values: [] };
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );
        }else{
          var fields = r1.result.fields;
          var types = r1.result._types._types.builtins;
          var columns = [];
          fields.forEach( function( f ){
            var dt = Object.keys( types ).reduce( function( r, key ){
              return types[key] === f.dataTypeID ? key : r;
            }, null );
            columns.push( { column_name: f.name, type: dt } );
          });

            :
            :
        }
      });
    }catch( e ){
      console.log( e );
    }finally{
      if( conn ){
        conn.release();
      }
    }
  }
}


赤字の部分の解説をします。まず "postgres://(ユーザー名):(パスワード)@(PostgreSQL サーバー名):(ポート番号)/(DB名)" というフォーマットの接続文字列を使ってデータベースに接続します(正しく接続できるのであれば、このフォーマットである必要はありません)。 そして接続後に "select * from items" というシンプルな SQL を実行して、結果を result という変数で受け取ります。この SQL 実行結果(レコード情報)自体は result.rows という属性に配列形式で格納されているのですが、今回ここは使いません。

この SQL を実行することにより、指定したテーブル(今回の場合は items)の列名とデータ型IDの情報が result.fields に、データ型IDとデータ型の関係を示す表が result._types._types.builtins に格納されているはずです。これらを取り出し、各列のデータ型を ID ではなく文字列に変換しなおして、最終的に columns という配列変数に記録しています。

この columns の実行結果を参照すると、このような値になっているはずです:
    [
      {
        "column_name": "id",
        "type": "VARCHAR"
      },
      {
        "column_name": "name",
        "type": "VARCHAR"
      },
      {
        "column_name": "price",
        "type": "INT4"
      },
      {
        "column_name": "body",
        "type": "TEXT"
      },
      {
        "column_name": "datetime",
        "type": "TIMESTAMP"
      }
    ]

"integer" 型が、より正確な "INT4" という型になっていたりはしますが、当初取得したかった列の定義情報を取得することができました。なお、この方法であれば SQL の実行結果(result.rows)そのものを参照しているわけではないため、実行結果が0件であっても(レコードがまだ1件も登録されていない場合でも)実行できるようです。

サンプルソースコードはこちらからどうぞ:
https://github.com/dotnsf/pg_fieldtype


(2023-12-18 追記)
ちなみに MySQL の場合、その名もズバリの mysql ライブラリを使うのが定番だと思ってますが、こちらの場合はテーブル一覧("show tables")もテーブル定義("desc (テーブル名)")も、CLI で使う命令文をそのまま利用して取得することができるので、深く考えなくてもよいのでした。




自分は多くのウェブサービスやウェブアプリを開発/公開していますが、そのほとんどが無料です。中には「有償でもいいかな?」と思えるものもあったりしますが、無料で公開している最大の理由は「課金管理の仕組みが難しい」からというのが大きいです。

自分で作ったサービスを有料公開しようとすると、いくつかの問題点が出てきます。まずそもそも「どうやって支払ってもらうのか?」を解決する必要があります。理想的には月額制サブスクにして、クレジットカードで・・みたいな感じになりますが、クレジットカードと連動する仕組みを個人で用意するのはかなり大変です。クレジットカード情報は自サービスに記録しないとしても、サービスと連動させるための情報は記録する必要があり、場合によっては個人情報保護も考慮する必要が出てきます。 個人情報を取得しなかった場合でも自サービスのユーザーの情報は自サービス内で管理する必要があります。ログインとかオンラインサインアップとか、パスワードを忘れてしまった場合のリセットなどです。これらの仕組みを提供してユーザーを管理した上で、どのユーザーが有償サービスに移行して、どのユーザーは無料ユーザーのままで、そして有償サービスに移行したユーザーはどういうプランで・・・といった情報をすべて管理する必要があるのでした。これらを解決するための仕組みづくりがサービス本体と比べてもかなり面倒なのでした。多くの個人開発者が共通に悩む点だと思っています。

そんな中で一念発起して、この「ユーザー管理&課金管理」に挑戦してみました。今回はユーザー管理機能として Auth0 を、課金部分は LINE Pay を使ってみました。いずれも Node.js 向けの外部連携ウェブ API (SDK) が提供されていて、普段の自分が開発している環境で比較的容易に実現できるものでした。ユーザー管理機能は Auth0 である必要はありませんが、他の IDaaS を使う場合はその IDaaS 向けにログインやログアウト、オンラインサインアップ等を実装する必要があります。ユーザー毎の課金管理はこのアプリ内のデータベースで管理しますが、その際に使うユーザーの ID をどのように用意するかをあらかじめ考慮しておく点がある点に注意ください。例えば後述のサンプルでは Auth0 のユーザーIDをそのまま使っています。これを見た目でよりわかりやすくするために、ユーザーのメールアドレスを ID として利用することも可能ですが、その場合は個人情報をデータベースに記録することになる、という点に留意が必要です(それが問題ないケースであれば、メールアドレスをユーザー ID とするのがリーズナブルだと思っています。今回は個人情報取得を避ける目的でユーザーを特定しにくい ID を使っています)。

以下にサンプルアプリの使い方と、そのサンプルアプリを利用するための準備作業を記載していきます。なお実際に動かす場合(実際の支払いは発生しませんが、一連の手順を確認する場合)、本アプリでは支払いに LINE Pay を使うので、LINE のアカウントと LINE のインストールされたスマートフォンが必要になります。こちらは事前に用意してください(※後述のサンプルは実際に料金を支払うわけではないのですが、LINE Pay の仕組みを使って支払い処理を行うことにはなるので LINE のインストールされたスマホが必要です)。

【PostgreSQL データベースの準備】
本サンプルアプリケーションでは(Auth0 の)ユーザーと、そのトランザクションを PostgreSQL データベースを使って紐づけて管理します。そのため PostgreSQL データベースが必要です。

クラウド環境でも、ローカルへのインストールでも、Docker コンテナなどでも構わないので、PostgreSQL データベースを1つ用意してください。以下では
 postgres://user:pass@xx.xx.xx.xx:5432/db  
(ユーザー名=user、パスワード=pass で利用可能なデータベース db が xx.xx.xx.xx:5432 で動いている)

という URL でアクセスできる PostgreSQL データベースが存在している想定で説明を続けます。


【IDaaS (Auth0)側の準備】
本サンプルではユーザー管理機能として Auth0 を使います。そのため Auth0 のアカウントを取得した上で、アプリ連携するための準備が必要です。Auth0 のアカウントを未取得の場合はこちらからサインアップしてください(開発用の無料アカウントでかまいません):
https://auth0.com/signup

アカウント取得後にログインし、ダッシュボード画面で「アプリケーション」を1つ作成します。ログイン後のダッシュボード画面左で "Applications" - "Applications" を選び、画面右上に表示される "Create Applications" ボタンをクリックします:
2022090501


"Create application" ダイアログが表示されるので、アプリケーション名(下図では "LINE Pay with IDaaS")を適当に入力後、"Regular Web Applications" を選択してから "Create" ボタンをクリックします:
2022090502


アプリケーション作成後に "Settings" タブを選択します。そして Domain, Client ID, Client Secret とそれぞれ書かれた3つのフィールドに値が設定された値を確認します(これらの値は後で必要になります):
2022090503


そのまま下にスクロールして、"Application URLs" を探します。ここには "Application Login URLs", "Application Callback URLs", "Allowed Logout URLs", ・・といった設定項目が並んでいます。最初の "Application Login URLs" は空のままでいいのですが、次の2つにはそれぞれ以下を入力します(※1):
 ・"Application Callback URLs": "http://localhost:8080/auth0/callback"
 ・"Allowed Logout URLs": "http://localhost:8080"
2022090601


※1 これらは localhost 上で動かす場合の指定です。このアプリをインターネット上の公開サーバーで実行する場合は、そのホスト名や IP アドレスも URL 形式で指定する必要があります。複数の URL を指定する場合は半角カンマで区切って指定します。


ここまで指定できたら画面の最下部までスクロールして "Save Changes" ボタンをクリックして内容を保存します。これで Auth0 の(最低限の)準備は完了です:
2022090602



この後でサンプルアプリケーションを動かします。その際に利用するユーザーは(そのユーザーのメールアドレスを使って)オンラインサインアップすることもできるのですが、この時点で直接作成しておくこともできます。ここではユーザーの作成方法を紹介するので、この方法でアプリケーションで利用するユーザーを作るか、あるいは後ほどアプリケーション実行時にオンラインサインアップしてユーザー登録を行ってください。

Auth0 のユーザーをダッシュボードから作成する場合は、左メニューで "User Management" - "Users" を選び、画面右上の "Create User" ボタンをクリックします:
2022090603


"Create user" ダイアログが表示されるので、上からメールアドレス、パスワード、パスワード確認を入力します。また "Connection" は "Username-Password-Authentication" を選択したままにします。最後に右下の "Create" ボタンをクリックするとユーザーを直接登録することができます:
2022090604


作成されたユーザーは Users 画面に(ダッシュボードから作成したユーザーと、オンラインサインアップしたユーザー両方が)一覧表示されます:
2022090605


Auth0 側の事前準備はこれで終わりです。


【LINE Pay 側の準備】
続いて課金管理を行う LINE Pay 側の事前準備作業を紹介します。具体的にはアプリケーションを LINE Pay 連携させる際に必要な Channel ID と Channel Secret と呼ばれるキー値が必要になるので、これらを取得します。

最終的に作成したアプリで本当にユーザー課金を求める(実際のお金で決済する)場合は LINE Pay に加盟店登録が必要になります。が、今回は試験的に動作確認できればよいレベルで動かしたくて、(開発途中で)試験的に動かした際に課金してほしくもない状態です。 そのような場合向けにサンドボックスと呼ばれる開発者向け環境が用意されているので、そのサンドボックスを使って Channel ID と Channel Secret を取得し、動作確認させてみます(サンドボックス環境での決済はあくまで動作確認のための決済処理であり、実際に支払われることはありません)。

サンドボックスを使うには以下のアドレスからサンドボックス生成申請を行います:
https://pay.line.me/jp/developers/techsupport/sandbox/creation?locale=ja_JP

国(JP(日本), TW(台湾),TH(タイ)から選択)、サービスタイプ(Online)、利用通貨(JPY(日本円)かUSD(アメリカドル)から選択)、そしてメールアドレスを入力し、最後に "Submit" ボタンをクリックします:
2022090606


すると指定したメールアドレスに以下のようなメールが届きます:
2022090607


メールに記載されているテスト ID とパスワードを使って LINE Pay の加盟店 My Page にアクセス/ログインしてみます。以下のページを開いてテスト ID とパスワードを入力し、最後に「ログイン」ボタンをクリックします:
https://pay.line.me/portal/jp/auth/login

2022090608


ログイン直後は以下のような画面が表示されます。サンドボックスに移動するには画面右上の "Sandbox" と書かれたメニューをクリックします:
2022090601


サンドボックスが表示されたら、画面左のメニューから「決済連動管理」- 「連動キー管理」を選択します。連動キー管理画面でパスワードチェックの画面が表示されたら、(上述のサンドボックス申請をした時にメールで送られてきた)パスワードを入力して「確認」ボタンをクリックします:
2022090602


すると連動キー管理画面内に "Channel ID" と "Channel Secret Key" が表示されます。これらはアプリケーションを動かす際に必要になるものです:
2022090603


LINE Pay 側の事前準備もこれで終わりです。ここまでできていれば実際にサンプルアプリを動かすことができます。


【サンプルアプリの準備】
では、ここまで準備してきた
 ・PostgreSQL データベース
 ・Auth0 によるユーザー管理
 ・LINE Pay によるオンライン支払い処理
が連動する Node.js サンプルアプリケーションを実際に動かしてみます。

まずサンプルアプリケーションは Node.js 前提で作っているので、 Node.js の導入がまだであれば最初に自分の環境向けのモジュールをインストールしておいてください:
https://nodejs.org/ja/


次にソースコードを入手します。以下のリポジトリから git clone するか zip & download & unzip でソースコードを入手してください:
https://github.com/dotnsf/linepay-idaas

2022090604


このソースコードのルートフォルダに linepay-idaas.ddl というファイルがあります。この DDL ファイル(の内容)で PostgreSQL データベースのテーブル定義を行います。psql などを使って PostgreSQL に接続し、この DDL ファイルと同じ内容を実行してテーブルを定義してください(psql を使う場合は接続語に "\t linepay-idaas.ddl" コマンドで実行できます):
> \i linepay-idaas.ddl
> \q

また、このアプリケーションはいくつかの環境変数を参照して動きます。その環境変数を指定できるよう、ソースコードのルートフォルダ(app.js と同じフォルダ)内に .env というファイルを作り、以下のような内容に編集して保存してください:
LINE_PAY_CHANNEL_ID=XXXXXXXX(LINE Pay Channel ID の値)
LINE_PAY_CHANNEL_SECRET=XXXXXXXX(LINE Pay Channel Secret の値)
LINE_PAY_CONFIRM_URL=http://localhost:8080/pay/confirm(このままの値を指定)
DATABASE_URL=postgres://user:pass@xx.xx.xx.xx:5432/db(PostgreSQL データベースのアクセスURL)
PGSSLMODE=no-verify(PostgreSQL データベースにアクセスする際の SSL モードを指定、SSL を使わない場合は disable を指定)
AUTH0_CALLBACK_URL=http://localhost:8080/auth0/callback(このままの値を指定)
AUTH0_CLIENT_ID=XXXXXXXX(Auth0 Client ID の値)
AUTH0_CLIENT_SECRET=XXXXXXXX(Auth0 Client Secret の値)
AUTH0_DOMAIN=dev-xxxxxxxx.us.auth0.com(Auth0 Domain の値)

↑上で説明した PostgreSQL や Auth0、 LINE Pay で取得した値を多く使っています。設定時に取得した値を正しく入力してください。

またこちらは編集必須ではないのですが、実際にオンラインで購入する(LINE Pay で支払う)商品の情報を item.json ファイルに記載しています。この中身を編集することで、この後の動作確認時に購入する商品の名称や価格を変更することができます(デフォルト状態では以下のようになっていて、この場合は「サービス利用料金」という商品を 「100 円」で購入することになります)。必要に応じて編集して使ってください:
{
  "productName": "サービス利用料金",
  "amount": 100,
  "currency": "JPY"
}


.env ファイルと item.json ファイルの準備ができたら、後はアプリケーションの実行に必要なライブラリをインストールします。以下のコマンドを実行して、依存ライブラリをインストールします:
$ cd linepay-idaas
$ npm install

【サンプルアプリの実行】
では実際にサンプルアプリケーションを起動して、実際にログイン&支払い処理の稼働確認をしてみましょう。まずは以下のコマンドでアプリケーションサーバーを起動します(ちなみにアプリケーションサーバーを止める場合は Ctrl +C を押します):
$ npm start

アプリケーションが起動すると 8080 番ポートで待ち受けるので、ウェブブラウザから http://localhost:8080 を指定してアクセスします(実際の運用ではスマートフォンのブラウザからアクセスすることもあると思っていますが、今回は PC ブラウザを使っています)。このアプリケーションは未ログインの状態でアクセスすると、強制的にログインページに遷移するように作られているので、Auth0 のログイン画面に移動します。ここで(ダッシュボードからユーザーを作成済みであれば)メールアドレスとパスワードを指定してログインします。または一番下のリンクからオンラインサインアップすることも可能です。いずれかの方法でログインします:
2022090605


ログインに成功すると、以下のような画面が表示されます:
2022090606


画面右上にはログインしたユーザーの情報が表示されています。鍵のマークがついたユーザーは「(まだ支払い処理を行っていない)無料ユーザー」であることを示しています。またその右には Auth0 のユーザーアイコン(特に指定していない場合はデフォルトアイコン)が表示されます。なお、この部分をクリックすることでログアウトも可能です:
2022090607


また画面中央にはそのユーザー(今の状態であれば無料ユーザー)向けの内容が表示されています。現在は無料ユーザーなので、無料ユーザー向けのコンテンツと、有料ユーザーになるための支払いを行うアイコンボタン(緑の "LINE Pay" ボタン)が表示されています。後で有料ユーザーになると、この画面が変わるので確認しておいてください:
2022090608


ではこの無料ユーザーが支払い処理を行うことにします。"LINE Pay" と書かれた緑のボタンをクリックします。すると以下のような画面に切り替わります。LINE Pay での支払い処理に移るため、LINE がインストールされたスマホを使って、LINE アカウントを指定してログインするか、画面の QR コードをスキャンしてログインしてください:
2022090601


するとスマホ側には以下のような画面が表示されます。item.json ファイルで指定した内容の商品名と価格が表示され、"PAY NOW" というボタンが表示されています。この "PAY NOW" ボタンをクリックして購入します(サンドボックス環境で実行しているので、実際の決済や支払いは行われません):
2022090601


購入すると、"PAY NOW" ボタンは「決済が完了しました」というメッセージに切り替わります:
2022090602


購入処理と同時に Auth0 にログインしていたウェブブラウザの画面が自動的に更新され、以下のような内容になります:
2022090606


変わった点として、まず画面右上のアイコンから鍵マークが消えています。これは「有料ユーザー」のアイコンで、無料ユーザーから有料ユーザーへ切り替わり、そのことをアプリケーションでも認識できたことを意味しています:
2022090607


また画面中央の内容が無料ユーザー向けコンテンツから有償ユーザー向けコンテンツに変わりました。入金によってユーザー属性が切り変わったことで有償ユーザー向けの画面を表示しています:
2022090608


↑なお、この画面内には「(動作確認検証用)フリーユーザーに戻る」というリンクが表示されています。実際のアプリケーションでは有償ユーザーがわざわざ無料ユーザーに戻る、というアクションを起こしたり、そのためのリンクを用意することはないと思うのですが、今回のアプリケーションでは何度も動作確認できるよう(一度有償ユーザーになった後も、無料ユーザーに戻って実行したくなることもあると思うので)意図的にこのようなリンクを用意しています。 このリンクをクリックすると確認メッセージ後に支払いデータの削除が実行され、無料ユーザーに戻ることができます(繰り返しますが、あくまで動作確認用の機能であり、実際のアプリケーションではこのようなリンクを付けることはないと思っています):
2022090606


なお、上述の「無料ユーザー向け画面」「有償ユーザー向け画面」は、ソースコード内の views/index.ejs 内で以下のように定義しています。この部分を変更して再実行することで無料ユーザーおよび有償ユーザー向けの画面をカスタマイズすることができます。興味ある方はカスタマイズにも挑戦してみてください:
  :
  :
<div class="container">
<% if( user.type == 0 ){ %>
フリーユーザー向けコンテンツ部分<br/>
<a href="/pay/reserve"><img src="/pay_btn.png"/></a>
<% }else{ %>
有償ユーザー向けコンテンツ部分<br/>
<a href="#" onClick="userDeleteType();">(動作確認検証用)フリーユーザーに戻る</a>
<% } %>
</div>
  :
  :

緑のタグに囲まれた赤字部分が無料ユーザー向け画面青字部分が有償ユーザー向け画面として表示されます

サンプルアプリ内でログインしたユーザーが課金しているかどうかの判断を含めて実現できていると思っています。


決済処理をした後で LINE Pay 加盟店 My Page にアクセスすると、取引管理や売上管理メニューから決済内容やその ID(=トランザクションID)を参照することができます(ただこの時点では、あくまで LINE 内の取引としてしか参照できないので、「誰が」決済したかを確認することはできません):
2022090609


このアプリケーションを使って「誰が」決済したのかを参照するには、アプリケーションの PostgreSQL データベース内を参照する必要があります。PostgreSQL データベースに接続して transactions テーブル内を参照すると、トランザクション ID とユーザーID、注文 ID などが紐づけられて管理されていることが確認できます:
2022090602


ユーザーIDは(このアプリケーションの場合は)Auth0 のユーザー ID なので、ユーザーを特定したい場合は Auth0 のユーザー一覧から、この ID をキーに検索することでユーザーを特定することができます。


なお、本サンプルアプリケーションでは IDaaS として Auth0 を使っていますが、他の IDaaS でも同様に使うこともできると思います。ログインやコールバックといった処理部分は書き換えが必要だと思っていますが、ユーザーを一意に特定する ID さえ取得できれば、その ID を PostgreSQL データ内の user.id として使うことになるだけなので、比較的容易に移植できると思っています。

いかがでしょう? とりあえずこのアプリをベースに改良することで(または同等のアプリを実装することで)個人開発サービスに組み込めるレベルで Auth0 と LINE Pay を使ったウェブサービスを開発できそうな目途は立ったように感じています。


【(サンドボックス環境ではなく)実際に取引する場合の注意】
とりあえず開発環境内で実際に取引できることは確認できたと思っています。これを(画面を変えたり、商品や値段を変えたり、決済方法を変えたり、など)カスタマイズして組み込むことで、個人の作ったウェブサービスに課金機能を組み込むこともできると思っています。が、そのためには3つの課題を意識する必要があります。

1つ目は「サンドボックスではなく、正式な取引をするには LINE Pay に加盟店登録が必要になる」ことです。私自身もここは実際に試したことはなく、どのくらい大変なのか/よゆーなのか、よくわかっていません。ちょっと調べた範囲で(2022/09/06 時点の)現状では PayPay への加盟店登録も必要そうで、ちと面倒そうな印象もあります・・・ 詳しい手続きについてはこちらを参照ください:
https://pay.line.me/merchant-apply/jp/selection/login-v2

(2022/09/11 追記)
実際に加盟店登録に挑戦してみました:
http://dotnsf.blog.jp/archives/1080934049.html


2つ目は支払い方法の問題です。例えば有償で提供するサービスが1回ごとの課金であったり、初回まとめて課金後はずっと使える、というものであれば実装にも大きな問題はないと思うのですが、例えば「月額サブスクリプション」のような支払いサービスだと LINE Pay 側が未対応のように思っています(違っていたらごめんなさい)。例えば支払いのタイミングを記録しておいて、過去の同一月に支払いの記録があればその月は有償ユーザーとして扱い、支払い記録がない場合は無料ユーザーとして扱う、などの工夫をアプリケーション側で行う必要があるように思えます。


3つ目は決済手数料の問題です。おそらくこの情報が最新だと思っているのですが、デジタルコンテンツの決済手数料は 5.5% です。つまり実際に1万円売り上げた場合 550 円を手数料として LINE Pay に支払う必要がある、またこの割合はいつか変わる可能性がある、という点に留意ください。
https://linecorp.com/ja/pr/news/ja/2021/3876



(こちらを参考にさせていただきました)
https://qiita.com/nkjm/items/b4f70b4daaf343a2bedc



Node.js + PostgreSQL 環境で複数レコードのバルクインサートを、それも対象テーブルに UNIQUE 制約が存在している場合のバルクインサート方法について調べる機会があったので、ブログにまとめておきます。


【Node.js で PostgreSQL を使う】
まず、Node.js で PostgreSQL クライアント機能を実装する場合、よく使われているのが pg(node-postgres) という npm のライブラリだと思っています:
2022080601


こんな感じでコネクションプーリングを作って、、
  :
  :
var PG = require( 'pg' );
var pg = new PG.pool( 'postgres://localhost:5432/mydb' );
  :
  :


こんな感じで SQL (以下の例は "INSERT" 処理)を実行し、結果を取得します:
  :
  :
if( pg ){
  var conn = await pg.connect();                                      //. コネクション割当
  if( conn ){
    try{
      var sql = "insert into table1( name, num ) values ( $1, $2 )";  //. SQL
      var query = { text: sql, values: [ "木村", 100 ] );             //. SQL パラメータ
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );                                         //. SQL 実行時エラー
        }else{
          console.log( result );                                      //. SQL 実行結果
        }
      });
    }catch( e ){
      console.log( e );                                               //. 例外発生
    }finally{
      if( conn ){
        conn.release();                                               //. コネクションのリリース
      }
    }
  }
}
  :
  :


今回もこの pg を使って紹介します。


【Node.js + PostgreSQL でバルクインサートを実装する】
上の例では一回の SQL 実行で1つのレコードをインサートする、という処理の例を紹介しました。複数のレコードをインサートする場合もこれを複数回続けて実行してもよいのですが、レコード数が多い場合に効率よくインサートできると便利です。このように「複数のレコードを1回の処理でまとめて作成する」ことを「バルクインサート」と呼びます。

Node.js + PostgreSQL でバルクインサートを実装する方法もいくつかありますが、今回は node-pg-format という npm ライブラリを使った例を紹介します:
2022080602


実際に使う場合はバルクインサートデータを配列で用意して、以下のように実装します:
  :
  :
var format = require( 'pg-format' );

var records = [         //. インサートするレコードを配列で用意
  [ "木村", 100 ],
  [ "鈴木", 101 ],
  [ "木村", 102 ]
];

if( pg ){
  var conn = await pg.connect();                                      //. コネクション割当
  if( conn ){
    try{
      var sql = format( "insert into table1( name, num ) values %L", records );  //. SQL
      var query = { text: sql, values: [] );                          //. SQL パラメータ
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );                                         //. SQL 実行時エラー
        }else{
          console.log( result );                                      //. SQL 実行結果
        }
      });
    }catch( e ){
      console.log( e );                                               //. 例外発生
    }finally{
      if( conn ){
        conn.release();                                               //. コネクションのリリース
      }
    }
  }
}
  :
  :

上例のように、SQL 文全体を pg-format でインスタンス化しています。その際にインサートする複数レコードを配列データとして付与しています(conn.query() 実行時のパラメータは空です)。これで records 配列変数で指定された複数レコードを1回の SQL 実行でまとめてインサートすることができるようになります。


【Node.js + PostgreSQL のバルクインサートと UNIQUE 制約】
ここからがこのブログエントリの本題に入っていきます。バルクインサートの方法は上で紹介しました。問題はこれではうまくいかないケースがあるのでした。具体的には対象テーブルに UNIQUE 制約が付与されていて、この制約にかかるレコードがバルクインサートの一部に含まれる場合です。

具体的に紹介します。例えば今回は table1 という名称のテーブルに複数レコードをバルクインサートする例を紹介していますが、この table1 テーブル作成時に以下のような指定がされていた可能性は排除できません:
create table table1( name varchar(50) default '', num int default 0 );
alter table table1 add unique( name, num );

1行目の "create table ..." はいいとして、問題は2行目です。この制約を与えたことによって name と num の組み合わせは一意(UNIQUE) でないといけない、という条件が付与されました。name は被ってもいいし、num も被ってかまわないのですが、name と num の組み合わせはユニークでないといけない(この条件に反するデータはインサートできない)ようになりました。

このような UNIQUE 制約が存在している前提で、改めて上例を見てみます。この例でバルクインサートしようとしているレコードは、
var records = [         //. インサートするレコードを配列で用意
  [ "木村", 100 ],
  [ "鈴木", 101 ],
  [ "木村", 102 ]
];

という内容でした(配列の1つ目が name 、2つ目が num としてバルクインサートされます)。ここだけを見るとこの例に限っては(name と num の組み合わせは全て異なるので)問題なくバルクインサートできそうです。 ・・・が、もしこのバルクインサート実行前に既に { name: "木村", num: 100 }
のデータが記録済みであった場合
はどうでしょう?バルクインサートの最初のレコードは UNIQUE 制約にかかってしまいます。つまり実行エラーとなります。

そして更にこの問題をややこしくしている事情があります。それは「バルクインサートが UNIQUE 制約エラーで失敗するのはいいが、その時 UNIQUE 制約にかからなかったレコードはインサートされるべきか?否か? という問題です。この答はおそらくデータやアプリケーション次第、つまりケース・バイ・ケースです。まとめてインサートされないと意味がないレコードであった場合はまとめてエラー(UNIQUE 制約にかからなかったレコードもインサートされない)という挙動が正しい挙動となり、UNIQUE 制約にかかったレコードはインサートされないが、他のレコードはインサートされるべきデータであった場合はバルクインサートの一部だけが失敗し、残りのレコードは挿入されているべき、ということになります。 

ちなみに上例で紹介したバルクインサート実装の場合は前者の挙動となります。つまり「全てのレコードが制約違反にならなかった場合のみバルクインサートが成功し、1つでも制約違反のレコードがあった場合はそうでないレコードも含めて1つもインサートされずにエラーとなる」という挙動です(そもそも厳密には「バルクインサート」とはそのような挙動および処理を意味する言葉です)。


【Node.js + PostgreSQL のバルクインサートと UNIQUE 制約】
長い前置きでした。ではここまでの背景を理解した上で UNIQUE 制約に違反しているレコードのみ挿入されず、違反していないレコードは正しく挿入されるようなバルクインサート(「バルクインサート」の定義としてはおかしいですけど・・)を実装するにはどのようにすればよいでしょうか? これが本ブログエントリの肝となる部分です。

これは実際には使っている RDB やライブラリ、プログラミング言語によって異なるようですが、PostgreSQL 9.5 以上で、Node.js + node-postgres を使っている場合であれば以下の方法で実現できるようです:
  :
  :

var records = [         //. インサートするレコードを配列で用意
  [ "木村", 100 ],
  [ "鈴木", 101 ],
  [ "木村", 102 ]
];

if( pg ){
  var conn = await pg.connect();                                      //. コネクション割当
  if( conn ){
    try{
      //. UNIQUE 制約に違反したデータを無視するような SQL を作る
      var sql = "insert into table1( name, num ) ";
      var selects = [];
      for( var i = 0; i < records.length; i ++ ){
        selects.push( "select '" + records[i][0] + "', " + records[i][1] );
      }
      sql += selects.join( " union all" ) + " on conflict ( name, num ) do nothing";

      var query = { text: sql, values: [] );                          //. SQL パラメータ
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );                                         //. SQL 実行時エラー
        }else{
          console.log( result );                                      //. SQL 実行結果
        }
      });
    }catch( e ){
      console.log( e );                                               //. 例外発生
    }finally{
      if( conn ){
        conn.release();                                               //. コネクションのリリース
      }
    }
  }
}
  :
  :


この例ではプログラミングによって、以下のような SQL 文を生成して実行しています:
insert into table1( name, num )
  select '木村', 100 union all
  select '鈴木', 101 union all
  select '木村', 102
  on conflict ( name, num ) do nothing

厳密にはバルクインサートではないのですが、1回の SQL 実行で複数のレコードをインサートし、"conflict"(UNIQUE 制約違反)が発生した場合は "do nothing"(無視)するよう指定しています。その結果、UNIQUE 制約違反のないレコードのみがまとめてインサートされる、という処理が実現できています。

バルクインサートで1度に挿入するレコード数が莫大になるようなケース(SQL 文自体が膨大で複雑になるケース)には向かないかもしれませんが、自分の手元では一応期待どおりに動くことを確認しました。ちょうどこういう処理を行いたいアプリを作っていて調べたのですが、他の人の参考になれば。


【参照】
https://stackoverflow.com/questions/53722405/how-to-insert-bulk-rows-and-ignore-duplicates-in-postgresql-9-3










初めて Heroku Connect を使ったアプリケーションを作ってみました。普段使わない環境や手順もあったことに加え、日本語資料をあまり多く見つけられなかったので、自分の備忘録も兼ねて一連の手順をまとめてみました:
2022022700



【Heroku Connect とは】
Heroku Connect は Heroku のアプリケーションリソースの1つで、SalesForce のデータと Heroku Postgres( PostgreSQL データベース)との双方向同期機能です。これによって SalesForce 上のデータを Postgres データベースのデータとして読み書きできるようになる(Heroku アプリケーションからは PostgreSQL DB に接続することで SalesForce のデータを読み書きできるようになる)というものです。ある意味で閉じられた SalesForce データを、オープンな Heroku アプリケーション環境の一部として取り扱うことができるようになります。

なお Heroku Connect にも Heroku Postgres にも無料枠があり、一定条件内であれば無料で動作確認程度はできるものです。

Heroku Connect について、詳しくはこちらも参照ください:
https://devcenter.heroku.com/articles/heroku-connect


【Heroku Connect の設定】
実際に Heroku アプリケーション上で Heroku Connect を有効に設定し、SQL でデータを取り出す、という手順を行うまでの設定手順を紹介します。

前提条件として、Heroku のアカウントはもちろんですが、SalesForce.com でオブジェクト開発のできるアカウントが必要です。無料の Developer Edition も用意されているので、アカウントを持っていない場合はまずはアカウントを作成しておいてください。Developer Edition はこちらから:
https://developer.salesforce.com/ja


順序としてはもう少し後でもいいと思うのですが、SalesForce.com 側の話になっているこのタイミングで Heroku Connect で読み書きするデータオブジェクトを決めておきます。私自身が SalesForce.com にあまり詳しくないので標準オブジェクトから1つ選択しますが、ここでの対象はカスタムオブジェクトでも構わないはずです。

SalesForce.com (Developer Edition)にログインし、「オブジェクトマネージャ」と書かれた箇所をクリックします:
2022022701


標準で用意されている(と、カスタムオブジェクトを追加した場合はカスタムオブジェクトも含めた)オブジェクトの一覧が表示されます。この中から Heroku Connect で取り出す対象を1つ決めます。私はよく分かっていないこともあって、標準オブジェクトでサンプルデータもはじめからいくつか格納済みの「取引先」オブジェクトを対象にする前提で以下を紹介します。別のオブジェクトを使うこともできると思いますが、適宜読み替えてください:
2022022702


オブジェクトの表示を絞り込む場合は「クイック検索」フィールドに名前を一部入力すると、オブジェクトのラベルでフィルタリングされます。下の例では「取引先」でフィルタリングした結果です。「取引先」オブジェクトは API 参照名が "Account" となっていることがわかります。この名称は後で使うことになるのでメモしておきましょう:
2022022703


SalesForce.com 側の準備はこれだけです。Heroku Connect で同期するオブジェクトデータが決まっていれば Heroku 側の準備にとりかかります。

改めて Heroku にログインし、アプリケーションを1つ作成します。以下の例では "forceobject" というアプリケーションに対して Heroku Connect を設定する想定で紹介しているので、アプリケーション名は自分のアプリケーション名に読み替えてください。またこの時点では Heroku Connect を含めたアドオンは1つも設定していないものとします:
2022022701


このアプリケーションに Heroku Connect をアドオンします。上記画面の "Configure Add-ons" と書かれた箇所をクリックします:
2022022702


アプリケーションのリソース画面に移動します。この "Add-ons" と書かれたフィールドに "Heroku Connect" と入力すると "Heroku Connect" が見つかります。見つかった名前の部分をクリックします:
2022022703


アプリケーションに Heroku Connect を追加する確認ダイアログが表示されます。使用条件と、プランが "Demo Edition - Free"(無料)となっていることを確認して "Submit Order Form" ボタンをクリックします※1:
2022022704


※1 Heroku Connect Demo Edition には以下の制約があるようです:
 ・最大 10,000 行のデータまで同期
 ・同期間隔の最小値は 10 分
 ・ログは7日間保持


アプリケーションに Heroku Connect がアドオンされました:
2022022701


続けて同期を行うデータベースである Heroku Postgres をアドオンします。先程と同様に "Add-ons" フィールドに "Heroku Postgres" と入力して、候補として見つかる "Heroku Postgres" をクリックします:
2022022701


アプリケーションに Heroku Postgres を追加する確認ダイアログが表示されます。使用条件と、プランが "Hobby Dev - Free"(無料)となっていることを確認して "Submit Order Form" ボタンをクリックします:
2022022702


これで Heroku Postgres もアドオンとして追加できました。次にこの2つ(Heroku Connect と Heroku Postgres)を接続するための設定が必要ですが、これにはスキーマと呼ばれる DB の情報が必要になります。スキーマを確認するため、画面上部の "Settings" と書かれたタブをクリックします:
2022022703


"Config Vars" 節の "Reveal Config Vars" ボタンをクリックして環境変数を確認します:
2022022702


すると環境変数 DATABASE_URL が設定されていることがわかります。その値を確認するため、 "DATABASE_URL" と書かれた行の右にある鉛筆ボタンをクリックします:
2022022703


以下のようなダイアログが表示され、"Value" と書かれたフィールドに環境変数 DATABASE_URL に設定された値を確認できます:
2022022704


この値は以下のような形式になっているはずです。この最後の "/" 文字から右にある部分がスキーマです。このスキーマ値を確認した上で Heroku Connect の設定に移ります:
postgres://*****:*******@ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com:5432/(スキーマ)


改めて Heroku Connect の設定を続けるため、"Heroku Connect" と書かれた箇所をクリックします:
2022022701



Heroku Connect の設定画面が表示されます。"Setup Connection" ボタンをクリックします:
2022022702


以下のような画面に切り替わります。"Enter schema name" と書かれたフィールド(デフォルトでは "salesforce" と入力されたフィールド)に先程確認したスキーマ名を入力します。正しく入力できたら画面右上の "Next" ボタンをクリック:
2022022701


ここで SalesForce.com のアカウント認証が必要になります。画面右上の "Authorize" ボタンをクリック:
2022022702


SalesForce.com のログイン画面が表示されるので、ID とパスワードを入力してログインします:
2022022703


正しく認証が行われると Heroku Connection の接続が行われ、以下のような画面になります:
2022022704


Heroku Connect 側の準備としての最後にマッピングを行います。画面上部の "Mapping" タブを選択し、右下の "Create Mapping" ボタンをクリックします:
2022022701


SalesForce 側のオブジェクトの一覧が表示されます。今回は「取引先」を対象に同期したいので、"Account" オブジェクトを選択します:
2022022702


Account オブジェクトのマッピング条件を指定する画面が表示されます。デフォルトでは10分おきに SalesForce からデータベースへの同期のみが有効になっていますが、その条件を変えたい場合はここで指定します。また画面下部には Account オブジェクトの中のどの属性値を同期の対象とするかを指定する表があります:
2022022703


デフォルトでもいくつか指定されていて、そのままでもいいと思います。とりあえず取引先名称となる Name がチェックされていることを確認しておきましょう:
2022022704


画面上部に戻り、最後に "Save" ボタンをクリックして、この条件を保存します:
2022022703


指定された条件が保存され、最初の同期が行われます。少し待つと Status が "OK" となります。これで SalesForce の取引先情報が Heroku Postgres のデータベースに格納されました。Heroku Connect の設定が完了です:
2022022701



【アプリケーションから同期されたデータを参照する】
ここまでの設定ができていれば、アプリケーションから(PostgreSQL を参照して)SalesForce のデータを取り出すことができます。といっても、ここまでの設定が済んでいれば該当の PostgreSQL サーバーに接続して、以下の SQL を実行するだけ(この SQL を実行するプログラムを書くだけ)です:
select * from (スキーマ名).Account

例えば Node.js の Express フレームワークを使ったウェブアプリとして実装するとこんな感じになります:
//. app.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );
PG.defaults.ssl = true;
var database_url = 'DATABASE_URL' in process.env ? process.env.DATABASE_URL : ''; 
var schema = '';
if( database_url.indexOf( '/' ) > -1 ){
  var tmp = database_url.split( '/' );
  schema = tmp[tmp.length-1];
}

var pg = null;
if( database_url ){
  console.log( 'database_url = ' + database_url );
  pg = new PG.Pool({
    connectionString: database_url,
    ssl: { require: true, rejectUnauthorized: false },
    idleTimeoutMillis: ( 3 * 86400 * 1000 )
  });
}


app.get( '/', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  try{
    var conn = await pg.connect();
    var sql = 'select * from ' + schema + '.Account';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( err );
        res.status( 400 );
        res.write( JSON.stringify( err, null, 2 ) );
        res.end();
      }else{
        res.write( JSON.stringify( result, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    console.log( e );
    res.status( 400 );
    res.write( JSON.stringify( e, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }

});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

(サンプルはこちら)
https://github.com/dotnsf/forceobject


上述の環境変数 DATABASE_URL を指定して、以下のように実行します:
$ DATABASE_URL=postgres://*****:*******@ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com:5432/ssssss node app

するとピンク色の部分で DATABASE_URL からスキーマを取り出し、青字の部分で SQL 文を生成して実行する、というものです。実行後にウェブブラウザで http://localhost:8080/ にアクセスすると以下のような SQL 実行結果を得ることができます:
2022022700


実行結果の JSON オブジェクト内の rows キーの配列値として SQL の実行結果が格納されています。各要素に含まれる属性はマッピング時に指定したものになっていて、特に name 属性には取り出したオブジェクト名称(つまり取引先の名称)が格納されていることが確認できます。


これを有効活用するには、まず自分自身がもう少し SalesForce 自体に詳しくなる必要がありそうですが、今は SalesForce 傘下にある Heroku らしいビジネス色の濃い機能が、Heroku の機能にうまく統合されて提供されているようでした。設定段階が比較的面倒な気もしていますが、一度設定できてしまえば後は普通に PostgreSQL を操作する感覚で使えるし、双方向同期を有効にすればウェブアプリ側から SalesForce のデータを活用して作成・更新・削除といった変更処理もできるようになると思います。


 

コンテナ環境が広まっていくと、アプリケーションやサービスが安定運用できるようになる、という話を耳にします。それはそれで事実だと思うのですが、本当にサービスを安定運用するにはアプリケーションが使うバックエンドも安定運用が必要で、そのあたりの話が飛ばされていることが多いと感じることもあります。 今回のブログエントリはそういう話の例として、Node.js からリレーショナルデータベース(今回は PostgreSQL)を使うアプリケーションでどういった点を考慮すべきか、という例を紹介します。


Node.js から PostgreSQL に接続する、というコード自体は node-postgres という npm パッケージを使うことで簡単に実現できます。具体的な方法もググって容易に見つけることができます。

一方、特にクラウドやコンテナ環境においてはマイクロサービス化を踏まえた設計になっていることも珍しくないと思っています。そのようなケースでは「PostgreSQL がメンテナンス状態になる(接続が切れる)」ことを想定する必要があります。アプリケーションとしては「データベース接続が切断される可能性があり、切断されたら再接続する(それも失敗したら再接続を続ける)」という挙動になるような、ちと面倒な実装が求められます:
20211027



そのような具体的に動くサンプルコードを探していたのですが、ピンポイントで見つけることができず、色々試しながら自分で作ってみました。docker で PostgreSQL イメージを起動し、コンテナを止めたり再スタートすることでアプリも再接続することを確認することができるものです:
https://github.com/dotnsf/postgresql_reconnect


なお、以下の内容は PostgreSQL で(一般的なアプリではこれが普通だと思っていますが)コネクションプーリングを使う前提での接続や SQL 実行を想定したコードを紹介します。


【動作確認】
以下 Node.js が導入された PC と、ローカルの docker を使ってアプリケーションの動作確認する場合の手順を紹介します。

まずはソースコード一式を入手してください。上述の Github リポジトリから git clone するか、ダウンロード&展開して postgresql_reconnect/ プロジェクトを手元に用意します。

まずは docker で PostgreSQL を動かします。起動時に DB を作成しますが、特にテーブルやデータを作ることもなく、単に起動するだけ、です:
$ docker run -d --name postgres -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=P@ssw0rd -e POSTGRES_DB=mydb -p 5432:5432 postgres

↑このコマンドはローカルホストに docker エンジンが導入されている前提で、
 ・ユーザー名: admin
 ・パスワード: P@ssw0rd
 ・データベース名: mydb
 ・公開ポート番号: 5432
でオフィシャル PostgreSQL イメージをコンテナとして起動するように指示するコマンドです。各指定オプションを変更して動かすことも可能ですが、後述するサンプルソースコードはこの内容で PostgreSQL インスタンスが生成される前提で記述されているため、ここから変更する場合はこの後に紹介するサンプルコードの内容も変更内容に合わせて適宜編集してから実行してください:
2021102606



まずは切断時の再接続を考慮しないコード: oldapp.js を実行してみます。ちなみに oldapp.js の内容は以下のようになっています(青字部分が PostgreSQL の接続情報赤字部分が接続部分、そしてピンクが SQL 実行部分です):
//. oldapp.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );

//. PostgreSQL
var pg_hostname = 'localhost';
var pg_port = 5432;
var pg_database = 'mydb';
var pg_username = 'admin';
var pg_password = 'P@ssw0rd';

var pg_clinet = null;
var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.connect( function( err, client ){
  if( err ){
    //. 初回起動時に DB が動いていない
    console.log( 'no db on startup', err.code );
  }else{
    console.log( 'connected.' );
    pg_client = client;
  }
});

//. top
app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

//. ping
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var sql = 'select 1';
  var query = { text: sql, values: [] };
  pg_client.query( query, function( err, result ){
    if( err ){
      console.log( { err } );
      res.status( 400 );
      res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
      res.end();
    }else{
      //console.log( { result } );
      res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
      res.end();
    }
  });
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

実際に Node.js で動かす場合、まず初回のみ依存ライブラリをインストールするため(Node.js 導入済みの環境で)以下のコマンドを実行します:
$ npm install

そして以下のコマンドで oldapp.js を起動します:
$ node oldapp

起動すると "server starting on 8080 ..." と表示され、サーバーが 8080 番ポートでリクエスト待ち状態になります。また、この oldapp.js の場合は起動直後に PostgreSQL に接続を試みるので "connected." と表示されます:
$ node oldapp
server starting on 8080 ...
connected.

上述の oldapp.js の内容からもわかるのですが、このアプリケーションは "GET /" と "GET /ping" という2つの REST API を処理します。前者は単に { status: true } という JSON を返すだけのものです。また後者は接続した PostgreSQL に対して "SELECT 1" という SQL を実行し、その実行結果を返すものです(PostgreSQL に接続できていればなんらかの結果が返るものです)。

試しに前者を実行してみます。ウェブブラウザで "http://localhost:8080/" にアクセスし、{ status: true } が表示されることを確認します:
2021102601


また後者も実行してみます。同様にウェブブラウザで "http://localhost:8080/ping" にアクセスし、{ status: true, result: { .... } } という文字列が表示されることを確認します:
2021102602



ここまでは普通に成功するはずです。
ここからが本番です。この状態でアプリケーションを動かしたまま PostgreSQL サーバーを止めてみます。docker コマンドで止める場合は
$ docker stop postgres

を実行します(docker デスクトップを使っている場合は稼働中のコンテナの STOP ボタンを押すと止まります):
2021102603



この状態で改めてウェブブラウザで各ページにアクセスするとどうなるか? 期待している挙動という意味では以下のようになると思っています:
・GET / へのリクエストについては(DB を使わないので){ status: true } を返す
・GET /ping へのリクエストについては(DB が止まっているので)「DB エラー」を返す
・(更に)DB が再稼働したら自動的に再接続して、GET /ping に対して SQL 実行結果を返す


ところが、実はこの時点でサーバーはクラッシュしています。$ node oldapp を実行したターミナルには Exception が表示された上にアプリケーションは終了し、プロンプトが表示されてしまっています:
2021102604


つまりサーバーがクラッシュしています。したがって GET /ping どころか、GET / へのリクエストもエラーとなってしまうし、稼働していないので自動再接続もできません:
2021102605


これでは困ってしまいます。簡単なデモ程度が目的の実装であれば oldapp.js の内容でも(とりあえず動くので)いいと思いますが、ある程度安定した連続稼働が求められる実運用を想定するとちょっと心細い状況と言えます。



では oldapp.js と同じ内容で、DB を止めてもアプリケーションが死ぬこともなく、DB が復活したら自動再接続して再び SQL が実行できるようになるような実装はどのようにすればよいでしょうか? その例が newapp.js です:
//. newapp.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );

//. PostgreSQL
var pg_hostname = 'localhost';
var pg_port = 5432;
var pg_database = 'mydb';
var pg_username = 'admin';
var pg_password = 'P@ssw0rd';

var retry_ms = 5000;  //. retry every 5 sec

var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
console.log( 'connecting...' );
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.on( 'error', function( err ){
  console.log( 'db error on starting', err );
  if( err.code && err.code.startsWith( '5' ) ){
    //. terminated by admin?
    try_reconnect( retry_ms );
  }
});

function try_reconnect( ts ){
  setTimeout( function(){
    console.log( 'reconnecting...' );
    pg = new PG.Pool({
      connectionString: connectionString
    });
    pg.on( 'error', function( err ){
      console.log( 'db error on working', err );
      if( err.code && err.code.startsWith( '5' ) ){
        //. terminated by admin?
        try_reconnect( ts );
      }
    });
  }, ts );
}

//. top
app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

//. ping
app.get( '/ping', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var conn = null;
  try{
    conn = await pg.connect();
    var sql = 'select 1';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( { err } );
        res.status( 400 );
        res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
        res.end();
      }else{
        //console.log( { result } );
        res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false, error: e }, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

青字部分が PostgreSQL の接続情報赤字部分が接続部分、そしてピンクが SQL 実行部分です(青字部分は全く同じです)。違いを紹介する前にまずは挙動を確認してみましょう。PostgreSQL を再び稼働状態に戻します:
2021102606


この状態で "$ node newapp" を実行して newapp.js を起動します:
$ node newapp
server starting on 8080 ...
connecting...

同じようなメッセージが表示されて、リクエスト待ち状態になります。まずは先ほど同様に GET / や GET /ping を実行します(実行結果自体は先ほどと同じです):
2021102601

2021102602


ではここでも同様に PostgreSQL を強制停止してみましょう。先ほどはアプリケーションがクラッシュしてリクエスト待ち状態ではなくなってしまいましたが、今回はプロンプトには戻らず、引き続き待ち受け状態が続くはずです:
2021102601


この状態で改めて GET / や GET /ping にアクセスしてみます。GET / は変わらず { status: true } を返し、GET /ping は(DB にアクセスできないので) { status: false, error: ... } という内容になりますが、ちゃんとレスポンスを返すことができています:
2021102601

2021102602


そして止まっていた PostgreSQL を再度スタートします:
2021102606



するとアプリケーションが自動的に再接続を行い、少ししてから GET /ping を実行すると、再び SQL 実行が成功した時の画面が表示されます:
2021102602


これでデータベースにメンテナンスが入っても自動再接続して稼働する、という実用的な挙動が実現できました。


【コード説明】
改めて2つのソースコードを比較します。といっても青字部分は共通なので比較の対象からははずし、赤字の接続部分と、ピンク字の SQL 実行部分を比較します。

まずは前者の自動再接続しない方。接続部分と SQL 実行部分は以下のようでした:
var pg_clinet = null;
var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.connect( function( err, client ){
  if( err ){
    //. 初回起動時に DB が動いていない
    console.log( 'no db on startup', err.code );
  }else{
    console.log( 'connected.' );
    pg_client = client;
  }
});

  :
  :

//. ping
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var sql = 'select 1';
  var query = { text: sql, values: [] };
  pg_client.query( query, function( err, result ){
    if( err ){
      console.log( { err } );
      res.status( 400 );
      res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
      res.end();
    }else{
      //console.log( { result } );
      res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
      res.end();
    }
  });
});

接続処理では単純に接続文字列を生成してコネクションプーリングを生成し、直後に connect() を実行してクライアントを1つ取り出しています。そしてこのクライアントをこの後の SQL 実行時に使いまわしています。

また SQL 実行処理でも SQL 文字列を定義して↑で取り出したクライアントを使って実行しています。処理そのものはわかりやすいのですが、一方でこれといった例外発生を考慮した内容でもありません(そのため DB が止まってしまうケースが想定できておらず、アプリケーションのクラッシュを引き起こしてしまう内容でした)。


一方の後者、例外発生を考慮して、切断後に自動再接続できるようにした方の接続部分と SQL 実行部分は以下のようでした:
var retry_ms = 5000;  //. retry every 5 sec

var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
console.log( 'connecting...' );
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.on( 'error', function( err ){
  console.log( 'db error on starting', err );
  if( err.code && err.code.startsWith( '5' ) ){
    //. terminated by admin?
    try_reconnect( retry_ms );
  }
});

function try_reconnect( ts ){
  setTimeout( function(){
    console.log( 'reconnecting...' );
    pg = new PG.Pool({
      connectionString: connectionString
    });
    pg.on( 'error', function( err ){
      console.log( 'db error on working', err );
      if( err.code && err.code.startsWith( '5' ) ){
        //. terminated by admin?
        try_reconnect( ts );
      }
    });
  }, ts );
}

  :
  :

//. ping
app.get( '/ping', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var conn = null;
  try{
    conn = await pg.connect();
    var sql = 'select 1';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( { err } );
        res.status( 400 );
        res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
        res.end();
      }else{
        //console.log( { result } );
        res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false, error: e }, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }
});

まず接続処理ではコネクションプーリングを生成するまでは同じですが、ここではそのまま終了します。connect() を実行してクライアントを取り出す、のは実際に SQL を実行する直前の処理に変更しています。またこのコネクションプーリングを管理する変数 pg を使ってエラーハンドリングを行い、DB 切断時に正しくハンドリングできるようにしています(具体的には数秒待ってから再びコネクションプーリングを生成し、新たに生成したコネクションプーリングに対してもエラーハンドリングを行う、という内容です)。

また SQL 実行時には以下のような処理を加えています:
(1)処理全体を try{ .. }catch{ .. }finally{ .. } で括り、どこで切断しても例外処理できるようにする
(2)try{ .. } 内の実際に SQL を実行する直前に pg.connect でクライアントを取り出す
(3)finally{ .. } 内でクライアントをリリースしてコネクションプーリングに戻す

この3つの処理を加えておくことで DB が突然死んでも正しくハンドリングして再接続(再びコネクションプーリングを作る)を試みるようにしています。また結果的に再接続に時間がかかってしまう場合であってもアプリケーションそのものはクラッシュせずに生き続けるので、(DB にアクセスはできないけど)利用者からのリクエストに返答できるようにしています。



・・・というわけで、公開&提供しているのはあくまでサンプルですが、この考え方で PostgreSQL サーバーの切断時でも自動接続して連続稼働ができるようになると思っています。また PostgreSQL 以外のコネクションを使う RDB を利用する際にも応用できる内容です。 利用するインフラプラットフォームによってはこういったメンテナンス時の再接続方法について特殊な機能が用意されていることもあるので、必ずしもこういう方法をとらないといけない、というわけではないのですが、一つのベストプラクティス的な内容だと思っています。

自分が痛い目にあったことに関わる内容でもあるので、困っている方のお役に立てば何より。


このページのトップヘ