刹那(せつな)の瞬き

Willkömmen! Ich heiße Setsuna. Haben Sie etwas Zeit für mich?

LinuxのNode.jsでnode-odbcとMS版ODBCドライバからSQLServerに接続する

node-odbc モジュールから SQLServer への接続は FreeTDS の ODBC ドライバとの組み合わせで確認できました。

しかし、同じソースコードを MicrosoftODBC ドライバで実行すると、文字化けしてしまい、接続に失敗します。

プログラミング ガイドラインに書いてあるようにsetlocale(LC_ALL, "")相当で解決するのは承知しているのですが、これを Node.js でどうすれば良いのか解りません。

MDNのIntl.localeあたりを読んで試してみたけど思う結果になりません。

仕方なく、setlocale(LC_ALL, "")相当を処理する Node.js のアドオンを作成したところ、無事に接続できるようになりました。

正規な設定方法は不明ですが、条件は整ったので node-mssql モジュールの場合と同様なコードを試してみます。

1. 環境

2. プロジェクト

(1) 準備

まずは適当なディレクトリを用意します。(今回は ~/work/crud_msodbc)

$ cd ~/work
$ mkdir crud_msodbc
$ cd crud_msodbc

初期化してモジュールをインストールします。

$ npm init -y
$ npm install odbc
(2) データベース

テスト用のデータベースを用意します。(今回は my_test_db)
※既存のデータベースを利用する場合は不要です。

$ sqlcmd -S localhost -U sa -P abcd1234$ -Q "CREATE DATABASE my_test_db;"
(3) ファイル

下記のソースコードをコピーして crud.js というファイルを作成します。
※ソース中の conn_str の値は、試す環境に適した接続文字列を指定してください。

ソースコード: crud.js

const addon = require('./addon/build/Release/addon').setlocale();

const odbc = require("odbc");
const conn_str = 'Driver={ODBC Driver 17 for SQL Server};Server=localhost;UID=sa;PWD=abcd1234$;Database=my_test_db;';

(async () => {
    console.log("#### Start ####");
    try {
        const conn = await odbc.connect(conn_str);

        console.log("-- DROP & CREATE TABLE");
        await conn.query("DROP TABLE IF EXISTS 会員名簿");
        await conn.query("CREATE TABLE 会員名簿 (番号 int, 氏名 nvarchar(40), 誕生日 date)");

        console.log("-- INSERT");
        for (const row of [
            [ 110, "岸本 龍也", "1989-11-06" ],
            [ 210, "荒井 伸次郎", "1974-01-30" ],
            [ 105, "江口 美奈", "1979-06-23" ],
            [ 304, "長田 隆次", "1991-05-25" ],
            [ 307, "中居 雄樹", "1984-02-29" ],
        ]) {
            const stmt_ins = await conn.createStatement();
            await stmt_ins.prepare("INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (?,?,?)");
            await stmt_ins.bind(row);
            const inserted = await stmt_ins.execute();
            console.dir([inserted.statement, inserted.parameters]);
        }
        display(await conn.query("SELECT * FROM 会員名簿"));

        console.log("-- UPDATE");
        const update_id = 307;
        const new_name = "中井 雄樹";
        const stmt_upd = await conn.createStatement();
        await stmt_upd.prepare("UPDATE 会員名簿 SET 氏名=? WHERE 番号=?");
        await stmt_upd.bind([new_name, update_id]);
        const updated = await stmt_upd.execute();
        console.dir([updated.statement, updated.parameters]);
        display(await conn.query("SELECT 番号,氏名 FROM 会員名簿 ORDER BY 番号"));

        console.log("-- DELETE");
        const delete_id = 210;
        const stmt_del = await conn.createStatement();
        await stmt_del.prepare("DELETE FROM 会員名簿 WHERE 番号=?");
        await stmt_del.bind([delete_id]);
        const deleted = await stmt_del.execute();
        console.dir([deleted.statement, deleted.parameters]);
        display(await conn.query("SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 番号"));

        conn.close();
    } catch (e) {
        console.log("#### Catch !! ####")
        console.log(e)
    }
    console.log("#### Finish ####");
})()

function display(rs) {
    // カラム名表示
    const columns = rs.columns;
    const header = [];
    for (const col of columns) {
        header.push(` | ${col.name}`);
    }
    header.push(" |");
    console.log(header.join(""));
    // ロー表示
    let row_count = 0;
    for (const row of rs) {
        const buff = [];
        for (const col of columns) {
            buff.push(` | ${row[col.name]}`);
        }
        buff.push(" |");
        console.log(buff.join(""));
        ++row_count;
    }
    console.log(`結果 ${row_count} 行 (${rs.length})`);
}
(4) Node.js addon 作成

続いて setlocale() に必要な C++ addon を作成します。
現在のディレクトリ配下に作業用ディレクトリ addon を作成します。

$ mkdir addon
$ cd addon

下記のソースコードをコピーしてそれぞれファイルを作成します。

・ファイル: binding.gyp

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "setlocale.cc" ]
    }
  ]
}

・ファイル: setlocale.cc

#include <node.h>
#include <locale>

void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
    std::locale::global(std::locale(""));
}

void Initialize(v8::Local<v8::Object> exports) {
    NODE_SET_METHOD(exports, "setlocale", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

それぞれのファイルを作成したら、確認後npx node-gyp configure buildを実行して addon を作成します。

$ ls
binding.gyp  setlocale.cc
$ npx node-gyp configure build
npx: 97個のパッケージを4.475秒でインストールしました。
gyp info it worked if it ends with ok
gyp info using node-gyp@7.1.2
gyp info using node@14.16.0 | linux | x64
gyp info find Python using Python version 3.8.5 found at "/usr/bin/python3"
gyp info spawn /usr/bin/python3

・・・(ざっくり省略)・・・

gyp info ok 
$ ls
binding.gyp  build  setlocale.cc
$ ls build/Release/
addon.node  obj.target
$ cd ..
$ ls
addon  crud.js  node_modules  package-lock.json  package.json

エラーがなければ、./addon/build/Release/ にaddon.nodeファイルが生成されます。

準備は以上です。

3. 実行結果

$ node crud.js
#### Start ####
-- DROP & CREATE TABLE
-- INSERT
[
  'INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (?,?,?)',
  [ 110, '岸本 龍也', '1989-11-06' ]
]
[
  'INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (?,?,?)',
  [ 210, '荒井 伸次郎', '1974-01-30' ]
]
[
  'INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (?,?,?)',
  [ 105, '江口 美奈', '1979-06-23' ]
]
[
  'INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (?,?,?)',
  [ 304, '長田 隆次', '1991-05-25' ]
]
[
  'INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (?,?,?)',
  [ 307, '中居 雄樹', '1984-02-29' ]
]
 | 番号 | 氏名 | 誕生日 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 210 | 荒井 伸次郎 | 1974-01-30 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 307 | 中居 雄樹 | 1984-02-29 |
結果 5 行 (5)
-- UPDATE
[ 'UPDATE 会員名簿 SET 氏名=? WHERE 番号=?', [ '中井 雄樹', 307 ] ]
 | 番号 | 氏名 |
 | 105 | 江口 美奈 |
 | 110 | 岸本 龍也 |
 | 210 | 荒井 伸次郎 |
 | 304 | 長田 隆次 |
 | 307 | 中井 雄樹 |
結果 5 行 (5)
-- DELETE
[ 'DELETE FROM 会員名簿 WHERE 番号=?', [ 210 ] ]
 | 番号 | 氏名 | 誕生日 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 307 | 中井 雄樹 | 1984-02-29 |
結果 4 行 (4)
#### Finish ####

node-odbc モジュールでは .query() の戻り値に発行した SQL 文とパラメータ値が保持されているので、ついでに表示してます。

4. 補足

MicrosoftODBC ドライバでは、無事にプレースホルダが使えました。
日本語も問題なく通ります。

ちなみに setlocale() の処理を消すと ODBC ドライバのエラーが発生します。

#### Catch !! ####
[Error: [odbc] Error executing the sql statement] {
  odbcErrors: [
    {
      state: '42000',
      code: 102,
      message: "[Microsoft][ODBC Driver 17 for SQL Server][SQL Server]'�' ��k\ri\x07jˇLB�~Y\x02"
    }
  ]
}

今回はアドオンの追加で回避しましたが、正規の方法があるなら知りたいです。