LinuxのNode.jsでnode-odbcとMS版ODBCドライバからSQLServerに接続する
node-odbc モジュールから SQLServer への接続は FreeTDS の ODBC ドライバとの組み合わせで確認できました。
しかし、同じソースコードを Microsoft 版 ODBC ドライバで実行すると、文字化けしてしまい、接続に失敗します。
プログラミング ガイドラインに書いてあるようにsetlocale(LC_ALL, "")
相当で解決するのは承知しているのですが、これを Node.js でどうすれば良いのか解りません。
MDNのIntl.localeあたりを読んで試してみたけど思う結果になりません。
仕方なく、setlocale(LC_ALL, "")
相当を処理する Node.js のアドオンを作成したところ、無事に接続できるようになりました。
正規な設定方法は不明ですが、条件は整ったので node-mssql モジュールの場合と同様なコードを試してみます。
1. 環境
- OS: KDE neon 5.21.3 (Ubuntu 20.04 ベース)
- Node.js v14.16.0 (npm / npx 6.14.11)
- SQL Server 2019 (RTM-CU9) (KB5000642) - 15.0.4102.2 (X64) on Linux
- SQL Server Command Line Tool (w/ Microsoft ODBC Driver) Version 17.7
- unixODBC 2.3.7
- gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
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 の値は、試す環境に適した接続文字列を指定してください。
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. 補足
Microsoft 版 ODBC ドライバでは、無事にプレースホルダが使えました。
日本語も問題なく通ります。
ちなみに 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" } ] }
今回はアドオンの追加で回避しましたが、正規の方法があるなら知りたいです。