Node.js
Bluemix
Cloudant

BluemixのCloudantDBでの挿入・削除・更新

Bluemixでnode.jsを使ってアプリを開発している。アプリ作成にDBが必要となり、お手軽に使えそうだなという理由だけで、CloudantDBを使ってみたらハマった。
僕がハマるところは、他人もハマる可能性がある。なのでポイントを書いておこうと思う。


DBへの接続準備

CloudantDBへのコマンドはRestAPIを使う方法が簡単そうなのだが、RestAPIが個人的に(喰わず)嫌いなので「Cloudant」というライブラリを使った。

Bluemixでは、package.jsonに以下のように、cloudantを指定。

package.json
"dependencies": {
    "cloudant": "1.7.x"
},

また、app.jsの冒頭で、データベースへの接続を行う必要がある。
親切にもCloudantライブラリのREADME.mdに親切にBluemixで接続する場合が書かれている。
「Running on Bluemix」で検索するとすぐ探し出せるぞ。
コードがたった3行とは、すごくシンプル!

  • 以下コードの DB_Service_nameには、自分のデータベースサービス名を指定。 例) Cloudant NoSQL DB-xx
  • Database_nameは、データベースサービスの管理画面等で用意したデータベース名を指定。 例)hege_db
app.js
var Cloudant = require('cloudant');
var cloudant = Cloudant({instanceName: "DB_Service_name", vcapServices: JSON.parse(process.env.VCAP_SERVICES)});
var db = cloudant.db.use('database_name');

DBへのドキュメント(レコード)の挿入

CloudantDBでいうドキュメントとは、RDBでいうレコードみたいなものらしい。
初めは「ドキュメントって何者?」と思った。なので章タイトルに「レコード」と書いてみた。これで読者にも分かりやすくなったと思う。
さて、DBに、ドキュメントをインサートするには、insertメソッドを使用する。
insertメソッドには、ドキュメントのデータを指定する。

以下は、

  • 先ほど宣言した、DB「database_name」にドキュメントを一行追加する。
  • ドキュメントのデータは、キー「data1」の値が「abcde」、キー「item1」の値が「fghij」というものとする。
  • 追加後の処理を無名のコールバック関数に仕込んである。
  • 追加後の結果がエラーだったら「DB Access Error!!!」とログに出力。

という例だ。
システムで必須となるキー「_id」の値は指定しなければ自動的に割り当てられる。

app.js
db.insert({ data1: 'abcde' , item1: 'fghij'}, function (er, result) {
    if (er) {
        console.log("DB Access Error!!!");
    } else {
        console.log("Write Success Result: %s", JSON.stringify(result));
    }
});     

ドキュメントの検索

DBを検索するには、findメソッド、または、searchメソッドを利用する。
私はfindメソッドをお勧めする。

findメソッドによるドキュメントの検索

findメソッドは、JSON形式のセレクターを指定する。
セレクターの文法はIBM Cloud資料 -Queryの「Selector Syntax」の章を参照してほしい。

以下の例では、項目dataが「hoge」という条件を満たす全ドキュメントの項目「_id、_rev、data、item」の値を表示するものである。
コードの最初の方でJSON形式のセレクターを生成している。

app.js
//セレクターを作成
//ここでは、dataという項目が「hoge」であるドキュメントの、_id、_rev、data、itamの項目の値を取ってくる。
var selector_text = {};
selector_text["selector"] = {};
selector_text.selector["data"] = {};
selector_text.selector.data["$eq"] = 'hoge';
selector_text["fields"] = ["_id","_rev","data","item"];
db.find(selector_text, function(er, result) {
    if (er) {
        console.log("DB Access Error!!!");
    } else {
        console.log('Number of Docs : %d', result.docs.length);
        for (var i = 0; i < result.docs.length; i++) {
            console.log("_id",result.docs[i]._id);
            console.log("_rev",result.docs[i]._rev);
            console.log("data",result.docs[i].data);
            console.log("item",result.docs[i].item);
        }
    }
});

searchメソッドによるドキュメントの検索

searchメソッドは以下の理由であまりお勧め出来ない。

  • 検索してヒットしたドキュメントの_idを返してくれるので、それをgetメソッドで利用してレコードの各項目の情報を取るた2度の手間がかかる。
  • 該当するドキュメントが25件以上ある場合は、初めにヒットした25件の_idしかわからない1 
  • BluemixでCloudantDBサービスを使う場合、プランによって1秒間に実行できるLookups、Queriesに制限があるが、searchメソッドでQuery1つ、getメソッドでLookupsをヒットした_id分消費する。

それでも参考までにsearchメソッドについて書く。searchメソッドは、デザインドキュメント名、インディックス名、クエリー文字列の順で指定する。
デザインドキュメント名、インディックス名は、DBサービスの管理画面でインデックス作成時に好きな名前を付けることが出来る。

クエリー文字列は、
{q:'[検索キー値]:[検索文字列]'}

という形式で指定する。

以下は、

  • キー「data1」の値がabcdから始まるドキュメントが検索される。
  • 検索結果は、resultという変数内にrowsという配列が作られ、その配列のキー「id」にドキュメントの_idが格納される。
  • 条件を満たすレコードの最初の_idを一個だけログ出力する。

という例だ。

app.js
db.search('design_document_name', 'index_name', {q:'data1:abcd*'}, function(er, result) {
    if (er) {
        console.log("DB Access Error!!!");
    } else {
        console.log('Showing %d out of a total %d 1st._id :%s', result.rows.length, result.total_rows, result.rows[0].id);
    }
});

ドキュメントの内容取得

検索ではドキュメントの_idのみが分かるので、他の値を参照するにはgetメソッドを使う。
getメソッドは、_idを指定する。

以下は、

  • searchメソッドで求めた条件に合致する全てのドキュメントの_idを求め、
  • 次にgetメソッドでその_idのドキュメントの中のitem1というキーの値を取得してログ出力する。(複数ドキュメントがあれば複数出力する。)

という例だ。

app.js
db.search('design_document_name', 'index_name', {q:'data1:abcd*'}, function(er, result) {
    if (er) {
        console.log("DB Access Error!!!");
    } else {
        console.log('Showing %d out of a total %d books by Dickens', result.rows.length, result.total_rows);
        if (result.rows.length === 0) {
//              ドキュメントが一件もないときの処理
        } else {
            for (var i = 0; i < result.rows.length; i++) {
                db.get(result.rows[i].id, function(er2, data) {
                    if (er2) {
                        console.log("DB Access Error!!!");  
                    } else {
                        console.log(data.item1);
                    }
                });
            }
        }   
    }
});

ドキュメントの更新

DBを更新する場合も、insertメソッドを使う。
insertメソッドは、書込みたいデータを指定するのだが、
更新したいときは更新したいドキュメントの_idと、_revを書込みたいデータに含める。当初、これに気づかずドキュメントの更新が出来なかった。とても苦労した覚えがある。

以下は、

  • 条件に合致したドキュメントのキーitem1の値を12345に書き換える。
  • 他の例と違ってデータにJSONフォーマットの文字列でなくオブジェクト変数を指定している。この方がデータを組み立てる時に楽なのだ。

という例だ。

app.js
db.search('design_document_name', 'index_name', {q:'data1:abcd*'}, function(er, result) {
    if (er) {
        console.log("DB Access Error!!!");
    } else {
        console.log('Showing %d out of a total %d books by Dickens', result.rows.length, result.total_rows);
        if (result.rows.length === 0) {
//              ドキュメントが一件もないときの処理
        } else {
            for (var i = 0; i < result.rows.length; i++) {
                db.get(result.rows[i].id, function(er2, data) {
                    if (er2) {
                        console.log("DB Access Error!!!");  
                    } else {
                        //データ組み立て
                        var write_doc = {};
                        //ここがミソ
                        write_doc._rev = data._rev;
                        write_doc._id = data._id;
                        //ここまでがミソ
                        //data1は、書き換えないという例。(読みだした値をそのまま書き込む。)
                        write_doc.data1 = data.data1;
                        write_doc.item1 = "12345";
                        //書き込み
                        db.insert(write_doc, function (er3, result2) {
                            if (er3) {
                                console.log("DB Access Error!!!");
                            } else {
                                console.log("Write Result: %s", JSON.stringify(result2));
                            }
                        });     
                    }
                });
            }
        }   
    }
});

ドキュメントの削除

ドキュメントの削除は、destroyメソッドを使う。
たった1ドキュメント削除なのにdestroyとは、ちょっとイメージしにくい。
destroyメソッドには、_id と _rev を指定する。
以下は、

  • DBから条件に合致するドキュメントをすべて削除する。

という例である。

app.js
db.search('design_document_name', 'index_name', {q:'data1:abcd*'}, function(er, result) {
    if (er) {
        console.log("DB Access Error!!!");
    } else {
        console.log('Showing %d out of a total %d books by Dickens', result.rows.length, result.total_rows);
        if (result.rows.length === 0) {
//              ドキュメントが一件もないときの処理
        } else {
            for (var i = 0; i < result.rows.length; i++) {
                db.get(result.rows[i].id, function(er2, data) {
                    if (er2) {
                        console.log("DB Access Error!!!");  
                    } else {
                        db.destroy( data._id, data._rev, function(er3, body) {
                            if (er3) {
                                console.log("DB Access Error!!!");
                            } else {
                                console.log("Deleate Record");
                            }
                        }); 
                    }
                });
            }
        }   
    }
});

ドキュメント検索時のエラーについて

BluemixのCloudantDBサービスのフリープランは、getメソッドの実行を毎秒20回までと制限している。
cloudant_limit.png

制限を超えた場合はメソッドがエラーコードを吐く。適切なエラー処理をしていないとプログラムが強制終了したりもする。
また、searchメソッドはヒットするドキュメントが20以上あると、21個目以降の_idを取得する術が不明だ。
よってBluemixのCloudantDBサービスを使う場合は、下図のようにヒットするドキュメントが1つにになるように設計し、1ドキュメントに複数のデータを独自の配列なりで格納するのが良いのかもしれない。

mameo_data_image2.png

参考にしたサイト

https://github.com/cloudant/nodejs-cloudant
http://nikami.org/cloudant-bluemix-node-js-%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%80%E3%83%B3%E3%83%88-%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB/


  1. searchメソッドで26件目以降の_idを知る術が見当たらない。知っている人がいたら教えてほしい。