PharoからOracle/MySQLに接続するnode.jsのapiサーバーを構築する

## はじめに
いままでSqueak4.4とPharo7でWebアプリサーバーを構築していましたが、DBXTalkのインストールが失敗するためOracleデータベースへの接続にはperlのDBI/DBDライブラリを利用していました。
ただし、perlを呼ぶOSProcessはWindowsでは動かないためLinux版Pharoでしか開発／テストが出来ず、WindowsPCで開発してLinuxサーバーで運用というスタイルが取れないのがこれまでの悩みでした。

今回Pharo8へのマイグレーションを行う際に、データベースへのアクセスをnode.jsに代行させるようにしたらすべて解決出来たので共有します。
ついでにMySQLの接続もサポートしました。

## 概念図
PharoでSeaside3をWebサーバーとして起動します。
node.jsはapiサーバーとして機能します（今回はWindows10PCに立てましたがWindowsでもLinuxでも好きな環境に立てて下さい）
![Metacello new.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80431/b4c7d85e-7e0b-5f9c-2bab-d5332fcc3550.png)

## ソースコード
# node.js
server.jsはサーバーとしてポート3000をリッスンして起動します。
運用環境ではpm2で自動起動するといいと思います。

```javascript:server.js
let restify = require("restify");
let server = restify.createServer();
server.use(restify.plugins.bodyParser());
const oracledb = require("oracledb");
const mysql = require("mysql");
const util = require("util");

let pool;

const init = async () => {
    try {
        await oracledb.createPool({
            user: "admin",
            password: "pass",
            connectString: "oracle.example.com/example",
            poolMax: 4,
            poolMin: 0
        });
        console.log("Connection pool started");

        pool = mysql.createPool({
            host: "localhost",
            user: "admin",
            password: "pass",
            timezone: "jst",
            multipleStatements: true
        });
    } catch (err) {
        console.error("init() error: " + err.message);
    }
}

const execOracle = async (sql, binds, options) => {
    let connection;
    try {
        connection = await oracledb.getConnection();
        const result = await connection.execute(sql, binds, options);
        return result;
    } catch (err) {
        console.error(err);
    } finally {
        if (connection) {
            try {
                await connection.close();
            } catch (err) {
                console.error(err);
            }
        }
    }
}

const execMysql = async (request) => {
    pool.query = util.promisify(pool.query);
    try {
        const result = await pool.query(request);
        return result;
    } catch (err) {
        throw new Error(err);
    }
}

const closePoolAndExit = async () => {
    console.log("\nTerminating");
    try {
        await oracledb.getPool().close(10);
        console.log("Pool closed");
        pool.end();
        process.exit(0);
    } catch (err) {
        console.error(err.message);
        process.exit(1);
    }
}

(async () => {
    try {
        server.post("/oracle", async (req, res, next) => {
            const request = JSON.parse(req.body.request);
            const sql = request.sql;
            const binds = request.binds;
            const options = request.options;
            const result = await execOracle(sql, binds, options);
            res.json(result);
            return next();
        });

        server.post("/mysql", async (req, res, next) => {
            const request = JSON.parse(req.body.request);
            const result = await execMysql8(request);
            res.json(result);
            return next();
        });

        process.once("SIGTERM", closePoolAndExit).once("SIGINT", closePoolAndExit);

        await init();
        server.listen(process.env.PORT || 3000, function() {
            console.log("Server started.");
        });
    } catch (err) {
        console.error(err);
    }
})();
```

# Smalltalkライブラリ
Pharoに以下のライブラリをロードします

```smalltalk:SeasideとNeoJSONのインストール
Metacello new
  baseline:'Seaside3';
  repository: 'github://SeasideSt/Seaside:master/repository';
  load.

Gofer it
  smalltalkhubUser: 'SvenVanCaekenberghe' project: 'Neo';
  configurationOf: 'NeoJSON';
  loadStable.
```

# Pharo
テストで使用したコードは以下の通りです。
実際に使う際にはラッパークラスを作って運用することになると思います。
apiサーバーからはJSONでデータが帰ってくるので、NeoJSONReaderでSmalltalkオブジェクトに変換することで簡易的にORマッパーも兼ねられました。

```smalltalk:Oracle接続テスト
NeoJSONReader fromString: (
   ZnClient new
      url: 'http://localhost:3000/oracle';
      formAt: 'request' put: (NeoJSONWriter toString:(
         Dictionary new
            at: 'sql' put: 'SELECT * FROM hoge WHERE id=:1';
            at: 'options' put: (Dictionary new at:'outFormat' put: 4002; yourself);
            at: 'binds' put: (Array with: 1);
            yourself
      ));
      post
).

```
MySQLでもコネクションプールを使うので接続先のデータベースは都度選択する想定です。

```smalltalk:MySQL接続テスト
NeoJSONReader fromString: (
   ZnClient new
      url: 'http://localhost:3000/mysql';
      formAt: 'request' put: (NeoJSONWriter toString:(
         Dictionary new
            at: 'sql' put: 'use fuga; SELECT * FROM hoge WHERE id=?';
            at: 'timeout' put: 40000;
            at: 'values' put: (Array with: 1);
            yourself
      ));
      post
).
```

## まとめ
これで、WindowsPCで開発してLinuxサーバーで運用するスタイルが確立出来ました。

## 余談
PharoのLibCクラスのresultOfCommand:を使えばOSProcess同様にWindowsのコマンドが実行できるのがわかったので、最初はnode.jsのプログラムをPharoからLibCで呼び出すようにしましたが、Oracleデータの取得に1300ミリ秒（内データベース接続に900ミリ秒）もかかってしまうため実用になりませんでした。
プログラムを毎回起動するとデータベース接続も毎回行われるため、コネクションプールを使う＝サーバー化することで、データの取得が20ミリ秒に短縮出来ました。