Cradle から Cloudant  をアクセスするメモ

ハイレベルなキャッシング CouchDB クライアント の Node.js ライブラリ Cradle を利用して、Cloudant をアクセスする方法の備忘録です。 Cloudant は CouchDB の DB as Service で、独自拡張した Cloudant API ライブラリもありますが、CouchDB のAPIライブラリも、同様に利用できます。

## Cradleの情報源
日本語の情報が殆ど無いマイナーな感じもするけど、そんな事は気にしないで良いものは使いたいですよね。

* npm cradle https://www.npmjs.com/package/cradle
* GitHub https://github.com/flatiron/cradle

## Bluemix の Cloudant への接続
下記のコードで、Bluemix のCloudant へ接続できました。

~~~
var cradle = require("cradle");
var cred = require('./cloudant_credentials.json');
var dbn = "test01";
var options = {
    cache : true,
    raw : false,
    secure : true,
    auth : {
        username : cred.credentials.username,
        password : cred.credentials.password
    }
};

// Cloudant接続                                                                                                      
var db = new cradle.Connection(
    cred.credentials.host,
    cred.credentials.port,
    options
).database(dbn);
~~~
cloudant_credentials.jsonは、Bluemix の Cloudant サービス資格情報をコピペするだけで作成できます。

~~~file:cloudant_credentials.json参考例
{
  "credentials": {
      "username": "********-****-****-****-************-bluemix",
      "password": "****************************************************************",
      "host": "********-****-****-****-************-bluemix.cloudant.com",
      "port": 443,
      "url": "https://********-****-****-****-************-bluemix:*************************************************\
***************@********-****-****-****-************-bluemix.cloudant.com"
  }
}
~~~
もちろん、npm install は必要です。

~~~
$ node --version
v4.4.7
$ npm install cradle
~~~

## データベースの作成
DBが存在しなければ、作成する定番パターン

~~~
// DBが存在しなければ作成                                                                                            
db.exists(function (err, exists) {
    if (err) {
        console.log('error', err);
    } else if (exists) {
        console.log('exist database: ', dbn);
    } else {
        console.log('create database:', dbn);
        db.create(function(err) {
            if (err) {
                console.log("err = ", err);
            } else {
                console.log("created");
            }
        });
    }
});
~~~

## データベースの削除
削除した後に確認するものです。

~~~~
// データベース削除                                                                                                  
db.destroy(function(err) {
    db.exists(function (err, exists) {
        if (err) {
            console.log('error', err);
        } else if (exists) {
            console.log('exist database: ', dbn);
        } else {
            console.log('database does not exists.');
	}
    });
});
~~~

## データの保管
### キーを指定しての保存
キーを指定して取り出す場合などで、有用なパターンです。 このサンプルコードでは、JSON形式のデータをキー yumi で登録します。

~~~
doc = {
    name: '由美',
    address: '東京都江東区',
    age: 25,
    email: 'yumi@sample.co.jp'
}

// データベース保存                                                                                                  
db.save('yumi', doc, function (err, res) {
    if (err) {
	console.log("err = ", err);
    } else {
	console.log("res = ", res);
    }
});
~~~

### キー無しで登録するパターン
IoT機器からのデータをロギングするなど、時系列に発生するデータを登録する目的に適していると思います。

~~~
doc = {
    name: 'ゴルゴ斎藤',
    address: '東京都江東区',
    age: 43,
    email: 'gsaito@sample.co.jp'
}

// データベース保存                                                                                                  
db.save(doc, function (err, res) {
    if (err) {
	console.log("err = ", err);
    } else {
	console.log("res = ", res);
    }
});
~~~

### 一度に登録するパターン キー指定有りと無し
JSONデータの配列に、_id の項目を付与する事で、キー指定ありで、一括で登録できます。また、_id を削除すれば、キー指定無しで、登録できます。

~~~
docs = [
    {
        _id: 'X-FILE-001',
        name: 'ゴルゴ斎藤',
        age: 48,
        occupation: 'スナイパー',
        address: '東京都江東区'
    },
    {
        _id: 'X-FILE-002',
        name: '次元大介',
        age: 32,
        occupation: 'ガンマン',
        address: '東京都港区'
    },
    {
        _id: 'X-FILE-003',
        name: 'スパイク・スピーゲル',
        age: 27,
        occupation: '賞金稼ぎ',
        address: '東京都大田区'
    },
    {
        _id: 'X-FILE-004',
        name: 'サイトー',
        age: 34,
        occupation: 'スナイパー',
        address: '東京都新宿区'
    }
];

// DBへ一度に保存                                                                                                    
db.save(docs, function (err, res) {
    console.log("err = ", err);
    console.log("res = ", res);
});
~~~

## 読み出しパターン
### キーを指定して一件だけ取り出すパターン

~~~
// データ一件取り出し                                                                                                  
db.get('yumi', function (err, doc) {
    if (err) {
        console.log("err = ", err);
    } else {
        console.log("doc = ", doc);
    }
});
~~~
上記コードの実行結果です。 キー 'yumi' で一つのデータを取り出します。

~~~
$ ./r08_cradle_get.js
doc =  Response {
  _id: 'yumi',
  _rev: '9-72e9f33952de62788f3bd99cf607c356',
  name: '由美',
  address: '東京都江東区',
  age: 25,
  email: 'yumi@sample.co.jp' }
~~~

### 全件取り出しパターン
格納されている全データを取り出します。 db.all だけでは、id,rev のヘッダー部分のみになりますから、id (key)を指定して、内容を取得する必要があります。

~~~
// 全データの取得                                                                                                    
db.all(function (err, res) {
    console.log("err = ", err);
    console.log("res = ", res);
    res.rows.forEach(function (row) {
        console.log(row.key);
        db.get(row.key, function(err,doc) {
            console.log("name = ", doc.name);
        });
    });
});
~~~

### 複数のキー指定で、複数のデータを取り出すパターン
キーを指定した分だけ、配列で結果が返ってきます。

~~~
keys = ['X-FILE-001','X-FILE-002','X-FILE-003'];
db.get(keys, function (err, docs) {
    console.log("err = ", err);
    console.log("docs = ", docs);
});
~~~
以下は、上記のコードの実行結果です。

~~~
err =  null
docs =  Response [
  { id: 'X-FILE-001',
    key: 'X-FILE-001',
    value: { rev: '9-2684c79638a30d75ce021aa2ef9a745c' },
    doc: 
     { _id: 'X-FILE-001',
       _rev: '9-2684c79638a30d75ce021aa2ef9a745c',
       name: 'ゴルゴ斎藤',
       age: 48,
       occupation: 'スナイパー',
       address: '東京都江東区' } },
  { id: 'X-FILE-002',
    key: 'X-FILE-002',
    value: { rev: '9-1f30b948bc8f585d129573ffd99e1f62' },
    doc: 
     { _id: 'X-FILE-002',
       _rev: '9-1f30b948bc8f585d129573ffd99e1f62',
       name: '次元大介',
       age: 32,
       occupation: 'ガンマン',
       address: '東京都港区' } },
  { id: 'X-FILE-003',
    key: 'X-FILE-003',
    value: { rev: '9-1eb62e0bf2d0268ba5d7a83ff23bbe35' },
    doc: 
     { _id: 'X-FILE-003',
       _rev: '9-1eb62e0bf2d0268ba5d7a83ff23bbe35',
       name: 'スパイク・スピーゲル',
       age: 27,
       occupation: '賞金稼ぎ',
       address: '東京都大田区' } } ]
~~~

## キー指定のアップデート
id を指定して save する事で、UPDATE になり、revを指定する必要はない。

~~~
// 更新 idとrev の前にアンダースコアは無い                                                                   
db.get(res.id, function (err, doc) {
    doc.age = doc.age + 1
    // id のみで更新可能 _id への変換は cradleの中で実施                                                     
    db.save(res.id, doc, function (err, res) {
        console.log("err = ", err);
        console.log("update = ", res);
    });
});
~~~

## 削除
### 一件削除
キーを指定して削除します。

~~~
db.remove('yumi', function (err, doc) {
    if (err) {
        console.log("err = ", err);
    } else {
        console.log("doc = ", doc);
    }
});
~~~

### 複数キー指定の一括削除 （失敗)
下記の様に削除したいキーを配列で渡して、一括削除はできませんでした。

~~~
keys = ['X-FILE-001','X-FILE-002','X-FILE-003'];

db.remove(keys, function (err, docs) {
    console.log("err = ", err);
    console.log("docs = ", docs);
});
~~~

実行結果では、_revが無いというエラーが返ってきます。
~~~
err =  [Error: No _rev found for X-FILE-001,X-FILE-002,X-FILE-003]
docs =  undefined
~~~

## レビジョン数の指定と設定取得
### レビジョン数の設定（失敗）
Cloudant では、以下のメソッドが提供されていない様です。

~~~
db.maxRevisions(1, function(err, res) {
    if (err) {
        console.log('error ', err);
    } else {
        console.log('res ', res);
    }
});
~~~
実行結果はエラーでした。

~~~
db.maxRevisions(1, function(err, res) {
   ^

TypeError: db.maxRevisions is not a function
    at Object.<anonymous> (/home/tkr/bluemix-cloudant-coding-patterns/r15_cradle_set_revisions_limit.js:31:4)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3
~~~


### レビジョン数の設定取得（失敗)
Cloudant では、以下のメソッドが提供されていない様です。

~~~
db.maxRevisions(function(err, limit) {
    if (err) {
        console.log('error', err);
    } else {
        console.log('Revisions limit is: '+limit);
    }
});
~~~
実行結果です。

~~~
db.maxRevisions(function(err, limit) {
   ^

TypeError: db.maxRevisions is not a function
    at Object.<anonymous> (/home/tkr/bluemix-cloudant-coding-patterns/r05_cradle_get_revisions_limit.js:31:4)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3
~~~


## ビュー
### ビューの保存 （検索条件の保存）
検索（フィルター）条件を予め保存しておき、タグを指定する事で結果を得られます。 この条件を保存する方法です。

~~~
// デザインドキュメント
ddoc = {
    all: {
        map: function(doc) {
            if (doc.name) emit(doc.name,doc);
	}
    },
    sniper: {
	map: function(doc) {
            if (doc.name && doc.occupation == 'スナイパー') {
		emit(null,doc)
            }
	}
    }
};

// 保存
db.save('_design/person', ddoc, function(err, res) {
    console.log("err = ", err);
    console.log("res = ", res);
});
~~~

### ビューを使った結果の取得
#### ALL指定
デザインドキュメントの名前の後に、ダグを付与してコールします。 次のコードは全件取得できます。

~~~
var view_db = 'person/all'
db.view(view_db, function (err, res) {
    console.log("err = ", err);
    res.forEach(function (row) {
	console.log(row.name);
    });
});
~~~
実行結果

~~~
err =  null
ゴルゴ斎藤
サイトー
スパイク・スピーゲル
次元大介
~~~

#### 特定条件 
このサンプルは、doc.occupation == 'スナイパー' に一致するものをリストします。

~~~
var view_db = 'person/sniper'
db.view(view_db, function (err, res) {
    console.log("err = ", err);
    res.forEach(function (row) {
	console.log(row.name);
    });
});
~~~
実行結果

~~~
err =  null
ゴルゴ斎藤
サイトー
~~~

## テンポラリ・ビューの利用（失敗）
Cloudantでは、この機能が停止されていました。調べた処によると、テンポラリ・ビューは計算コストが高いため、本番環境には適用するべきではないとの記述がありました。 参照先 O'REILLY Nodeクックブック P118

~~~
db.temporaryView({
    map: function (doc) {
        if (doc.name && doc.age > 30) emit(doc.name, doc);
    }
}, function (err, res) {
    if (err) console.log(err);
    console.log(res);
});
~~~
実行結果

~~~
{ [CouchError: forbidden: temp views are disabled on Cloudant]
  name: 'CouchError',
  message: 'forbidden: temp views are disabled on Cloudant',
  error: 'forbidden',
  reason: 'temp views are disabled on Cloudant',
  headers: 
   { 'cache-control': 'must-revalidate',
     'content-length': '69',
     'content-type': 'application/json',
     date: 'Sun, 14 May 2017 11:59:28 GMT',
     server: 'CouchDB/2.0.0 (Erlang OTP/17)',
     'x-couch-request-id': '6b9df37fa7',
     'x-couchdb-body-time': '0',
     'x-content-type-options': 'nosniff',
     'x-cloudant-backend': 'bm-cc-dal-02',
     via: '1.1 lb1.bm-cc-dal-02 (Glum/1.34.0)',
     'strict-transport-security': 'max-age=31536000',
     status: 403 } }
undefined
~~~

以上です。