Clojure によって CouchDB を使用する

ライブラリーをサポートすることで、Clojure を使った CouchDB へのアクセスを魅力的な選択肢にする

この記事では、JVM のための動的言語である Clojure を使用して CouchDB API にアクセスする方法を紹介します。記事に記載する例では、Clutch API ライブラリーと clj-http ライブラリーを並行して使用して、上位レベルの CouchDB API 呼び出しと、下位レベルの REST ベースの呼び出しについて説明します。CouchDB を使いたいと思っている新米 Clojure 開発者、そして CouchDB の基礎となっている REST API に興味を持つ誰にとっても、この記事は参考になるはずです。

Ryan Senior, Senior Engineer, Revelytix

Ryan SeniorRyan Senior は、Revelytix 社のシニア・エンジニアとして、Clojure を使用してセマンティック Web ソフトウェアを開発しています。以前は、製造業や金融業、医療産業など、さまざまな業界で Java 開発者として働いた経験があります。University of Illinois at Urbana-Champaign でコンピューター・サイエンスの修士号を、Western Illinois University でコンピューター・サイエンスの学士号を取得しました。彼は、Strange Loop コア・チームのメンバーでもあります。Twitter では @objcmdo として、ブログスフィアでは Object Commando として投稿しています。



2011年 2月 22日

Apache CouchDB は、オープンソースの Erlang をベースとするドキュメント指向のデータベースです。CouchDB は、ドキュメントがそれぞれに独立していて、(ID とリビジョン番号を除き) 特定のフィールドを必要としないという点で、スキーマレスなデータベースとなっています。このデータベースの操作は、データベースへの問い合わせから、データの挿入、データの変更に至るまで、すべて REST ベースの API によって行います。あまり構造化されていないデータを扱うアプリケーションは尚更のこと、多くのアプリケーションにとって、CouchDB はリレーショナル・データベースに代わる理想的な手段となる可能性があります。この記事では、基本的な CouchDB の操作、ビューを使用した CouchDB へのクエリー、そしてデータベースのレプリケーションを、Clojure を使用して行う場合を取り上げます。サンプル・コードは Clojure で記述されており、CouchDB の REST API を上位レベルで利用する方法と下位レベルで利用する方法が示されています。上位レベルで利用する場合は Clutch API を使用する方法を、下位レベルで利用する場合は基本的なHTTP ライブラリー clj-http を使用する方法を紹介しています。

環境のセットアップ

この記事のサンプル・コードは、CouchDB 1.0.1、Clojure 1.2.0、Clutch 0.2.4、および clj-http 0.1.2 を対象に作成しました。サンプル・コードの依存関係をダウンロードしてセットアップするために使ったのは、Leiningen ビルド・ツールです。これらのサンプル・コードは、Clojure REPL でコーディングするという観点から作成されています。

作業を始める前に、CouchDB がインストールされていることを確認してください (インストール方法については、「参考文献」を参照)。多数のオペレーティング・システムに対応したバイナリーがあらかじめパッケージ化されているので、お使いのオペレーティング・システムに対応するバイナリーも、デフォルトでこのパッケージに組み込まれている可能性があります。コードを実行するための環境をセットアップするには、まず Leiningen をインストールします (ダウンロード・リンクについては、「参考文献」を参照)。次に、lein new couchdb-from-clojure を実行して新規の Leiningen プロジェクトを作成します。リスト 1 のような内容になるように、プロジェクトに Clutch と clj-http を追加してください。

リスト 1. Clojure project.clj を使用する CouchDB
(defproject couchdb-with-clojure "1.0.0-SNAPSHOT"
  :description "CouchDB from Clojure Examples"
  :dependencies [[org.clojure/clojure "1.2.0"]
      [org.clojure/clojure-contrib "1.2.0"]
     [com.ashafa/clutch "0.2.4"]
     [clj-http "0.1.2"]])

続いて、lein deps を実行して必要な JAR ファイルをダウンロードします。このコードは、どの環境でも REPL から実行することができます。REPL は、lein repl を実行して Leiningen から起動することも、任意の IDE から起動することもできます。REPL プロンプトが表示されたら、リスト 2 の REPL セッションに示されている文を入力して、この記事で使用する名前空間をインクルードします。

リスト 2. clj-http、contrib.json、および Clutch のインクルード
user> (require ['com.ashafa.clutch :as 'clutch])
nil
user> (require ['clj-http.client :as 'client])
nil
user> (require ['clojure.contrib.json :as 'json])
nil
user> (def movies-db "http://localhost:5984/movies")
#'user/movies-db

記事の例では、リスト 2 の最後の文で定義している URL を使って CouchDB データベースにアクセスします。ローカルにインストールされた CouchDB のデフォルト URL は http://localhost:5984 です。お使いの CouchDB がこれとは異なるポートを使って構成されている場合は、REPL セッションで必要に応じてポート番号を変更してください。


JSON の操作

CouchDB 内のデータは、他に依存しない JSON (JavaScript Object Notation) ドキュメント形式で構造化されています。この点が、リレーショナル・データベースとは大きく異なります。例えば、映画のデータベースを考えてみてください。映画をリレーショナル・データベースでモデル化するには、タイトルや公開日など、映画特有の情報を保管するテーブル (例えば movie テーブル) を作成するはずです。映画には、俳優、監督、プロデューサーなどがいますが、おそらくこれらの情報を movie テーブルに保管することはしません。代わりに actors テーブルや、より一般的なテーブル (製作スタッフのテーブルなど) を作成して、movie テーブルから actors テーブルの行を参照します。さらに、この単純な構造だけでは足りず、結合テーブルによって多対多の関係をセットアップすることになるでしょう。この場合、特定の映画の俳優を判別するためには、複数の結合が必要になってきます。正規化と呼ばれるこのプロセスは、データを再編成して冗長性を抑えることが目的です。リレーショナル・データベースは、このように編成されたデータを処理するように調整されます。

CouchDB の movies データベースでは、特定の映画に関するすべての情報が単一のドキュメントに含まれることになります。そのため、より正規化された構造とは対照的に、重複が発生します。例えば、ある俳優の名前は、その俳優が出演した映画のドキュメントのそれぞれに現れます。リスト 3 に、CouchDB に保管、またはこのデータベースから出力される場合の映画のドキュメントを記載します。

リスト 3. 映画データに対応する JSON ドキュメントの例
{"movie-title":"Psycho",
 "director":"Alfred Hitchcock",
 "runtime":109,
 "year-released":1960,
 "studio":"Shamley Productions"
 "actors":["Anthony Perkins" "Vera Miles" "John Gavin" "Janet Leigh"]}

JSON フォーマットは、オブジェクト、配列、およびリテラルをそれぞれ区別します。波括弧 {} はオブジェクトを示し、角括弧 [] は配列を示します。リテラルは、"Psycho" などの文字列、そして 1960 などの整数です。このフォーマット設定は、Clojure の永続データ構造とぴったり適合します。Clojure では、波括弧はマップに使用され、角括弧はベクトルに使用されます。さらに、リテラルも同じ構文を持つからです。表 1 に、JSON のデータ型とそれに対応する Clojure のデータ型の例を記載します。

表 1. JSON と Clojure のデータ型の対応
データ型JSON の例Clojure の例説明
数値11整数および実数を表す型
文字列"Example String""Example String"文字列を表す型
ブール値true/falsetrue/falseブール型
配列[1, 2, 3, 4][1 2 3 4]JSON での配列、Clojure でのベクトル
オブジェクト{"key1" : "value1", "key2" : "value2"}{:key1 "value1" :key2 "value2"}JSON でのオブジェクト、Clojure でのマップ

リスト 4 に、JSON ドキュメント (CouchDB に保管されたドキュメント) と、このドキュメントに相当する Clojure でのマップ表現を続けて記載します。

リスト 4. JSON でのオブジェクトと Clojure でのマップの比較
;;JSON object for Psycho
{"_id":"Psycho"
 "Director":"Alfred Hitchcock",
 "runtime":109,
 "year-released":1960,
 "studio":"Shamley Productions"}

;;Clojure map for Psycho
{:_id "Psycho"
 :director "Alfred Hitchcock"
 :runtime 109
 :year-released 1960
 :studio "Shamley Productions"}

Clojure での JSON の操作は、clojure.contrib.json ライブラリーによって簡易化することができます。このライブラリーでは、Clojure のデータ構造を JSON の文字列に変換します。また、その逆の変換も行います。JSON のオブジェクトのマップ・エントリーに含まれるキーは、Clojure のキーワードに変換され、Clojure で操作しやすい表現になります。以降に記載する HTTP ベースの例では、clojure.contrib.json を使用します。Clutch API は、裏でこのライブラリーを使用して、JSON に特有の内容を開発者が扱わなくても済むようにします。


CouchDB ドキュメントの作成

例えば、CouchDB から、映画「サイコ (Psycho)」に関するすべての情報を取得できるようにしたいとします。それには、リスト 3 に記載するようなドキュメントを保管する必要があります。CouchDB は多数のデータベースを保持できるので、最初のステップとなるのはデータベースを作成して、そこにドキュメントを追加することです。リスト 5 のコードは、movies-db という名前で定義した URL に新規データベースを作成し、続いてドキュメントを作成します。

リスト 5. Clutch を使用した CouchDB ドキュメントの作成
user> (clutch/create-database movies-db)
{:ok true...}

user> (clutch/with-db movies-db
         (clutch/create-document {:director "Alfred Hitchcock"
                                  :runtime 109
                                  :year-released 1960
                                  :studio "Shamley Productions"}
                                 "Psycho"))
{:_id "Psycho" ... }

リスト 5 に示されているのは、Clutch が API の背後で CouchDB ドキュメントを作成するために行っている処理の一部です。create-document に渡している最後の引数は、ドキュメントの ID であることに注意してください。

リスト 6 に、上記のコードに相当する clj-http のコードを記載します。

リスト 6. clj-http を使用した CouchDB ドキュメントの作成
user> (client/put movies-db) ;; Create Database
{:status 201 ... :body "{\"ok\":true}\n"}

user> (->> {:director "Alfred Hitchcock"
             :runtime 109
             :year-released 1960
             :studio "Shamley Productions"}
            json/json-str
           (hash-map :body)
           (client/put (str movies-db "/Psycho")))

{:status 201... :body "{\"ok\":true,
\"id\":\"Psycho\",\"rev\":\"1-ba6b110617a1a8920903b648f208a8fac\"}\n"}

CouchDB では、同じデータベースを重複して作成することはできません。リスト 5リスト 6 のサンプル・コードの両方を実行するには、(client/delete movies-db) を使ってデータベースを削除してから、もう一度データベースを作成してください。

リスト 6 のコードは、映画の情報でハッシュマップを作成した後、それを Clojure のハッシュマップから JSON ドキュメントに変換します (json/json-str を使用)。変換されたドキュメントは、clj-http がリクエストの本体として認識する別のハッシュマップ内に組み込まれます。このコードは最後に、ドキュメントを保管するために CouchDB に PUT リクエストを発行します。注意する点として、ドキュメントの PUT リクエストに使用されている URL は movies データベースの URL で、その後に CouchDB ドキュメントの ID (この例では Psycho) が続きます。

作業結果をダブルチェックする

ドキュメントがデータベースに正常に永続化されていることを確認するには、そのドキュメントをプログラムによって CouchDB から取得して調べるという方法があります。リスト 7 に、Clutch を使用してドキュメントを取得し、JSON でのレスポンスから Clojure のマップに変換する方法を示します。

リスト 7. Clutch を使用した CouchDB からのドキュメントの取得
user> (clutch/with-db movies-db
         (clutch/get-document "Psycho"))

{:_id "Psycho",
 :_rev "1-a6b110617a1a8920903b648f208a8fac",
 :director "Alfred Hitchcock",
 :runtime 109,
 :year-released 1960,
 :studio "Shamley Productions"}

リスト 8 に、今度は clj-http を使用した場合のサンプル・コードを記載します。

リスト 8. clj-http を使用した CouchDB からのドキュメントの取得
user> (-> (str movies-db "/Psycho")
           client/get
           :body
           json/read-json)

{:_id "Psycho",
 :_rev "1-a6b110617a1a8920903b648f208a8fac",
 :director "Alfred Hitchcock",
 :runtime 109,
 :year-released 1960,
 :studio "Shamley Productions"}

リスト 8 の clj-http コードは、JSON でのレスポンスから本体の部分を取得して、その本体で json/read-json を呼び出すことによって、JSON でのレスポンスから Clojure のマップへの変換を行っています。

コードの外部で作業結果を確認するのも簡単です。これにはいくつかの方法がありますが、その 1 つは、コードで使用しているのと同じ REST URL をブラウザーに入力するか、cURL または同様のツールで指定するという単純な方法です。具体的には、前に GET で指定した URL、http://localhost:5984/movies/Psycho を入力します。

最も簡単な方法は、CouchDB の Futon アプリケーションを使用することです (「参考文献」を参照)。CouchDB に付属の Futon は、URL http://localhost:5984/_utils にアクセスすると表示することができます (お使いの CouchDB インストールがデフォルト・ポートを使用するように構成されていない場合には、この URL 内のポート番号を正しいポート番号に置き換えてください)。さらに、Futon は、ドキュメントやビューの作成、レプリケーションなどのさまざまな操作にも使用することができます。

次は、CouchDB へのドキュメントの追加について、もう少し掘り下げて説明します。


ドキュメントの作成 ― 詳細

CouchDB ドキュメントの作成」セクションでは、映画「サイコ (Psycho)」のレコードを作成しました。適切なドキュメント ID を選択することの重要性を説明する例として、新しい映画をデータベースに追加する場合について検討します。「サイコ (Psycho)」は 1998年にリメイクされたので、この新しいバージョンをデータベースに追加してみてください (リスト 9 を参照)。

リスト 9. 競合する ID を使用したドキュメントの追加
user> (clutch/with-db movies-db
         (clutch/create-document {:director "Gus Van Sant"
                                  :runtime 105
                                  :year-released 1998
                                  :studio "Universal Pictures"}
                                 "Psycho"))
;;409 Conflict

リスト 9 のコードを実行するとエラーになります。その原因は、データベース内ですでに使用している ID を使おうとしているためです。すべての CouchDB ドキュメントは ID が振られて保管されることから、それぞれのドキュメント ID は一意でなければなりません。けれども、この映画のタイトルは (この例では一意であるかのように思えるかもしれませんが) 一意ではありません。映画のタイトルを ID としていることが、競合の原因となったわけです (この例では、たった 1 つのデータベースでも競合が発生しました。分散環境ともなれば、さらに競合が発生しやすくなるはずです。このようなレプリケーションの問題については、後で詳しく説明します)。したがって、ドキュメント ID の指定方法を変える必要があります。お薦めの方法は、例えば UUID (Universal Unique Identifier) など、一意性が保証された ID を使用することです。Clutch の場合、ID が指定されていなければ、自動的に ID が生成されます。

リスト 10 は、一意の ID が必要であることを考慮してリファクタリングした新しいドキュメントです。

リスト 10. より適切な映画用 ID の選択 (Clutch の例)
user> (clutch/with-db movies-db
         (clutch/create-document {:movie-title "Psycho"
                                  :director "Gus Van Sant"
                                  :runtime 105
                                  :year-released 1998
                                  :studio "Universal Pictures"}))
{:_id "d6993381eb5ede34fded2f018b9f10b0",
 :_rev "1-29ff788958134c2023d9be94a9231528",
 :movie-title "Psycho",
 :director "Gus Van Sant",
 :runtime 105,
 :year-released 1998,
 :studio "Universal Pictures"}

リスト 10 のドキュメントは、元の ID (Psycho) を movie-title に移し、ID キー・ペアを残していません。このドキュメントには 2 つのフィールドが追加されていることに注目してください。1 つは _rev で (後で説明します)、もう 1 つは _id です。後者のフィールドに自動生成される一意の値が、リスト 9 での問題を防ぎます。注意する点として、_id_rev の値は自動生成されるので、皆さんの環境で実行したときの値は、リスト 10 に示されている値とは異なります。

リスト 11 は、上記と同様の clj-http コードです。

リスト 11. より適切な映画用 ID の選択 (clj-http の例)
user> (->> {:movie-title "Psycho"
             :director "Gus Van Sant"
             :runtime 105
             :year-released 1998
             :studio "Universal Pictures"}
            json/json-str
            (hash-map :body)
            (client/put (str movies-db "/" (java.util.UUID/randomUUID)))
            :body
            json/read-json)
{:ok true,
 :id "f043a641-045b-4316-83f5-67c8f9bb99c3",
 :rev "1-29ff788958134c2023d9be94a9231528"}

リスト 11 の大部分は前に記載した clj-http コードと同じですが、ID がどこから提供されるかが異なります。リスト 11 では JVM が生成した UUID が使用されています。リスト 11 のドキュメントを CouchDB に POST 送信した場合も、CouchDB が自動的に UUID を生成し、CouchDB 独自の UUID 生成ストラテジーを使ってドキュメントにその UUID を追加します。CouchDB が生成した UUID は、URL http://localhost:5984/_uuids から取得することもできます。

ドキュメントを正しく作成したことを検証するには、データベースからすべてのドキュメント ID のリストを取得してください。リスト 12 は、ID のリストを Clutch を使って取得する場合のコードです。

リスト 12. Clutch を使用したデータベース内ドキュメント ID の取得
user> (clutch/with-db movies-db
         (->> (clutch/get-all-documents-meta)
              :rows
              (map :id)))
("d6993381eb5ede34fded2f018b9f10b0" "Psycho")

リスト 13 のコードは、clj-http を使用して ID のリストを取得します。

リスト 13. clj-http を使用したデータベース内ドキュメント ID の取得
user> (->> (str movies-db "/_all_docs"
            client/get
            :body
            json/read-json
            :rows
            (map :id))
("d6993381eb5ede34fded2f018b9f10b0" "Psycho")

リスト 12 およびリスト 13 の呼び出しが CouchDB に要求している内容は、movies データベース内のすべてのドキュメント・メタデータを取得して、ID が自動生成されたドキュメントと、Psycho という名前のドキュメントを返すというものです。一貫性を保つために、リスト 14 のコードで Psycho ドキュメントを削除し、同じドキュメントを自動生成された ID を付与して追加し直します。

リスト 14. 古いキーを持つドキュメントの削除と再追加
(clutch/with-db movies-db
    (let [original-psycho (clutch/get-document "Psycho")]
        (clutch/delete-document original-psycho)
        (-> original-psycho
            (assoc :movie-title (:_id original-psycho))
            (dissoc :_id)
            clutch/create-document)))
{:_id "84bbfce1b0e4cf6c9aa2f4196909f39d", :movie-title "Psycho"...}

リスト 14 のコードは、データベースに現在保管されている Psycho ドキュメントを取得し、このドキュメントを CouchDB から削除した後、新しい movie-title キーと自動生成された最新の ID 値を追加し、その ID をマップから削除して、ドキュメントを再作成します。ID を削除しなければならない理由は、ID がマップに含まれていると、Clutch はその ID を使ってドキュメントを作成するためです。


CouchDB ドキュメントの更新

ドキュメントを更新する方法は、1 つの小さな違いを除けば、ドキュメントを挿入する場合とほとんど同じです。ドキュメントを作成すると、そのドキュメントには自動的にリビジョン番号が付与されます。リスト 15 に一例として、新しく作成された映画データの出力を記載します。

リスト 15. リビジョン番号が設定されたドキュメントの例
user> (clutch/with-db movies-db
         (clutch/create-document {:movie-title "Rear Window"
                                 :director "Alfred Hitchcock",
                                 :runtime 112,
                                 :year-released 1955,
                                 :studio "Paramount Pictures"}))

{:_id "1f91c6a2e1af23fa89ca640e889bbdb6",
 :_rev "1-43386b891e9ad538de0d16fcb66aff5e",
 :movie-title "Rear Window"...}

リビジョン番号に該当するのは、リスト 15_rev マップ・エントリーです。このリビジョン番号は、実際にはドキュメントの MD5 ハッシュであり、CouchDB によって自動的に追加されます。そしてドキュメントが変更されるたびに、このハッシュが変更されます。CouchDB ドキュメントを更新するときには、このリビジョン番号が常に必要になります。このリビジョン番号があることで、CouchDB はドキュメントのどのバージョンが更新対象であるかを認識することができます。リスト 16 に例として、映画「裏窓 (Rear Window)」のドキュメントを取得し、変更を加えた後、そのドキュメントを更新して別のタイトルを映画に追加するコードを記載します。

リスト 16. ドキュメントの更新
user> (clutch/with-db movies-db
         (-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
             (clutch/update-document {:alternate-titles ["La ventana indiscreta"]})))

=> {:alternate-titles ["La ventana indiscreta"]
    :_id "1f91c6a2e1af23fa89ca640e889bbdb6",
    :_rev "2-6601a377a55d733c0bd111539801edc8",
    :movie-title "Rear Window"...}

リスト 16 では、UUID を基準にドキュメントの問い合わせをしているので、リスト 12 (またはリスト 13) で皆さん独自の UUID を調べ、それをリスト 16 で使用してください。

リスト 16update-document を呼び出す際には、2 つの引数を渡しています。1 番目の引数は元のドキュメント、2 番目の引数はハッシュマップです。このハッシュマップは、更新済みドキュメントが CouchDB に保管される前の元のドキュメントにマージされます。update-document 関数は、実際には複数のメソッドであり、キーと値のペアをマージし、update-in のようにネストされた構造を更新するなど、標準的な Clojure のマップ操作手法の多くを反映しています。この関数はまた、必要な変更がすでに行われた (ただし、リビジョン番号および ID は変更されていない) 単一のマップを引数として受け入れます。

リスト 16 の手法は、並行性の面からすると少し楽観的です。今度はリスト 17 のコードを見てください。

リスト 17. 競合を伴う更新
user> (clutch/with-db movies-db
        (let [client1-rw (clutch/get-document
                         "1f91c6a2e1af23fa89ca640e889bbdb6")
              client2-rw (clutch/get-document
                          "1f91c6a2e1af23fa89ca640e889bbdb6")]
         (clutch/update-document client1-rw
                                 #(conj % "Fenêtre sur cour")
                                 [:alternate-titles])
         (clutch/update-document client2-rw
                                 #(conj % "Arka pencere")
                                 [:alternate-titles])))
;; 409 Conflict Error

上記ではドキュメントの取得が 2 回行われます。最初の更新は問題なく進みますが、2 番目の更新は 409 エラーで失敗します。この HTTP Conflict エラー・コードは、アプリケーションが呼び出し側に、現状のリソースとの競合によって操作を完了できなかったことを伝えるために使用するコードです。ドキュメントが取得された時点では、ドキュメントのリビジョン ID は正しいため、最初の更新は成功します。けれどもこの更新によって、ドキュメントのバージョンは 2 番目のアップデーターが確認していなかった新しいバージョンになるため、2 番目の更新は失敗してしまいます。CouchDB は、リビジョン番号が古くなっている場合、ドキュメントの更新を許可しません。2 番目のアップデーターが更新を成功させるにはどうすれば良いのかと言うと、残念ながら、それは何を目標としているかによって異なります。このエラーが発生する可能性を減らす方法の 1 つは、常に更新する直前にドキュメントを取得することです。リスト 17 のコードをリスト 18 のように変更すれば、更新が成功します。

リスト 18. 競合しない 2 つのクライアントの更新
(clutch/with-db movies-db
  (-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
      (clutch/update-document  #(conj % "Fenêtre sur cour")
                               [:alternate-titles]))
  (-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
      (clutch/update-document #(conj % "Arka pencere")
                              [:alternate-titles])))
{:movie-title "Rear Window",
 :alternate-titles ["La ventana indiscreta"
                    "Fenêtre sur cour"
                    "Arka pencere"]
 ...}

この方法で当面の問題は解決されるものの、それでも問題が発生する可能性は残っています。必要なのは、このようなシナリオに対処するコードを作成することです。要件次第では、ドキュメントを再取得して、その新しいバージョンと再マージするという簡単なソリューションになることもあります。あるいは、ユーザーにエラーを返すという方法も考えられます (例えば、ユーザーが在庫にない商品を購入しようとした場合など)。

ドキュメントの更新について指摘しておく必要のある最後の点は、CouchDB にはドキュメントを部分的に変更するという概念がありません。ドキュメントが変更されたことだけを認識します。これは、ドキュメントをどのように変更したとしても、変更によって常にドキュメントのハッシュが新しくなるためです (CouchDB はこのハッシュを使用して、リビジョン ID を作成します)。純粋に付加的な変更でも、ドキュメントの部分的な削除でも、さらにはドキュメントの修正でも、すべて同じように扱われてリビジョン番号が新しくなります。これは、データベースのレプリケーションにおいても重要な点です。


CouchDB のビュー

リレーショナル・データベースとは異なり、CouchDB をクエリーするには SQL を使用しません。データを取得する主要な手段としては、ビューと呼ばれる MapReduce スタイルのコードを使用します。ビューを作成するために使用する言語は、さまざまな言語から選ぶことができます (デフォルトの言語は JavaScriptです。以降の例は JavaScript でも機能しますが、具体的な MapReduce コードは異なります)。

この記事では、Clutch に組み込まれている Clojure ビュー・サーバーから、Clojure をビュー言語として使用します。Clojure ビュー・サーバーを使用するには、このビュー・サーバーがお使いの CouchDB にインストールされている必要があります (Clutch Web サイトに提供されているインストール情報へのリンクについては、「参考文献」を参照してください)。ビュー・サーバーは、クライアント・コードにではなく、サーバーに追加されることに注意してください。

まずはビューを作成して、実行する方法を説明するところから始め、その後、ビューの詳細について探ることにします。まず、クエリーの対象となるデータベースに項目を追加するため、ドキュメントをいくつか追加します (リスト 19 を参照)。

リスト 19. Clutch を使用したドキュメントの一括追加
user> (clutch/with-db movies-db
        (clutch/bulk-update
          [{:movie-title "The Godfather"
            :director "Francis Ford Coppola"
            :runtime 175
            :year-released 1972
            :studio "Paramount"}
           {:movie-title "The Godfather II"
            :director "Francis Ford Coppola"
            :runtime 200
            :year-released 1974
            :studio "Paramount"}
           {:movie-title "The Godfather III"
            :director "Francis Ford Coppola"
            :runtime 162
            :year-released 1990
            :studio "Paramount"}]))

リスト 19 のコードでは、CouchDB の bulk-update 機能を使ってドキュメントをまとめて追加しています。この機能は、新しくドキュメントを作成する場合にも、複数の既存のドキュメントを更新する場合にも使用することができます。

リスト 20 のコードでは、すべてのドキュメントを対象にクエリーを行って、データベース内のすべての映画の上映時間を示す一時的なビューを作成します。

リスト 20. 一時的なビューの例
user> (clutch/with-db movies-db
        (clutch/ad-hoc-view
          (clutch/with-clj-view-server
            {:map (fn [doc] (when (and (:movie-title doc)
                                      (:runtime doc))
                             [[(:movie-title doc)
                               (:runtime doc)]]))})))
{:total_rows 6,
 :rows [{:id "d6993381eb5ede34fded2f018b9f10b0",
         :key "Psycho",
         :value 105}
        {:id "84bbfce1b0e4cf6c9aa2f4196909f39d",
         :key "Psycho",
         :value 109}
        ...]}

各映画について、その映画のタイトルが上映時間と併せて返されます。上記では、movie-titleruntime の有無も確認していることに注目してください。その理由は、このコードは新しく作成されたドキュメントを含め、各ドキュメントで実行されるためです。CouchDB は定義されたスキーマを使用しないので、すべてのドキュメントに同じフィールドを含める必要はありません。したがって、ビューには、クエリーの対象とするフィールドがどのドキュメントにも含まれていない場合に備えた対策を取っておくことが賢明です。

リスト 20 の関数は、複数のベクトルが含まれるベクトルを返します。これは、ドキュメントによって、マップ・エントリーを 1 つも生成しない場合もあれば、多数のエントリーを生成する場合もあり、生成される各マップ・エントリーはベクトルとして表現されるためです。内部に含まれているベクトルにある最初の項目はキー (この例の場合は、映画のタイトル (movie-title)) であり、2 番目の項目は値 (この例の場合は、上映時間を表す整数 (runtime)) です。

このビューは、前に作成したドキュメントのようなものを出力します。この例の場合、出力するのはマップではなく、映画の名前と上映時間の値ですが、概念的にはマップと同じです。映画に関する値は、必要に応じてマップにすることもできます。また、出力はドキュメントに似ていますが、ドキュメントと同じ一意性の要件は適用されないことに注意してください。ビューの場合、key (内部に含まれているベクトル内にある最初の項目) として出力するすべてのものは、内部でその key が生成されたドキュメントの ID とペアを構成します。ドキュメントの ID は、リスト 20 のビュー出力を見るとわかります。出力は期待したとおりの内容であり、データベース内のすべての映画の上映時間が示されます。

上記で説明したビューの実行方法には、いくつかの問題があります。1 つ目の問題は、この方法は開発時に使われるように考えられた一時的なものであることです。この方法は実行されるたびに、データベース内の各ドキュメントに再びアクセスして調べます。それは、前回この方法が実行されてからドキュメントが変更されていないとしても同じことです。「各ドキュメントに再びアクセスする」とは、各ドキュメントが関数に渡され、その結果が出力マップに追加されることを意味します。2 つ目の問題は、この Clojure コードを使用したクエリーしか実行できないことです。

この両方の問題は、ビューを永続化することで解決することができます (リスト 21 を参照)。

リスト 21. Clutch を使用した CouchDB ビューの保管
user> (clutch/with-db movies-db
        (clutch/save-view "movies" "runtimes"
          (clutch/with-clj-view-server
            {:map (fn [doc] (when (and (:movie-title doc)
                                      (:runtime doc))
                             [[(:movie-title doc)
                               (:runtime doc)]]))})))
{:_id "_design/movies",
 :language "clojure",
 :views {"runtimes" ...}}

user> (clutch/with-db movies-db
        (clutch/get-view "movies" "runtimes"))
{:total_rows 6,
 :rows [{:id "d6993381eb5ede34fded2f018b9f10b0",
         :key "Psycho",
         :value 105}
        {:id "84bbfce1b0e4cf6c9aa2f4196909f39d",
         :key "Psycho",
         :value 109}
         ...]}

ビューを永続化することで、クエリーを最初に実行したときに、その結果が保管されるようになります (ドキュメントが変更された場合にのみ、結果が更新されます)。さらに、すべてのユーザーが (Clojure、Web ブラウザー、別の言語などを使用して) クエリーを実行できるようになります。リスト 21 のコードは、リスト 20 と同じ結果を返しますが、キャッシングに関しては賢くなっているため、他の言語や Futon、あるいはブラウザーでも再利用することができます。


CouchDB のビュー ― 詳細

ビューを使用すると、手動でデータの問い合わせを行う場合に比べ、明らかにパフォーマンスが向上します。この記事の前のほうでは、ドキュメントのリストを取得して見つけたキー、または (データベースに映画のタイトルでキーが設定されている場合は) あらかじめわかっているキーをドキュメント・キーとして使って、データベースに対してクエリーを実行しただけでした。検索している特定のドキュメントの ID がわかっていれば、クエリーの実行は高速化されます。ドキュメント ID がわからなければ (わからない可能性の方が高いですが)、クエリーには時間がかかります。movies データベースにはドキュメントが少ししか保管されていないため、一時的なビューでも結果を素早く返しますが、数千、数十万ものドキュメントが保管されているデータベースともなると、すべてのドキュメントで map 関数を実行するには時間がかかります。ビューを有効に活用するには、結果を保管することが不可欠です。

リスト 21 では Clutch を使用してビューを保存するために、save-view 関数に 2 つの文字列と Clojure のマップを渡しました。このマップは、map の単一のキーと値のペアで構成され、値には関数が指定されます。ビューのドキュメントを保存する際の細々とした処理は、Clutch が上手く抽象化してくれます。これらのビューは、実際には通常の CouchDB ドキュメントとして保管されますが、特殊な名前が付けられます。

リスト 22 は、clj-http を使用してビューのドキュメントを作成する例です。

リスト 22. clj-http を使用したビューの保管
user> (->> {:language "clojure"
            :views {:runtimes
                     {:map "(fn [doc]
                        (when (and (:movie-title doc)
                                   (:runtime doc))
                          [[(:movie-title doc)
                            (:runtime doc)]]))"}}}
            json/json-str
            (hash-map :body)
            (client/put (str movies-db "/_design/movies/")))
{:status 201
 ...
 :body "{\"ok\":true...}\n"}

user> (-> (str movies-db "/_design/movies/_view/runtimes")
          client/get
          :body
          json/read-json
          :rows)
[{:id "d6993381eb5ede34fded2f018b9f10b0",
  :key "Psycho",
  :value 105}
  ...]

リスト 22 のコードには、いくつか興味深い点があります。まず、これは今まで CouchDB に保管した他のすべてのドキュメントとまったく同じような CouchDB ドキュメントです。何が違うかと言えば、特殊な名前 (この例では _design/movies) で保管されるという点しかありません。ビューが格納される CouchDB ドキュメントは、CouchDB デザイン・ドキュメントと呼ばれます。デザイン・ドキュメントの名前は、_design で始まります。デザイン・ドキュメントには language プロパティー (この例では Clojure) と、そのデザイン・ドキュメントに含まれる特定のビューのマップを格納する views プロパティーがあります。リスト 21 のコードが Clutch を使用して save-view を呼び出すときには、最初の 2 つのパラメーターによって、デザイン・ドキュメントとビューの名前を定義します。マップのこのビュー・セクションは、関連する多数のクエリーを収容するためにあります。runtimes ビューには、Clutch API で定義した元の関数のようなマップが関連付けられます。これで具体的な名前を付けたCouchDB ドキュメントを作成すれば、ビューの作成は完了です。リスト 22 の後半では、特殊な URL を使用してビューの結果を取得しています。

ビューの項目を問い合わせる

これまでの例には、ビューによって返されるすべての結果を取得する方法が示されています。この方法は役には立つものの、皆さんが期待するような方法ではないかもしれません。当初、データベースのキーとして映画のタイトルを選んだ理由を考えてみてください。映画のタイトルが一意に決まるものではないとしても、映画のタイトルで movies データベースに対してクエリーを実行したいと思うのは当然のように思われます。この目的を踏まえ、映画のタイトルでキーを設定した、映画に関する完全なドキュメントを返すビューを作成することもできます。そのためのコードは、皆さんがすでに目にしたコードと同様ですが、今回は数値を 1 つ返す代わりに、完全なドキュメントを返すことになります。リスト 23 に、ビューを作成して映画のタイトルでクエリーを実行するためのコードを記載します。

リスト 23. Clutch を使用した、映画のタイトルによるクエリー
user> (clutch/with-db movies-db
        (clutch/save-view "movies" "by_title"
          (clutch/with-clj-view-server
            {:map (fn [doc] (when (and (:movie-title doc)
                                      (:runtime doc))
                             [[(:movie-title doc)
                               doc]]))})))

user> (clutch/with-db movies-db
        (:rows (clutch/get-view "movies" "by_title" {:key "Psycho"})))
[{:id "d6993381eb5ede34fded2f018b9f10b0",
  :key "Psycho",
  :value {:_id "d6993381eb5ede34fded2f018b9f10b0",
          :movie-title "Psycho",
          :director "Gus Van Sant",
          ...}
  ...}]

特定の映画を問い合わせる場合と、すべての映画を対象にクエリーを実行する場合とでは、唯一異なる点はクエリー・パラメーターだけです。リスト 23 の Clutch コードで使用しているクエリー・パラメーターはマップで、その結果は単なる URL のクエリー・ストリングとして得られます。リスト 24 に記載する clj-http クエリーでも同様です。

リスト 24. clj-http を使用した、映画のタイトルによるクエリー
user> (->> "\"Psycho\""
            java.net.URLEncoder/encode
            (str movies-db "/_design/movies/_view/by_title?key=")
            client/get
            :body
            json/read-json
            :rows)
[{:id "d6993381eb5ede34fded2f018b9f10b0",
  :key "Psycho",
  :value {...}
  ...}]

CouchDB には、昇順/降順のソートや、キーの範囲、絞り込みなど、豊富なクエリー・オプションが揃っています。さらに、キーをリストやマップなどの他の JSON 構造にすることもできます。CouchDB のビューについての詳細は、「参考文献」を参照してください。

reduce 関数

開発者のニーズの大部分には、ビューから map 関数のみを使用してデータを問い合わせれば、おそらく対応することができます。しかし、情報を集計しなければならないこともあります。データの平均や合計を計算したり、その他の集計処理を行ったりする場合には、map 関数だけでは対応しきれません。そのような場合のために CouchDB が提供しているのが、reduce 関数です。リスト 25 に、データベース内の映画のうち、特定の配給会社 (studio) に関連する映画の合計数を示すビューを作成する例を記載します。

リスト 25. reduce 関数を使用したビュー
user> (clutch/with-db movies-db
        (clutch/save-view "movies" "studio"
          (clutch/with-clj-view-server
            {:map (fn [doc] (when (:studio doc)
                             [[(:studio doc) 1]]))
             :reduce (fn [keys vals rereduce]
                       (if rereduce
                         (apply + vals)
                         (count vals)))})))

user>  (clutch/with-db movies-db
        (clutch/get-view "movies" "studio"))
{:rows [{:key nil, :value 6}]}


user> (clutch/with-db movies-db
        (clutch/get-view "movies" "studio" {:key "Paramount"}))
{:rows [{:key nil, :value 3}]}

リスト 25 に記載されている reduce 関数は、以下の 3 つの引数を取ります。

  • 1 番目の keys 引数は、map 関数で作成されたキーのリストです。キーのリストには、リスト 25 で出力される studio だけでなく、配給会社名と ID も含まれることに注意してください。
  • 2 番目の vals 引数は、関数に渡されたキーに対応する値のリストです。この例では、出力される一連の値が含まれます。
  • 3 番目の rereduce 引数は、この reduce 関数が情報の集計処理を行っているのか、それともマップからの結果 (map 関数からの結果) をそのまま処理しているのかどうかに関係します。

reduce 関数を理解するには、CouchDB がこの関数の実行結果を保管する方法について、ある程度の知識が必要です。reduce 関数を呼び出した結果は、B 木に保管されます。つまり、データの保管位置がルートに近ければ近いほど、集計の対象とする範囲が広くなります。studio ビューの最初の呼び出しは 6 を返します。これは、このツリーのルートにあるビューです。この時点でキーを出力すると、データベース内のドキュメントごとに [studio doc-id] のペアも表示されることになります。ツリーのルートから離れると、他の集計結果 (ルートでの集計よりも対象が少ない集計の結果) を出力することができます。リスト 25 のビューの呼び出しの 2 番目では、配給会社が「パラマウント (Paramount)」の映画を要求しています。この呼び出しは、ツリーを (対数時間で) トラバースして、ルート・ノードに最も近いノードでの配給会社の映画の本数を合計します。この構造が目的としているのはパフォーマンスです。データが変更された場合や、値の計算が必要になった場合には、集計を使用できるため、必ずしも計算のすべてを再実行する必要はありません。rereduce パラメーターの背後にあるのも、この構造です。rereduce パラメーターは、計算対象のノードの子が既に計算済みである場合には true になります (この例では、計算対象のノードの子は、単にそれぞれが値として 1 を持つというわけではありません)。


CouchDB のレプリケーション

CouchDB はスケーリングによって、そのインスタンスの数を増やし、クラスター内のさまざまなノードの間でインクリメンタルにレプリケートすることができます。私は、そのベースとなる CouchDB のレプリケーション機能が、分散された CouchDB データベースをスケーリングする以外にも役立っていることに気付きました。CouchDB 内のデータをレプリケートするプロセスは、API の観点からするとワン・ステップのプロセスです。レプリケーションの作業は、ローカル・データベースまたはリモート・データベース、あるいはその組み合わせで行うことができます。CouchDB では、これまで使用してきた REST インターフェースを使用して、データベース・レベルでレプリケートできるようにすることによって、このレプリケーションの作業を容易なものにしています。ここでは、CouchDB をスケーリングするためにレプリケーションを適用する方法ではなく、CouchDB のレプリケーション・サポートに着目し、このサポートをプログラムによって使用する方法に焦点を当てます (CouchDB のスケーリングについての詳細は、「参考文献」を参照してください)。

CouchDB のレプリケーションは、2 つの既存の (ローカルまたはリモート) データベースの間で行われます。レプリケーションを行う前に 2 つのデータベースの間に要求される、系統に関する要件はありません。レプリケーションは Futon アプリケーションを使って行うことも、プログラムによって行うこともできます。例えば、パフォーマンスに問題があり、CouchDB データベースの 2 つ目のインスタンスを追加して需要に対応する必要があるとします。レプリケーションのテストを簡単に行えるようにするには、単に別のローカル・データベースにレプリケートすればよいのです (リスト 26 を参照)。

リスト 26. Clutch によるレプリケーションの例
user> (def moviesb-db "http://localhost:5984/movies-b")
#'user/moviesb-db

user> (clutch/create-database moviesb-db)
{:ok true,...}

user> (clutch/replicate-database "movies" "movies-b")
{:ok true...}

user> (let [movie-ids (clutch/with-db movies-db
                        (->> (clutch/get-all-documents-meta)
                             :rows
                             (map :id)))
           movie-b-ids (clutch/with-db moviesb-db
                         (->> (clutch/get-all-documents-meta)
                              :rows
                              (map :id)))]
         (= movie-ids movie-b-ids))
true

リスト 26 のコードは、movies-b という名前 (両方のデータベースが同じ名前を持つことはできないため) の新しい (空の) データベースを作成し、movies データベースをこの新規データベースにレプリケートします。次に、すべてのドキュメント ID を取得して、これらの ID が同じであることを確認します。ここでは単純にレプリケートしただけなので、ID はもちろん同じです。リスト 27 に、clj-http を使用して上記と同じ内容を行う場合のコードを記載します。

リスト 27. clj-http によるレプリケーションの例
user> (def moviesb-db "http://localhost:5984/movies-b")
#'user/moviesb-db

user> (client/put moviesb-db)
{:ok true,...}

user> (->> {:source "movies" :target "movies-b"}
           json/json-str
           (hash-map :body)
           (client/post "http://localhost:5984/_replicate"))
{:ok true...}

user> (let [movie-ids (->> (str movies-db "/_all_docs")
                           client/get
                           :body
                           json/read-json
                           :rows
                           (map :id))
           movie-b-ids (->> (str moviesb-db "/_all_docs")
                            client/get
                            :body
                            json/read-json
                            :rows
                            (map :id))]
           (= movie-ids movie-b-ids))
true

リスト 27 では、レプリケーション元とレプリケーション先が含まれる JSON ドキュメントを POST 送信しています。その後の処理は、CouchDB が引き継ぎます。この時点で、データの 2 つのコピーを作成したことによる利点と欠点に直面することになります。2 つ目のデータベースを追加したことで、CouchDB はこれまでよりもリクエストへの対応を迅速に行えるようになりますが、データベースが変更された場合はどうなるのでしょうか。一例として、新しい映画をそれぞれのデータベースに追加し、この 2 つのデータベースの間でレプリケートしてみます (リスト 28 を参照)。

リスト 28. 両方のデータベースへの新規ドキュメントの追加
user>  (clutch/with-db movies-db
         (clutch/create-document
           {:movie-title "Vertigo"
            :director "Alfred Hitchcock",
            :runtime 128,
            :year-released 1958,
            :studio "Paramount Pictures"}))
{:_id "728b2293180e0be566cea3f3127b6cf3"...}

user> (clutch/with-db moviesb-db
        (clutch/create-document
          {:movie-title "North by Northwest"
           :director "Alfred Hitchcock",
           :runtime 131,
           :year-released 1959,
           :studio "MGM"}))
{:_id "386d0400e336e54933a47aec656289c4"...}

(clutch/replicate-database "movies" "movies-b")
(clutch/replicate-database "movies-b" "movies")

リスト 28 の最初の 2 つの文を実行すると、データベースの 2 つのコピーは同じでなくなります。movies データベースには「めまい (Vertigo)」のドキュメントがありますが、movies-b データベースにこのドキュメントはありません。また、movies-b データベースには「北北西に進路を取れ (North by Northwest)」のドキュメントがありますが、これは movies データベースにはありません。この 2 つのドキュメントは、異なる ID を持ち (異なる ID が生成されます)、それぞれが異なる映画を表しています。したがって、これらのドキュメントはデータベース内では別個のものなので、レプリケーションは問題なく完了します。ただし、レプリケーションは片方向でしか行われないことに注意してください。movies から movies-b へのレプリケーションを行う場合、movies-b のドキュメントは 1 つもレプリケートされません。そのため、movies-b から movies へのレプリケーションも行う必要があります (「北北西に進路を取れ (North by Northwest)」ドキュメントのコピーを作成するため)。新規のドキュメントが両方のデータベースに存在することを確認するには、リスト 27 のコードを再利用することができます。このコードで、2 つのデータベースのドキュメント・リストを比較してください。

レプリケーションでの競合を解決する

前述のシナリオは、幸運な事例です。扱っているのは新規ドキュメントだけなので、競合はありません。別の幸運な例は、レプリケーション元では更新されていて、レプリケーション先では更新されていないドキュメントのレプリケーションを行う場合です。では、ドキュメントが 2 つの異なるデータベースで変更されてからレプリケートされるとしたら、どうなるでしょうか?このシナリオは、レプリケーションに問題を起こす可能性があります。この問題に対処する最も簡単な方法は、競合する更新が行われないようにデータベースを設計することです。ドキュメント指向のデータベース設計では、ドキュメントをできるだけ自己完結型にすることが最善の策となっています。データベースでも同じように、新しい情報は新しいドキュメントに配置し、ドキュメントの更新は発生しないように設計できないものでしょうか?これは常に可能であるとは限りませんが、こうすることが可能な場合には、レプリケーションは遥かに簡単になります。リスト 29 のコードは、新しいキーを各データベース (映画が公開された年を保管するデータベースと、映画のサウンド・ミキシングに関する情報を保管するデータベース) 上のドキュメントに追加する例です。

リスト 29. レプリケーションでの競合
user> (clutch/with-db movies-db
        (-> (clutch/get-document "386d0400e336e54933a47aec656289c4")
            (clutch/update-document {:re-released 1996})))
{:re-released 1996
 :movie-title "North by Northwest"...}

user> (clutch/with-db moviesb-db
        (-> (clutch/get-document "386d0400e336e54933a47aec656289c4")
          (clutch/update-document  {:sound-mix "Mono"})))
{:sound-mix "Mono"
 :movie-title "North by Northwest"...}

user> (clutch/replicate-database "movies" "movies-b")
{:ok true...}

すべてが適切にレプリケートされたように見えますが、それは、リスト 30 の結果を確認するまでの話です。

リスト 30. レプリケーションで発生した競合の確認
user> (clutch/with-db moviesb-db
        (keys (clutch/get-document "386d0400e336e54933a47aec656289c4")))
 (:movie-title :director :_conflicts :_rev :language
    :runtime :studio :_id :sound-mix :year-released)

movies からの更新は、re-released キーがないことから、消失したように見えます。何が起こったかと言うと、moviesmovies-b にレプリケートされたときに競合が発生しました。その理由は、同じドキュメントの異なるリビジョンをレプリケートしようとしたためです。このような競合が CouchDB で発生しても、情報は一切失われません。代わりに、Couch は当該ドキュメントに関連付けた競合レコードを新規に作成します。このレコードはリスト 31 に記載する Clutch クエリーによって取得することができます。

リスト 31. 競合に関する調査 (Clutch の例)
user> (clutch/with-db moviesb-db
        (clutch/get-document "386d0400e336e54933a47aec656289c4" {:conflicts true}))
{:movie-title "North by Northwest",
 ...
 :_conflicts ["2-ac7e4d143dff32f7be437de99a659ba1"]
 ...}

リスト 32 に、上記に相当する clj-http コードを記載します。

リスト 32. 競合に関する調査 (clj-http の例)
user> (-> (str moviesb-db "/386d0400e336e54933a47aec656289c4?conflicts=true")
          client/get
          :body
          json/read-json)
{:movie-title "North by Northwest"
 ...
 :_conflicts ["2-ac7e4d143dff32f7be437de99a659ba1"]
 ...}

リスト 31リスト 32 には、競合が発生したこと、そしてその競合はドキュメントの特定のリビジョン (この例では、2-ac7e4d143dff32f7be437de99a659ba1) で発生したことが示されています。これで、その特定の競合するリビジョン番号を取得すれば、そのドキュメントを更新することで競合を解決することができます。リスト 33 は、Clutch を使用してリビジョン番号を取得する場合のコードです。

リスト 33. 競合するドキュメントの取得 (Clutch の例)
user> (clutch/with-db moviesb-db
        (keys (clutch/get-document "386d0400e336e54933a47aec656289c4"
                                   {:rev "2-ac7e4d143dff32f7be437de99a659ba1"}
                                   #{:rev})))
(:_id :_rev :re-released :movie-title :director
 :runtime :year-released :studio)

リスト 34 に、上記に相当する clj-http コードを記載します。

リスト 34. 競合するドキュメントの取得 (clj-http の例)
user> (->  (str moviesb-db
                "/386d0400e336e54933a47aec656289c4"
                "?rev=2-ac7e4d143dff32f7be437de99a659ba1")
           client/get
           :body
           json/read-json
           keys)
(:re-released :movie-title :director :_rev :language
 :runtime :studio :_id :year-released)

keys リストには、sound-mix のキーと値のペアは含まれていませんが、re-released のキーと値のペアは含まれていることに注意してください。これらの競合を解決する作業は、開発者の責任です。さらに、ここには「CouchDB ドキュメントの更新」のセクションで説明したルールも当てはまります。つまり、ドキュメントにいかなる変更を加えた場合でも新しいリビジョン ID が生成されるため、それによって競合が発生する可能性があるということです。ドキュメントの異なる部分を変更したとしても、この問題が解決されることにはなりません。問題を解決するための手順は、以下のとおりです。

  1. 最新のドキュメントを読み取ります。
  2. 古い (競合する) バージョンを読み取ります。
  3. ドメイン固有のマージ・ロジックを適用します。
  4. ドキュメントを新しい (マージした) バージョンに更新します。
  5. 競合するドキュメントのバージョンを削除します。

ステップ 5 は、他のあらゆるドキュメントを削除する場合と同じです。ただし、ドキュメントを取得した方法を反映するリビジョン・パラメーターを追加する必要があります。

レプリケーションの競合に関する重要なポイントは、エラーの処理はドメインに固有だということです。この例の場合には、更新をマージして、代替タイトル・フィールドを結合しなければなりません。このロジックに代わる方法は他にも多数あり、例えば、常に最新の更新を取得するという方法もあります。この方法は、映画の興行収入などのドキュメントには適しています。映画の興行収入に関しては、最新の情報だけが興味の対象となるからです。別のケースでは、最初に行われた更新を常に優先するという方法も考えられます。


まとめ

JSON ドキュメントのシンプルなフォーマット、REST API、そして Clutch の優れた Clojure サポートを考慮すると、Clojure を使用して CouchDB にアクセスするという方法は、強くお薦めできる方法です。

CouchDB のビューを Clojure で作成できるということは、アプリケーションでサポートするべき言語が 1 つ減り、コードの継続性が強化されることを意味します。CouchDB の REST の基礎を確実に理解していれば、Clutch が提供する抽象化によって、保守の容易な CouchDB ベースのアプリケーションが短時間で開発できるようになります。

参考文献

学ぶために

  • CouchDB - The Definitive Guide』(J. Chris Anderson、Jan Lehnardt、Noah Slater 共著、O'Reilly Media、2010年1月): CouchDB に関する言語非依存の情報については、この本を調べてください (オンライン・バージョンは無料で入手できます)。第 19 章で、CouchDB のクラスター化について詳しく説明しています。
  • Introducing clj-http, a Clojure HTTP Client」(Mark McGranaghan 著、2010年8月): clj-http を学ぶ出発点となります。
  • CouchDB Wiki: Apache の CouchDB サイトにアクセスして、CouchDB のインストール手順、Futon、および HTTP View API に関するセクションを読んでください。
  • Clojure: Clojure のオンライン・マニュアルを調べてください。
  • Clojure プログラミング言語」(Michael Galpin 著、developerWorks、2009年9月): Clojure を導入して、その構文を学び、Eclipse 対応の Clojure プラグインを利用してください。
  • Java 開発 2.0: Groovy の RESTClient を使用して REST によって CouchDB の操作を行う」(Andrew Glover 著、developerWorks、2009年11月): CouchDB の基礎、そして Groovy で CouchDB を操作する方法を学んでください。
  • Java technical podcast series」(Andrew Glover、developerWorks): Java および関連技術を話題にしたこの技術ポッドキャスト・シリーズでは、Stuart Halloway に Clojure について、Aaron Miller と Nitin Borwankar に CouchOne ついてインタビューしています。
  • Github サービスでの Clutch: ビュー・サーバーのセットアップ手順および使用例については、README を読んでください。
  • Technology bookstore で、この記事で取り上げた技術やその他の技術に関する本を探してください。
  • developerWorks Java technology: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

  • CouchDB: CouchDB を入手してください。
  • Clojure: Clojure をダウンロードしてください。
  • Github サービスでの clj-http: clj-http をダウンロードしてください。
  • Clutch: Clutch をダウンロードしてください。
  • Leiningen: Clojure 対応の Leiningen ビルド・ツールをダウンロードしてください。

議論するために

  • developerWorks コミュニティーに参加してください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者が主導するブログ、フォーラム、グループ、ウィキを調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=642234
ArticleTitle=Clojure によって CouchDB を使用する
publish-date=02222011