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

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

タグ:npm

本ブログエントリは IBM Cloud アドベントカレンダー 2021 に参加しています。12/4 ぶんのネタです。


IBM Cloud から提供されているマネージド NoSQL データベースである IBM Cloudant をよく使っています。最大の魅力は無料アカウントでも容量 1GB まで使えるストレージで、かつデータが分散管理されているという安全性です。仕様が曖昧な状態からアジャイルに作り始める際は(テーブル定義などを意識する必要がないこともあって)むしろ NoSQL データベースの方が便利だったりもします。

この IBM Cloudant を Node.js プログラムから使う場合のライブラリとして長く @cloudant/cloudant を使ってきましたが、2021/12/31 を以って End of Support を迎えることになり、現在は deprecated なライブラリ、という扱いになっています。2021年8月の今は「まだ使えるけど、もうすぐ使えなくなるよ」って所でしょうか。可能であれば、これから新たに作り始める時には利用を避けたほうが安全だと思います。

で、その後継ライブラリとして公開されているのが @ibm-cloud/cloudant (IBM Cloudant Node.js SDK)です。IBM Cloudant だけでなく、そのベースとなっている Apache CouchDB に対してももちろん使うことができるものです。現在 @cloudant/cloudant ライブラリを使っているアプリケーションはなるべく今年中にこの新しい @ibm-cloud/cloudant に移植することをおすすめします。

・・・と言うのは簡単ですが、実際この @ibm-cloud/cloudant は @cloudant/cloudant と比べてどのくらい似ていて/同じで、移植はどの程度簡単/難しいのでしょう? というわけで、まずは @ibm-cloud/cloudant を使ってみることにしました。


【サンプルソースコード】
以下で紹介するサンプルアプリケーションのソースコードを github で公開しています:
https://github.com/dotnsf/cloudant-node-sdk_sample


【接続方法の決定】
@ibm-cloud/cloudant を使って Cloudant(CouchDB) にアクセスする場合の接続方法には3通りあります:
(1)IAM
(2)COUCHDB_SESSION
(3)BASIC


(1)の IAM は IBM Cloudant を IAM 接続サポート形式で作成した場合に利用できる方法です。公式ドキュメントでも「この方法が使える場合はこの方法で」と紹介されています。

(2)の COUCHDB_SESSION を使った方法でも接続が可能です。

(3)の BASIC はいわゆる「ベーシック認証」です。この3つの中では「この方法しか使えない場合の選択肢」と紹介されています。CouchDB を自分でインストールして使う場合など、この方法で接続する前提のセットアップをしているとこの方法でしか接続できないことになります。もちろん Cloudant でもベーシック認証をサポートした形式で作成している場合は利用可能です。

(1)、(2)、(3)のいずれの方法でアプリケーションとデータベースを接続するかを決めておく必要があります。正確には @ibm-cloud/cloudant は「環境変数を設定して接続する」のですが、どの方法で接続するかによって、設定が必要になる環境変数が変わる点に注意が必要です。


【Node.js から接続する際に必要な環境変数】
@ibm-cloud/cloudant を使って Cloudant/CouchDB に接続する際に設定が必要な環境変数は以下です(横のカッコ付き数字は、上述の(1)、(2)、(3)のどの方法を使った時に必要な環境変数か、を表しています)。また変数名の頭の "CLOUDANT" 部分は別の値でも構いませんが、同じ文字列に統一して設定する必要があります:

変数意味
CLOUDANT_AUTH_TYPE上述の接続方法。"IAM"(1), "COUCHDB_SESSION"(2), "BASIC"(3) のいずれか。デフォルト値は "IAM"
CLOUDANT_URLデータベースの URL((1)、(2)、(3))
CLOUDANT_APIKEYAPI キーの値((1))
CLOUDANT_USERNAMEユーザー名((2)、(3))
CLOUDANT_PASSWORDパスワード((2)、(3))


環境に合わせてこれらの値を用意して実際に接続してみます。


【Node.js から Cloudant に接続】
(1)IAM 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'IAM'; //(デフォルト値なので設定しなくてもよい)
process.env['CLOUDANT_APIKEY'] = 'xxxxx'; //(API Key の値)
process.env['CLOUDANT_URL'] = 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud'; //(Cloudant のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT' } ); 

このコード実行が成功すると、Cloudant に接続したインスタンスが client という変数に格納され、実際のデータの CRUD 処理が可能になります。

なお上記コードの serviceName 値として 'CLOUDANT' を指定していますが、この部分は変更可能です。ただ変更する場合は環境変数として指定した変数名の頭の CLOUDANT 部分をここで指定する値と同じものに変更してください。

(2)COUCHDB_SESSION 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'COUCHDB_SESSION';
process.env['CLOUDANT_USERNAME'] = 'username'; //(ユーザー名の値)
process.env['CLOUDANT_PASSWORD'] = 'password'; //(パスワードの値)
process.env['CLOUDANT_URL'] = 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud'; //(Cloudant/CouchDB のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT' } ); 

(3)BASIC 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'BASIC';
process.env['CLOUDANT_USERNAME'] = 'username'; //(ユーザー名の値)
process.env['CLOUDANT_PASSWORD'] = 'password'; //(パスワードの値)
process.env['CLOUDANT_URL'] = 'http://xxx.xxx.xxx.xxx:5984'; //(Cloudant/CouchDB のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT', disableSslVerification: true } ); 

なお(CouchDB の場合に多いと想像していますが) SSL 接続が不要の場合は上述のように接続時のパラメータに disableSslVerification: true を追加してください。

参考までに、@cloudant/cloudant の場合は、
var Cloudantlib = require( '@cloudant/cloudant' );
var cloudant = Cloudantlib( { account: "username", password: "password", url: 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud' } );
var db = cloudant.db.use( "dbname" );

といった感じでデータベースまでを取得した上で、
db.get( "id", function( err, result ){
  if( err ){
    console.log( err );
  }else{
    console.log( result );
  }
});

のようにして特定データを取得したりしていましたが、この @ibm-cloud/cloudant ではまだデータベースが特定されていない点にご注意ください。


【接続後の CRUD 処理の例】
では接続後の Cloudant クライアントを使って、IBM Cloudant のデータを読み書きしてみましょう。まず対象データベースと id がわかっている特定データを取得してみます。@ibm-cloud/cloudant では以下のような getDocument() メソッドを使うコードとなります:
client.getDocument( { db: "dbname", docId: "id" } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

client.getDocument() を実行し、そのパラメータ内でデータベースと id を指定する、という方式になります。この点からして @cloudant/cloudant とは異なってますね。

また特定データベースの全データをまとめて取得(@cloudant/cloudant だと db.list() )する場合は以下のような postAllDocs() メソッドを使うコードです:
client.postAllDocs( { db: "dbname", includeDocs: true } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

データを一件追加する場合は以下のような postDocument() メソッドを使うコードになります:
client.postDocument( { db: "dbname", document: { name: "Kimura" } } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

以上、あくまでいくつかのメソッドを紹介しただけですが、全般的に行うオペレーションとメソッド名の関係がわかりやすく整頓されているように感じて、比較的慣れやすいのではないかと感じています。



Node.js で XML を扱うのは難しいので、いったん JSON に変換してから扱うことが多いと思っています。そんな場合に自分はよく xml2json というライブラリを使っていました。

xml2json はその名前の通り、XML を JSON に変換してくれるパーサーを実装したライブラリです。使い方は例えば以下のような感じで、XML 文字列をそのまま引数にして実行すると、結果が JSON 文字列となって戻してくれます(JSON の文字列ではなく JSON オブジェクトとして扱う場合は更に JSON.parse() を実行します)。挙動も速く、便利に使っていました:
  :
var parser = require( 'xml2json' );
  :

  :
var xmlStr = fs.readFileSync( xml_filename, 'utf-8' );   //. ファイル名を指定して XML 文字列を取得
var jsonStr = parser.toJson( xmlStr );                   //. XML 文字列を JSON 文字列に変換
var jsonObj = JSON.parse( jsonStr );                     //. JSON 文字列を JSON オブジェクトに変換
  :

ところが上述のような処理を記述した Node.js のソースコードを Windows 環境下で実行しようとした時に問題が発生しました。実行前のライブラリインストール(npm install)の段階でエラーが発生してしまうのでした。詳細なエラーメッセージ等は後述のリンク先を参照していただきたいのですが、node-expat という xml2json の依存ライブラリを node-gyp でビルドする際に何やらエラーが発生していました。

で、この現象を調べた所、どうやら Windows 環境下では xml2json ライブラリ自体がビルドできないという根本的な問題を抱えているようでした:
npm install xml2json error


※厳密には「ビルドできない」わけではないけど、別途 Python や Visual Studio C++ 2012 などの他にインストールする必要があるツールが多く存在しているようです。


↑リンク先でも回避策として「別の XML -> JSON 変換ライブラリを使う」ことが提案されています。というわけで Windows でも使える同機能のライブラリを探したところ、単純な変換であれば fast-xml-parser が使えそうでした。

fast-xml-parser を使う場合は、上述の内容は以下のようなコードになります:
  :
var parser = require( 'fast-xml-parser' );
  :

  :
var xmlStr = fs.readFileSync( xml_filename, 'utf-8' );   //. ファイル名を指定して XML 文字列を取得
var jsonObj = parser.parse( xmlStr );                    //. XML 文字列を JSON オブジェクトに変換
  :

現実問題として自分個人の開発環境として Windows がベースとなることは今のところ考えにくいのですが、(WSL とかではなく)Windows 環境下で開発しないといけない人との共同作業が発生するようなケースではこういったことも意識しないといけないこともでてくると感じています。


社内ネットワークに Proxy サーバーが設置されている環境は珍しくないと思いますが、そのような環境下でパブリッククラウドを利用して開発作業を行ったり、アプリケーションをデプロイする際に Proxy 環境に応じた設定が必要になります(ついでにいうと、そのような環境下では特定ポート以外を通さない設定になっていることも多いので、単なる Proxy 対応だけでは不十分なこともあります)。

実際にそのような環境でのお客様対応を通じて苦労した得た情報を設定手順含めて共有します。なお以下は IBM Cloud を使ったケースとして紹介していますが、そこそこ広くパブリッククラウド利用時に役立つ情報だと思っています。


【Proxy 環境下で git コマンドを使う】
コマンドプロンプトやターミナルを開いて以下を入力します:
$ git config --global http.proxy http://my.proxyserver.com:8080

$ git config --global https.proxy http://my.proxyserver.com:8080


なお http://my.proxyserver.com:8080 部分は Proxy サーバー名およびポート番号です(以下も同様)。


【ssh でなく https で github(gitlab) を使う】
これは Proxy とは直接関係ないのですが、ssh プロトコル通信が閉じられているような環境下でプライベートな github(gitlab) リポジトリを使いたい場合の、つまり https プロトコルでプライベートな github(gitlab) リポジトリを使う場合に必要な設定項目です。 作業内容としては Private Access Token を設定することで https でも認証が可能になり、プライベートリポジトリを利用することができるようになります。以下 github を使う前提での画面で紹介しますが、gitlab でもほぼ同様です。

まず github にログインし、画面右上の "Settings"  を選択します:
2020022801


次に画面左の "Developer Settings" を選択します:
2020022802


Developer Settings のメニューから "Personal access tokens" を選択し、画面右の "Generate new token" ボタンをクリックします:
2020022803


新たに生成するトークンの設定を指定します。まず名前を(myToken など)適当に入力し、scopes を選択します(わからなければとりあえずは全部):
2020022804


そして最後に画面下部にある "Generate token" ボタンをクリック:
2020022805


すると以下のような画面になり、トークン文字列が表示されます。この文字列はこの一回しか表示されません(一度異なるページを表示した後に再度表示する方法は用意されていません)。別ファイルにコピーするなどしてこの値を再度入力できるようにしてください:
2020022806

ここまで完了していれば、以下のコマンドで https プロトコルだけで github から git clone ができます:
$ git clone https://github.com/aabbcc/xxyyzz.git
 Username: (GitHub のユーザー名)
 Password: (取得したトークン文字列)

なお gitlab の場合は以下のようになります:
$ git clone https://gitlab.com/aabbcc/xxyyzz.git
 Username: oauth2
 Password: (取得したトークン文字列)

【Proxy 環境下で npm コマンドを使う】
サーバーサイド JavaScript である Node.js を使ってアプリケーションを開発する場合、ほぼ npm コマンドを併用することになると思っています。この npm コマンドを Proxy 環境下で使う場合にも設定が必要です。
$ npm -g config set proxy "http://my.proxyserver.com:8080/"
$ npm -g config set https-proxy "http://my.proxyserver.com:8080/"
$ npm -g config set registry "http://registry.npmjs.org/"

proxy と https-proxy の設定をすれば動くはず、ですが、この2つだけではエラーになることがあるらしいです。その場合は registry も設定してください。


【Proxy 環境下で cf コマンドを使う】
これは IBM Cloud 環境に特化した設定かもしれませんが、PaaS である Cloud Foundry ランタイムにアプリケーションを push(デプロイ)する際に利用する cf コマンド(ibmcloud cf コマンド)も Proxy 環境下ではそのための環境設定をしないと使うことはできません。

※Delivery Pipeline サービスを利用することで、cf コマンドを使わずに Git と連動してデプロイすることは可能です。


具体的には環境変数の設定を行う必要があります。以下は Windows 10 での環境変数設定方法です。

コントロールパネル - システムとセキュリティ - システム - システムの詳細設定 を選択します:
2020022807


「詳細設定」タブの「環境変数」ボタンをクリック:
2020022808


ユーザー環境変数で「新規」ボタンをクリック:
2020022809


新しいユーザー変数として、以下の2つを設定します:
変数名変数値
http_proxyProxy サーバー URL
https_proxyProxy サーバー URL

20200228010


2つの環境変数が新たに追加されていることを確認します:

20200228011


これで cf コマンドを指定した Proxy サーバー経由で実行することができるようになりました。

Node.js には npm という便利なパッケージ管理ツールがあります。これはプロジェクト内に package.json というファイルを用意し、その中にプロジェクトで利用したいオープンソースライブラリを記述しておくだけで、後は
$ npm install
と実行するだけでそれらのオープンソースライブラリをまとめてインストールしてくれる、という機能があるのです。しかもその記述されたライブラリだけでなく、それらのライブラリが必要とするライブラリもまとめて調べ、まだインストールされていないものがあればそれだけを(2重にインストールしたりせずに)選別してインストールしてくれる、というものです。要するに深く考えずに npm install すれば、そのプロジェクトの実行に必要なライブラリがまとめてインストールすることができるようになります。

ただ実行環境を揃えるだけならこのように簡単なのですが、どのようなライブラリがインストールされることになり、そしてそれぞれのライブラリはどのような種類のオープンソースライセンスのもとで配布されているのか、を改めて調べようとするとちと面倒なことになります。まず上述のように導入されるライブラリは package.json に記述されたものだけではなく、それらが必要とするライブラリや、更にそれらが必要とするライブラリ・・・と、再帰的に含まれるので、小さなプロジェクトでも実際に調べる対象となるライブラリの数は膨大になってしまいます。試しに先程ウェブフレームワークとして有名な express(v4.17.1) だけを指定して npm install してみたところ、express 自身も含めて 49 個ものライブラリがインストールされました:
2019080401


なので express だけを使うプロジェクトであっても、利用するオープンソースライブラリのライセンスを調べるには、これら 49 個のライブラリそれぞれのライセンスを調べる必要がある、ということになります。

これはさすがに面倒・・・ということで、まとめて調べるツールを作ってみました:
https://github.com/dotnsf/licenses

2019080402


このツールはローカルファイルシステム内のプロジェクトフォルダ(ディレクトリ)を指定して、そのプロジェクトの中で使われるオープンソースライブラリを調べて一覧表示します。ウェブの UI をもたせることも考えたのですが、ローカルファイルシステムを指定するということは対象プロジェクトのソースコード一式をローカルシステム内に(ダウンロードなり git clone なりで)取得しているということになるので、GUI がなくても使える人が大半だろう、、、と思って付けませんでした。GUI でパイチャートとか付けたい人は改造しちゃってください。なおこのツール自体は MIT ライセンスです。

利用するにはまず上記プロジェクトをダウンロードか git clone します。実質的に licences.js だけを使います。そしてこのファイルの3行目あたりにある target_folder 変数の値を調査対象とするプロジェクトのフォルダとなるよう書き換えてください(例: "../myproject" など)。

そして Node.js で実行します:
$ node licenses

成功すると target_folder で指定したフォルダ内の node_modules/ サブフォルダの中を調べ、更に導入済みライブラリのサブフォルダを調べて各ライブラリ毎に配布ライセンスを推測します。そして全てのライブラリを調べ終わったら、結果を一覧表示します(下は例、赤字はコメント):
$ node licenses
target_folder = '../myproject'  対象フォルダ名
MIT: 73.52% MITライセンスが73.52%
[ 'accepts',  MIT ライセンスと推測されたライブラリ一覧
  'abbrev',
  'append-field',
     :
]
ISC: 21.32% ISCライセンスが21.32%
[ 'are-we-there-yet', ISCライセンスと推測されたライブラリ一覧
  'chownr',
  'console-control-strings',
    :
]
Apache: 4.41% Apacheライセンスが4.41%
[ 'detect-libc', Apacheライセンスと推測されたライブラリ一覧
  'ejs',
  'cfenv',
    :
]
BSD: 0.73% BSDライセンスが0.73%
[ 'esprima' ] BSDライセンスと推測されたライブラリ一覧

これでいちいちフォルダ毎に調べなくても依存ライブラリのライセンスがわかります。たぶん。



Node.js で MySQL データベースを利用する場合、npm の mysql ライブラリが多く使われると思っています:
mysql - npm


このライブラリを使うと、例えば SELECT 文を実行するのであれば、こんな感じに記述することで実装できます:
var Mysql = require( 'mysql' );

//. データベースへ接続
var mysql = Mysql.createConnection({
  host: '192.168.10.10',
  user: 'username',
  password: 'password',
  database: 'mydb'
});
mysql.connect();

//. SELECT 文実行
mysql.query( 'select * from items where price > 1000', function( error, results, fields ){
  if( error ) throw error;
  results.forEach( function( result ){
    var id = result.id;
       :
       :

    //. 終了
    mysql.end();
  });
});

また INSERT 文やプレースホルダーっぽい機能を使うこともできます:
var Mysql = require( 'mysql' );

//. データベースへ接続
var mysql = Mysql.createConnection({
  host: '192.168.10.10',
  user: 'username',
  password: 'password',
  database: 'mydb'
});
mysql.connect();

//. INSERT 文実行
mysql.query( 'insert into items set ?', { id: 1234, name: 'シャンプー', price: 500 }, function( error, result ){
  if( error ) throw error;
       :
       :

    //. 終了
    mysql.end();
  });
});

詳しくは上記公式ページを参照してください。


さて、MySQL では create table でテーブルを定義する際に blob(バイナリラージオブジェクト)型の列を指定することができます。画像ファイルなどバイナリデータをそのまま格納することができる列が定義できます:
> create table items( id int primary key, name varchar(50), price int, img blob );

定義は上記のように指定すればいいのですが、ではこの blob 列に、特に mysql ライブラリを使ってどのように指定すればデータを格納すればよいか、が今回のテーマです。特にウェブ画面から画像ファイルを指定してアップロードするような場合に、その画像ファイルの内容を具体的にはどのようにして blob 列に格納すればよいか、という内容です。

この要件について、少しググると MySQL の LOAD_FILE() 関数を使う方法が見つかります。この場合、具体的には以下のように記述します(目的の画像ファイルが /tmp/aaa.png に存在すると仮定します):
    :

mysql.query( 'insert into items set ?', { id: 1234, name: 'シャンプー', price: 500, img: LOAD_FILE('/tmp/aaa.png') }, function( error, result ){
: :

この方法はローカル MySQL サーバーに対しては有効に利用できます。LOAD_FILE() 関数はサーバー側のファイルシステムに対して実行されます。なので上記命令を実行してデータを格納する MySQL サーバーのファイルシステムに /tmp/aaa/png というファイルが存在していれば正しく動きます(命令を実行するクライアント側のファイルシステムにあっても動きません)。

しかし一般的なウェブシステムではウェブサーバーとデータベースサーバーは分離しています。そのようなケースでは(ウェブサーバーはデータベースクライアントになるので)ユーザーがウェブでアップロードしたファイルはウェブサーバーに一時格納されるだけで、データベースサーバーへは送られません。そこでウェブサーバー上で LOAD_FILE を実行してもデータは格納できないことになります。

では改めて、 LOAD_FILE() を使わずに blob データをどのように MySQL に格納するか、その方法がこちらです:
var fs = require( 'fs' );
    :
var img_content = fs.readFileSync( '/tmp/aaa.png' );
mysql.query( 'insert into items set ?', { id: 1234, name: 'シャンプー', price: 500, img: img_content }, function( error, result ){
    :
    :

ファイルシステムライブラリである fs をロードし、readFileSync 関数でローカルの(ウェブサーバー上の)バイナリファイルを読み込み、その結果をプレースホルダーに指定するだけです。

ちなみに取り出すときはこんな感じ:
    :
mysql.query( 'select * from items where id = 1234', function( error, results, fields ){
  if( error ) throw error;
  var img_content = results[0].img;
    :
    :

パフォーマンス等の観点からバイナリラージオブジェクトを MySQL などのデータベースに格納するべきか?という問題はあると思いますが、S3 ストレージなどの外部に格納する場合と比べて「データベースのバックアップ/リストアでオブジェクトごとバックアップ/リストアされる」というメリットはあります。用途に応じては使う価値があると思っています。



このページのトップヘ