NoSQLデータベース「CouchDB」と互換性のあるJavaScriptデータベース「PouchDB」を使えば、オフライン時はローカルに、オンライン時はサーバーに保存する処理が簡単に実装できます。Webアプリの開発が捗りそうですね。
近年、クライアントサイドのWebアプリケーションはますます洗練されてきました。ブラウザーでは絶えずJavaScriptのパフォーマンスの改良が提供され、ジオロケーション(geolocation)などのリッチJavaScript APIやピアツーピア通信によってどんどん多くのことができるようになっています。
リッチWebアプリケーションが進歩するにつれ、クライアントサイドの良好なストレージメカニズムも必要になり、最近になってPouchDBなどのJavaScriptデータベースが登場してきました。
PouchDBとは?
PouchDBは、ブラウザーで快適に動作するよう設計された「Apache CouchDB」に触発されてできたオープンソースJavaScriptデータベースです。
JavaScriptデータベースとは?
簡単に説明すると、JavaScriptデータベースとは、データを入力、取得し、検索する方法(またはAPI)を提供するJavaScriptオブジェクトのことです。実際、もっともシンプルな形のJavaScriptデータベースは「plain old JavaScript object(昔からある普通のJavaScriptオブジェクト)」です。Meteorに精通している人なら、MongoDB APIを模倣した別のクライアントサイドデータベース「Minimongo」について聞いたことがあるでしょう。
PouchDBはCouchDBのJavaScript版です。CouchDB APIをそっくりに模倣し、しかもブラウザーやNode.jsで動くことを目的としています。
PouchDBはデフォルトで「インメモリ」で動作するだけでなく、ストレージの際バックグラウンドでIndexedDBを使用する点でMinimongoなどのデータベースと異なっています。「IndexedDB」はファイル、ブロブ(blob)を含む大量構造化データのクライアントサイドストレージ向けのローレベルAPIです。つまり、PouchDBではデータがディスク上に格納され、ページの更新後も利用できるのです(とはいえ、1つのブラウザーで保存されたデータは別のブラウザーでは利用できません)。
アダプターの種類によってベースとなるデータストレージ層を変更できます。
CouchDBとの関係
PouchDBはCouchDBのAPIをできるだけ正確に模倣した、CouchDBのJavaScript版です。
CouchDBでは、次のAPI呼び出しですべてのドキュメントをフェッチします。
/db/_all_docs?include_docs=true
PouchDBでは次のようになります。
db.allDocs({include_docs: true})
PouchDBを使うと、アプリケーションがオフライン状態でもデータをローカルに保存でき、その後アプリケーションがオンライン状態に戻ったときにCouchDBとデータを同期できます。
では、アプリケーションでのPouchDBの使い方を説明していきます。
インストール
PouchDBを使い始めるにあたり、PouchDBクライアントライブラリーのインクルードが必要です。スタンドアロンでビルドでき、その場合PouchDBコンストラクターがwindowオブジェクト上でグローバルに利用可能になります。
<script src="https://cdn.jsdelivr.net/pouchdb/5.4.5/pouchdb.min.js"></script>
あるいは「Node.js/browserify/webpack」環境を使っているならnpmでPouchDBをインストールできます。
$ npm install pouchdb --save
そしてJavaScriptに次のように記述します。
var PouchDB = require('pouchdb');
(興味深いことに、npm isntall pouchdbでも動きます!)
PouchDBの操作
データベースの作成
PouchDBデータベースの作成は、PouchDBコンストラクターを呼び出すだけの簡単なものです。「Movies」という名前のデータベースを作成します。
var movies = new PouchDB('Movies');
上記を実行したのち、Promiseを返すinfoメソッドを使ってデータベースについての基本的な情報を確認できます。
movies
.info()
.then(function (info) {
console.log(info);
})
上のコードの出力は次のようになります。
{"doc_count":0,"update_seq":0,"idb_attachment_format":"binary","db_name":"Movies","auto_compaction":false,"adapter":"idb"}
adapterフィールドはIndexedDBをベースとして使っていることを示しています。
ドキュメントの操作
PouchDBはNoSQLでドキュメントベースのデータベースなので、厳格なスキーマはなく、JSONドキュメントを直接挿入できます。ドキュメントの挿入、更新、検索、削除などの方法を説明します。
■ドキュメントの作成
putメソッドを使って新規ドキュメントを作成できます。
// returns a promise
db.put(doc, [docId], [docRev], [options])
[ ]カッコ内のパラメーターはオプションです。各ドキュメントには関連の_idフィールドがあり、ドキュメント固有の識別子の役割を担っています。
次のコードを実行して、先ほど作成したMoviesデータベース内に新規ドキュメントを作成します。
movies
.put({
_id: 'tdkr',
title: 'The Dark Knight Rises',
director: 'Christopher Nolan'
}).then(function (response) {
console.log("Success", response)
}).then(function (err) {
console.log("Error", err)
})
成功の場合、レスポンスは次のようになります。
Success {ok: true, id: "tdkr", rev: "3-f8afdea539618c3e8dceb20ba1659d2b"}
ここでmovies.info()を呼び出すと、ドキュメントが実際に挿入されたことを示す別のデータとともに{doc_count:1}が与えられます。
レスポンス内のrevフィールドは、ドキュメントのリビジョンを示します。各ドキュメントには_revという名前のフィールドが含まれます。ドキュメントが更新されるたびに、ドキュメントの_revフィールドが変更されます。各リビジョンは過去のリビジョンを提示します。PouchDBでは各ドキュメントの履歴が保持されます(Gitによく似ています)。
■ドキュメントの読み取り
PouchDBはドキュメントをIDで検索できるgetAPIメソッドを提供しています。次のように実行します。
movies
.get('tdkr')
.then(function(doc) {
console.log(doc)
})
.catch(function (err) {
console.log(err)
})
レスポンスは次のようになります。
{title: "The Dark Knight Rises", director: "Christopher Nolan", _id: "tdkr", _rev: "3-f8afdea539618c3e8dceb20ba1659d2b"}
■ドキュメントの更新
たとえばドキュメントに「year」フィールドを追加する場合、次のコードを実行して、先ほど作成したドキュメントを更新します。
movies
.get('tdkr')
.then(function(doc) {
doc.year = "2012" // new field
console.log(doc._rev) // doc has a '_rev' field
return db.put(doc) // put updated doc, will create new revision
}).then(function (res) {
console.log(res)
})
ドキュメントを更新する場合、_revフィールドを設定している必要があります。
先ほどと同様、コンソールで次のようなドキュメントの新しいリビジョンを示す出力を確認できます。
{ok: true, id: "tdkr", rev: "4-7a34189fb8f2e28fe08b666e699755b8"}
■ドキュメントの削除
PouchDBでドキュメントを削除するには、ドキュメントの_deletedプロパティをtrueに設定するだけでOKです。次のように.remove()メソッドを呼び出しても削除できますが、
movies
.get('tdkr')
.then(function(doc) {
return movies.remove(doc) // return the promise
}).then(function(res) {
console.log("Remove operation response", res)
})
次のコードでも同じ結果が得られます。
movies
.get('tdkr')
.then(function (doc) {
doc._deleted = true
return db.put(doc)
})
.then(...)
データベースの削除
データベースオブジェクト上でdestroy()メソッドを呼び出すことにより、データベースを削除できます。
// returns a promise
movies.destroy()
バルクオペレーション
ここまでは、PouchDBでドキュメントを個別に扱ってきました。しかし、PouchDBはドキュメントのコレクションを扱うAPIも提供しています。PouchDBにはバルクオペレーション用として、一括書き込み(bulk write)用のbulkDocs()、一括読み込み(bulk read)用のallDocs()の2つのメソッドが搭載されています。
bulkDocs()メソッドは、データベースに挿入するドキュメントを列挙するだけのとてもシンプルなものです。
複数のドキュメントの挿入
// Returns a promise
movies.bulkDocs([
{
_id: 'easy-a',
title: "Easy A",
// other attribues
},
{
_id: 'black-swan',
title: 'Black Swan',
// ...
}
])
レスポンスの例です。
[
{
"ok": true,
"id": "easy-a",
"rev": "1-84abc2a942007bee7cf55007cba56198"
},
{
"ok": true,
"id": "black-swan",
"rev": "1-7b80fc50b6af7a905f368670429a757e"
}
]
複数のドキュメントを挿入する場合、通常は複数のput()リクエストを実行するよりもバルクAPIを使用したほうが良いでしょう。バルクオペレーションは(ローカルのIndexedDB/WebSQLストア用の)単一の処理または(CouchDBのリモートサーバー用の)単一のHTTPリクエストにまとめられるため、個別のオペレーションより高速になることが期待できます。
複数のドキュメントの検索
複数のドキュメントの読み込み用に、PouchDBにはallDocs()メソッドが搭載されています。
// without {include_docs: true}, only document ids are returned
movies
.allDocs({include_docs: true})
.then(function (docs) {
console.log(docs)
})
これは高速でとても便利なメソッドです。PouchDBのドキュメントには次のように書かれています。
allDocs()はPouchDBにおける「縁の下の力持ち」です。ドキュメントを順序どおりに返してくれるだけでなく、順序の入れ替え、_idによるフィルタリング、 「~より大きい」「~未満」などの操作を使った_id上での「slice and dice(さまざまな切り口による分析など)」、ほかにもいろいろ使えます。
デフォルトで、ドキュメントは_idの昇順で返されます。{descending:true}と指定すると順序を逆にできます。
movies
.allDocs({
include_docs: true,
descending: true
})
.then(...)
startkeyとendkeyパラメーターを指定して、ある範囲内のドキュメントも取得できます。たとえば_idが「a」または「b」で始まる動画をすべて取得する場合、次のクエリを実行できます。
movies
.allDocs({
include_docs: true,
startkey: 'a',
endkey: 'c'
})
.then(console.log)
.catch(console.log)
startkeyとendkeyパラメーターはページ分割されたAPIで特に便利です。
変更フィードでリアルタイム処理を実現
各リビジョンが過去のリビジョンを提示することにより、PouchDBが_revフィールドを使ってリビジョンの追跡を保持することについてはすでに書きました。PouchDBとCouchDBでは、このリビジョンの連結を使用してデータベースを複製します。
しかし、この複製アルゴリズムには、次のような質問の答えを与えてデータベースの履歴の確認を可能にするという意味合いもあります。
- 所定の時点以降、データベースにどのような変更があったか?
- 特定のドキュメントにどのような変更があったか?
ここでchanges()APIの出番です。
開始時間以降のすべての変更をフェッチします。
db.changes({
since: 0,
include_docs: true
}).then(function (changes) {
console.log(changes)
}).catch(...)
しかし、WebアプリケーションではUIを必要に応じて変更できるように、通常は最初のページのロード後に発生するデータベースへの変更の確認に対してより関心が高くなります。PouchDBとCouchDBのコンビが、リアルタイムの変更フィードで応えてくれます。
db
.changes({
since: 'now',
live: true,
include_docs: true
})
.on('change', function (change) {
// This is where you can modify UI, based on database change.
// change.id contains the doc id, change.doc contains the doc
if (change.deleted) {
// document was deleted
} else {
// document was added/modified
}
})
.on('error', function (err) {
// handle errors
})
たとえば基本のリストアプリケーションがあるとして、1つのウィンドウでアイテムを追加すると、そのアイテムが別のウィンドウでリアルタイムに表示されます。
デモでこの動作を確認できます。
同期:PouchDBのデータをブラウザー以外でも利用する
大半のアプリでは、ブラウザー内だけでなくバックエンドにもデータを格納する必要があります。PouchDBを使えばデータをローカルに保存し、しかもバックエンドのCouchDBインスタンスと同期して、データを特定のブラウザー内だけでなく任意の場所で利用できるようになります。
これを実現するため、PouchDBはとてもシンプルなAPIを提供しています。考えてみてください、設定済みのリモートCouchDBデータベースがあれば、たった2行のJavaScriptで同期できてしまうのです。
// local database, that lives in the browser's IndexedDB store
var localDB = new PouchDB('mylocaldb')
// remote CouchDB
var remoteDB = new PouchDB('http://localhost:5984/myremotedb')
次のように記述すると、ローカルの変更をリモートDBに複写できます。
localDB
.replicate
.to(remoteDB)
.on('complete', function () {
// local changes replicated to remote
}).on('error', function (err) {
// error while replicating
})
しかし、多数のユーザーが同一のデータベースにアクセスする場合もあるので、リモートDBのほうからブラウザーに変更を同期できるともっと便利です。PouchDBはこれも引き受けてくれます。
次のJavaScript1行で双方向同期が実現できます。
// replicates once
localDB.sync(remoteDB);
リアルタイム同期もできます。
// keeps syncing changes as they occur
localDB.sync(remoteDB, {live: true})
次のステップ
スペースの関係でここでは取り上げませんでしたが、PouchDBにはプラグインとフレームワークアダプターに関する拡大中のエコシステムもあるので、ぜひ確認しておいてください。さらにPouchDBと合わせてChromeの拡張機能「PouchDB Inspector」を使えば、データベース閲覧用の優れたGUIも実現できます。
PouchDBを紹介してきました。データベースの1つとして、PouchDBに対する世間の関心は確実に高まっています。オフラインファーストのリアルタイムアプリケーション構築でPouchDBをどのように使えるか、理解してもらえれば幸いです。
※本記事はSebastian Seitz、Taulant Spahiuが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Getting Started with PouchDB Client-Side JavaScript Database)
[翻訳:新岡祐佳子/編集:Livit]