はじめに
いままで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でも好きな環境に立てて下さい)
ソースコード
node.js
server.jsはサーバーとしてポート3000をリッスンして起動します。
運用環境ではpm2で自動起動するといいと思います。
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に以下のライブラリをロードします
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マッパーも兼ねられました。
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でもコネクションプールを使うので接続先のデータベースは都度選択する想定です。
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ミリ秒に短縮出来ました。