目次


Web 時代の非リレーショナルデータベース

第 5 回 Apache CouchDB の最新機能を知り、適用の勘所を掴む

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Web 時代の非リレーショナルデータベース

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Web 時代の非リレーショナルデータベース

このシリーズの続きに乞うご期待。

今回の内容

当連載が開始されてから1年が経過しましたが、当連載の対象であるCouchDBはその間に大きな進化を遂げました。2009/03/31 にリリースされた0.9.0 には、この連載で紹介されていない魅力的な機能が多数追加されています。今回はその一部を紹介すると共に、CouchDBの使いどころを、筆者らの経験を交え考察してみようと思います。

0.9.0 で追加された主要な機能

Apache CouchDB 0.9.0 では、大幅な機能追加が発表されていますが、とりわけWebアプリケーションの開発パラダイムに大きなインパクトを与えるのは次の2つでしょう。

  • ドキュメントの出力フォーマット調整機能[3]
  • ドキュメントの入力バリデーション機能[4]

これらの機能を用いると、(認証・認可の機能を除いて)Webアプリケーションサーバーの支援を得ずに、データベースだけでWebアプリケーションを作成することが可能になります。以下に2つの機能の概要を紹介します。

ドキュメントの出力フォーマット調整機能

第2回 の記事では、Ruby のHTTPクライアントを使って、CouchDBからJSONドキュメントを取得し、Railsのフレームワークに従って(ActionController/AcitonViewを使って)HTMLがレンダリングできることを紹介しました。このとき、Webアプリケーション層(Rails) は複雑な動作をするわけではなく、単にデータベースからドキュメントを取得しJSONフォーマットのドキュメントをHTMLに変換するだけの仕事しかしていません。

CouchDB の0.9.0以降では、このようなアプリケーションが単純なデータの変換作業しかしないようなケースにおいて、データベース自身がJSONデータを操作し、直接End Userのクライアント(ブラウザ)に任意の出力フォーマットで応答する機能を利用することができます。具体的には、第3回で紹介したデザインドキュメントに対して、showsおよびlistsというキーを設けて、出力ロジックをJavaScriptで記述することで、MapReduceの時と同様にデータベース側でアプリケーション処理を走らせることができます。

例えば、第2回のWikiアプリケーションにおいて、Wikiページの表示には、次のようなController/Viewのコードを記述していました。

class WikiPagesController < ApplicationController
  ...(snip)...

  def show
    begin
      @wiki_page = WikiPage.find(params[:id])
    rescue Net::HTTPServerException => e
      if e.response.is_a?(Net::HTTPNotFound)
        # 404 Not Found は返さずに新規作成ページ
        # (/wiki_pages/new?title=XXXX)にリダイレクトします
        flash[:notice] = "ページが見つかりません。新規に作成します。"
       redirect_to new_wiki_page_url(:title => params[:id])
      else
        # エラー処理は実装していません
        render :text => e.response.body, :status => e.response.code
      end
    end
  end

  def new
    @wiki_page = WikiPage.new("title" => params[:title])
  end

  ...(snip)...
end
<h1><%= @wiki_page.title %></h1>
<div><%= flash[:notice] %></div>
<div>
<%= link_to '編集', edit_wiki_page_url(@wiki_page) %>
<%= link_to '削除', wiki_page_url(@wiki_page), 
    :confirm => "削除してよろしいですか?", 
    :method => "delete" %>
</div>
<div>
<%= simple_format @wiki_page.content %>
</div>
<div>
revision: <%=h @wiki_page.revision %>
</div>
<h1>新規ページの作成</h1>
<div><%= flash[:notice] %></div>
<% form_for(@wiki_page) do |f|%>
<div>
  ページタイトル:<%=h @wiki_page.title %>
  <%= f.hidden_field :title %>
</div>
<div><%= f.text_area  :content, :size => "80x10" %></div>
<%= submit_tag '作成' %>
<% end %>

これらの機能は、次のようにCouchDB のデザインドキュメントに格納できます。

{
   "_id": "_design/couchiki",
   "_rev": "23-176481494",
   "lists": {
       "wiki_index" : "function ... "
   },
   "shows": {
       "wiki_show": "function ...",
       "wiki_edit": "function ...",
   },
   "views" : {
       ...
   }
}

第3回 で見たように、デザインドキュメントに記述する関数は文字列でなければならないため、便宜上、wiki_showの値となる文字列をJavaScriptの構文で記述したものを掲載します。

/**
 *
 * URI:
 *   http://{host}:{port}/{db}/_design/couchiki/_show/wiki_show/{docId}
 *
 * Description:
 *   http://{host}:{port}/{db}/{docId} をフォーマットするためのJavaScript
 *
 * Parameters:
 *   doc - データベース内の{docId}にマッチする_idをもつドキュメント
 *   req - リクエスト環境変数(クエリパラメーターやUserAgentなど)
 *
 */
function(doc, req){
  if(doc){
    // {docId} が指定され、ドキュメントが見つかった場合 
    // WikiPagesController#show に相当


    // 編集用のレンダリングは 
    // http://{host}:{port}/{db}/_design/couchiki/_show/wiki_edit/{docId} 
    // とする

    // 削除は Client の JavaScript で HTTP DELETE を発行する(省略)
    var docDeleteFun = "(function(){ ... })()";
    // HTML の構成
    return "<html><body>" +
      "<h1>" + doc.title + "</h1>" +
      "<div>"  +
        "<a href='javascript:" + docDeleteFun + "'>編集</a> " +
        "<a href='../edit/" + doc._id + "'>削除</a>" +
      "</div>" +
      "<div>"  +
      doc.content +
      "</div>" +
      "<div>"  +
        "revision: " + doc._rev +
      "</div>" +
      "</body></html>";
  }else if(req.docId){
    // {docId} が指定され、ドキュメントが見つからなかった場合 
    // WikiPagesController#new に相当
    var newDoc = {
      title : req.docId,
      content : ""
    };

    // HTML の構成
    return "<html><body>" +
      "<h1>新規ページの作成</h1>" +
      "<div>ページが見つかりません。新規に作成します。</p>" +
      "<form onsubmit='" + docCreateFun + ";return false;'>"  +
      "<div>"  +
      "ページタイトル:" + newDoc.title +
      "<input type='hidden' name='title' value='" + newDoc.title + "'/>" +
      "</div>" +
      "<div>"  +
      "<textarea name='content'>" + newDoc.content + "</textarea>" +
      "</div>" +
      "<div>" +
      "<input type='submit' value='作成'></input>" +
      "</div>" +
      "</form>" +
      "</body></html>";
  }else{
    // {docId} が指定されていない場合
    // 200 以外でかえす場合は {code: xxx, body: yyy} 形式の
    // オブジェクトを返すようにする
    return {
      code: 404,
      body: "Not Found"
    };
  }
}

shows の機能は、(他のCouchDBの機能と同様に) HTTP 経由で提供されます。したがって、ブラウザで直接showsの示すURIにアクセスすることによって、アプリケーションサーバー(Railsの動作するサーバー)を介することなくブラウザにアプリケーションを提供できます。showsでは、URIの一部にドキュメントのIDを渡すことができ、これにより「データベースからドキュメントを取得する」部分をCouchDBに任せることができます。つまり、開発者はデータベースアクセスのおきまりの作業から解放され、アプリケーションロジックに専念できます。ただし、ERB、あるいは関連するユーティリティメソッド(simple_formatなど)の高度な機能については別途JavaScriptライブラリの導入が必要なるケースがほとんどです。よりJavaScript関連の技術エリアについて精通しておく必要があるでしょう。

上記の例では、1つのドキュメントの例を示しました。複数のドキュメントに対してフォーマット調整を行うためには、lists を用います。lists を用いるためには、あらかじめフォーマット対象となる複数のドキュメントを特定するためのMapReduce(views)を記述しておく必要があります。

shows はデータベース上にある1つのドキュメントに対して処理が適用されますが、lists はMapReduceの結果の(0個以上の)ドキュメントに対して処理が適用される点が使い分けの方針となります。

ドキュメントの入力バリデーション機能

出力フォーマット機能を使うことで、参照系機能に関しては(単純なものであれば)データベース上で処理可能なことを見てきました。では、一方で入力、更新系の機能はどうでしょうか。

CouchDB 0.9.0 では、入力系アプリケーションの支援機能として、HTTP PUT/POSTで送信されたドキュメントに対してJavaScriptで記述したバリデーションを行うことができるようになっています。一般的なWebアプリケーションでも、入力についてはフォームから入力されたデータをアプリケーションサーバー上で検査し、その後、データベースに投入することがほとんどです。CouchDBでは、この「アプリケーションサーバー上で検査し」の部分をデータベース上で行うことができ、そのルールの記述にJavaScriptを用いることができます。

この機能を使うのは簡単です。デザインドキュメント上に(views/shows/listsなどと同じように)validate_doc_updateというキーを登録します。値には検査用のJavaScriptを配置します。

{
   "_id": "_design/couchiki",
   "_rev": "24-3099602959",
   "lists": {
       "wiki_index" : "function ... "
   },
   "shows": {
       "wiki_show": "function ...",
       "wiki_edit": "function ...",
   },
   "views" : {
       ...
   },
   // 検証用関数
   "validate_doc_update" : "function ..."
}
/**
 *
 * Description:
 *   couchiki データベースのデータ検証用関数
 *
 * Parameters:
 *   newDoc  - HTTP PUT/POST されたドキュメント
 *   oldDoc  - 同じ_idで既に登録されているドキュメント
 *   userCtx - 認証情報
 *      userCtx.db   - データベース名
 *      userCtx.name - ユーザー名
 *      userCtx.role - ロール名(0.9.0ではadminsのみ対応)
 */
function(newDoc, oldDoc, userCtx){
  if(oldDoc){
    // 更新の場合
    if( oldDoc.title != newDoc.title ){
      throw({forbidden : "Cannot change the title."});
    }
  }else{
    //  新規作成の場合
  }
  // 更新, 新規作成共通
  if( newDoc.title == null ||
      newDoc.title == ""){
    throw({forbidden : "Title is not specified."});
  }
  if( newDoc.title.length > 255){
    throw({forbidden : "Title is too long."});
  }
}

上記は、ドキュメントが title というメンバーを持ち、title が空文字ではないことを保証しています。さらに、ドキュメントの更新時にはタイトルが変更されないことを保証します。

尚、validate_doc_updateによる検証は、デザインドキュメントが配置されたデータベース単位で有効になります。つまり、1つのデータベースに複数の異なるvalidate_doc_updateが定義されたデザインドキュメントが配置されている場合は、すべてのvalidate_doc_updateが検査されます。これは、複数のアプリケーションでデータベースを共有する際に、データの堅牢性を維持するために機能します。

ここまで見てきたshows/lists/validatesの入出力機能によって、CouchDBをデータベース機能付きアプリケーションサーバーとして利用することが可能になることがわかるでしょう。デザインドキュメントは、それぞれが1つのCouchDB用アプリケーションパッケージであることを意味しています。さらに、デザインドキュメント自体もCouchDBにストアされるドキュメントであることに変わりはありません。このことは、第4回で示したレプリケーションによって、CouchDB用のアプリケーションパッケージが簡単に配布、複製できることを意味しています。

尚、実際に本格的なCouchDBの上で動作するアプリケーション開発(デザインドキュメント開発)を行う場合には、CouchApp [5]と呼ばれるフレームワーク(およびユーティリティライブラリ)を使うと便利でしょう。CouchAppを使うことで、アプリケーション開発の開始からデプロイまでを効率的に進めることができます。

CouchDBの適用

当連載「Web 時代の非リレーショナルデータベース」では、Apache CouchDBに注目をし、その特徴的な機能を紹介してきました。筆者らは、これらの機能を使っていくつかのアプリケーションのプロトタイピングに取り組みました。これらの例を紹介し、CouchDB 適用の勘所をまとめます。

Webベースのアンケートアプリケーション

1つ目のアプリケーションは、アンケートをWeb上でユーザーが作成・公開し、そのURLを回答者に配布することで、アンケートの収集を効率的に行うためのアプリケーションです。これは、従来、MySQLをバックエンドにして、アンケートの概要、アンケートの質問項目、回答データ、などを正規化したテーブルで保持していました。単純なアンケートシステムであったとしても、回答選択肢、単一回答用質問項目、複数回答用質問項目、任意のテキスト回答用質問項目、など、一般的な機能を満足させようとするとテーブルの設計は複雑になっていきます。そこで、CouchDBを採用し、次の2つのシンプルなデータモデルで多くの機能を実現していきました(図1)。

図 1. Relational DBからDocument Oriented DBへ
Relational DBからDocument Oriented DBへ
Relational DBからDocument Oriented DBへ
// アンケート回答用紙テンプレート
{
   "_id"  : "0d7647098c6841bf4eed6bfc4eacbc68",
   "_rev" : "5-3504214704",
   "type" : "Survey",

   "status"     : "published",
   "start_on"   : "2009-10-01",
   "end_on"     : "2009-12-31",
   "created_at" : "2009/09/15 01:01:45 +0000",
   "updated_at" : "2009/09/15 01:01:45 +0000",
   "created_by" : "ysasaki2@jp.ibm.com",

   "title": "Test",
   "description": "これはテストアンケートです。",

   "questions": [
       {   // 単一行テキスト
           "required": true,
           "type": "SingleLineText",
           "description": "単一行テキストで回答する質問です。"
       },
       {   // ドロップダウンリスト
           "selections": [
               "a",
               "b",
               "c"
           ],
           "type": "DropDownList",
           "include_blank": true,
           "description": "ドロップダウンリストで回答する質問です。"
       },
       ... (snip) ...
   ]
}
// アンケート回答用紙(記入済み)
{
   "_id"  : "6b86381bca5991c9de78973c01181159",
   "_rev" : "1-1855487811",
   "type" : "Response",

   "created_at" : "2009/09/15 01:01:45 +0000",
   "updated_at" : "2009/09/15 01:01:45 +0000",
   "survey_id"  : "0d7647098c6841bf4eed6bfc4eacbc68",
   "created_by" : "testuser@jp.ibm.com",

   "answers": [
       {
          "value": "test"
       },
       {
          "value": 1
       },
       ... (snip) ...
   }
}

上記を見てわかるとおり、2つの単純なJSONドキュメントで、回答用紙のテンプレートと回答用紙(記入済み)を表現できます。SurveyおよびResponse という、大まかなデータ構造は決まっていますが、その詳細の構造(questions配列の要素データの構造)はそれぞれ異なる点が特徴的です。なぜ、このような設計が有効なのでしょうか。そのヒントは、アプリケーションのユースケースにあります(図2)。

図 2. アンケートアプリケーションにおけるドキュメント指向
アンケートアプリケーションにおけるドキュメント指向
アンケートアプリケーションにおけるドキュメント指向

アンケートの作成者は、回答用紙のテンプレートを(WordやExcelなどで作成するように)、WebのHTMLフォームを用いて作成するとJSONで表現されたSurveyドキュメントとして、CouchDBにテンプレートが格納されます。そして、アンケートの回答者は回答用のWebのURLにアクセスすると、アプリケーションは回答用紙となるHTMLフォームをユーザーに表示すると共に、回答者の入力に対して回答者毎のResponseドキュメントを作成し、入力結果をCouchDBに保存します。アンケートの回答が集まり出すと、アンケート作成者は、回答用紙の集計作業に入ります。このとき、アプリケーションはMapReduceによるクエリでの集計を行い、Surveyテンプレートドキュメントを使って、集計結果をHTMLまたはExcel(CSV)形式でアンケート作成者に提示します。

このユースケースは”現実世界の書類ベースのやりとり”をそのままWebで実現したものです。回答用紙の配布はURLリンクの配布(とユーザーのアクセス)で再現され、アンケート用紙または記入済み回答用紙の作成にはHTMLフォームのsubmitが利用されます。データ定義は、現実世界の書類に記入するデータを忠実に再現したもので、この類のアプリケーションはドキュメント指向アプリケーションと呼ばれます。アンケートなどを含む、CMSやグループウェアに分類されるアプリケーションでは、ドキュメント指向によるアプリケーションデザインをすることで、データの取り扱いに対する煩雑さを解消できる場合があります。

一方、リレーショナルデータベース(MySQL)からCouchDBへの移行に際して、正規化を崩してしまったことで、データベース管理者から見ると、データの重複や冗長性が見えてきてしまいます。しかし、「アンケート」作業という特徴からすれば、データを管理する主体は「アンケート作成者」であるユーザーの側にありますから、データベース管理者がアンケートの項目の冗長性を排除できなくても、それほど問題になることはありません。データベース管理者の主業務は、データベースをバックアップ/リストアすること、および古くなって不要となったアンケートデータを別のデータベースにレプリケーションで分割することです。アプリケーションのバージョンアップに伴うテーブル保守作業からは完全に解放されます。

ワークフロー処理アプリケーション

もう1つのアプリケーションでは、ワークフローの実行要求を処理するためのリポジトリとしてCouchDBを活用しました。CouchDBの中に、JSONで記述した簡易なワークフローの実行定義を蓄積し、要求に応じてワークフローの処理を実行するものです。尚、ここでいうワークフローとは、BPEL等で議論されるビジネスプロセスワークフローではなく、いわゆるシステムタスク(シェル)の自動化に用いる処理手順のことを指しています。

こちらのアプリケーションは、「マシンリソースの調達」にかかる書類ベースのやりとりをそのままWebプラットフォーム上で実現したものです(図3)。昨今では、Amazon EC2 や Tivoli Provisioning Manager といったプロビジョニング機能を実装したサービスあるいはソフトウェアを用いて、自動的にOS環境を作成することが容易になっています[6][7]。そして、その機能はAPI として公開され、シェルなどからも簡単に利用できるようになっています。これらのAPIとCouchDBを利用してワークフローリポジトリを構築しました。

図 3. ドキュメント指向モデルに基づくワークフローリポジトリ
ドキュメント指向モデルに基づくワークフローリポジトリ
ドキュメント指向モデルに基づくワークフローリポジトリ

そこで、これらのAPI機能とエンドユーザーのプロビジョニングリクエストを結びつけるためのリソース申請文書の提出処理フローをシステムタスクにマッピングするためのレイヤーをCouchDBで実装しました。こちらもアンケートアプリケーションと同じように、リソース申請文書を事前に登録しておき、それらを今回紹介した出力フォーマット機能でブラウザにHTML フォームとして表示させます。そして、エンドユーザーは必要事項を記入して、リポジトリに記入済み申請文書をサブミットすることで各種プロビジョニングツールを利用することができるようになります。

// リソース申請文書(テンプレート)
{
   "_id": "template:iyo:Template_RHEL5U3_x86_64_minimum",
   "_rev": "2-1594708596",
   "doc_type": "RequestTemplate",
   "req_type": "CreateInstance",

   // インスタンス作成を要求するリソーステンプレート
   "name": "Template_RHEL5U3_x86_64_minimum",
   "template_info": {
       "spec": {
           "cpu": 2,
           "memory_mb": 1024,
           "disk_gb"  : 15,
       },
       "description": 
          "Red Hat Enteprise Linux 5 update 3 x86_64 minimum image"
   },

   // ワークフローに関する記述
   "handler": {
       "name": "deploy_new_instance",      // 使用するスクリプト
       "arguments": {                      // 引数の名前とデフォルト値
            // Tivoli Provisioning Manager 7.1 を利用
           "engine"   : "tpm71",           
           "template" : "Template_RHEL5U3_x86_64_minimum" 
       }
   }
}
// リソース申請文書(記入済み, 処理済み)
{
   "_id": "521bd5b66c7a9cfb62cb13b8bd2ce5c2",
   "_rev": "3-156337340",
   "doc_type"  : "Request",
   "req_type"  : "CreateInstance",
   "client_id" : "test_project1",
   "created_at": "2009-09-11T15:39:25Z",
   "updated_at": "2009-09-11T16:04:31Z",

   // 処理結果, 正常処理していればリソース納品文書が作成されているので、
   // そのリンクを埋め込むこととする
   "result": {
       "status" : "success",
       "create_doc_id": "b384393566080329ca509d867fbe24cf",
   },

   // 処理要求内容
   "handler": {
       "name": "deploy_new_instance",
       "arguments": {
           "template": "Template_RHEL5U3_x86_64_minimum",
           "engine": "tpm71"
       }
   }
}
// 仮想マシンインスタンス納品文書
{
   "_id": "b384393566080329ca509d867fbe24cf",
   "_rev": "1-4177540201",
   "doc_type": "DeliveredInstance",
   "created_at": "2009-09-11T06:34:43Z",
   "updated_at": "2009-09-11T06:34:43Z",

   "name"      : "521bd5b66c7a9cfb62cb13b8bd2ce5c2",
   "client_id" : "test_team_room",
   "request_doc_id" : "521bd5b66c7a9cfb62cb13b8bd2ce5c2",

   // 納品されたインスタンス情報
   "instance_info": {
       "ipaddr": "192.168.128.27",
       "access_methods": {
           "ssh": {
               "public_key"  : "ssh-rsa ..."
               "private_key" : "-----BEGIN RSA PRIVATE KEY-----...",
               "user": "root"
           }
       },
       "hostname": "pc-000C296E92E6"
   },

   // 後続処理のテンプレート
   "request_templates": {
       "destroy": {
           "doc_type": "RequestTemplate",
           "req_type": "DestroyInstance",
           "name": "Destroy",
           "handler": {
               "name": "destroy_instance",
               "arguments": {
                   "instance_name": "521bd5b66c7a9cfb62cb13b8bd2ce5c2",
                   "engine": "tpm71"
               }
           },
           "description": "A workflow to destroy this instance"
       }
   }
}

プロビジョニングに必要なデータ項目は、個々のツールの実装によって異なっているため、一部の項目(例えばプリインストールするOSの名称など)は共通化できますが、すべてが共通化されたスキーマを用いることは難しくなってきます。例えば、上記の例ではTivoli Provisioning Manager を用いているため、テンプレートをリクエストすることで同時にディスクも入手できますが、Amazon EC2の場合は、AMIというテンプレートにはディスクがないため、別途ディスク取得用のデータを引数として入力する必要があります。

この点について、CouchDBはスキーマの事前定義が必要なく、事前に共通化されたデータ項目の定義と、共通化されていない、アプリケーションによって順次決められるデータ項目の定義を同一のドキュメント内で扱うことができるため、起こりうる問題を解決する手段として役立ちます。

さらに、システムタスクを定義したワークフローは、場合によっては定義の複製を実行することができる場合があります。つまり、あるワークフローの定義をCouchDB間でレプリケーションを行うことによって、複数のノードで同じタスクを実行する、あるいは1つのノードでタスクが失敗したことを受けて、定義が複製された別のノードでワークフローを再開する、などの応用ロジックも実装することができます。CouchDBのレプリケーションは、現時点では完全に手動のみに駆動になるため、このような応用ロジックの実装にはそれなりの開発コストがかかります。しかし、システムタスクワークフローの複製実行1つをとってみても、分散システムを構築する手段の選択肢としては非常に魅力的ではないでしょうか。

まとめ

これまで、CouchDBの適用ケースとして、筆者らが行った2つのプロトタイプの例を紹介しました。いずれの例も、リレーショナルデータベースでは設計が難しくなるケースを題材にし、リレーショナルな設計にとらわれないCouchDBを利用することで享受できる部分に言及しました。もちろん、現時点では、データの非機能要件(堅牢性、可用性、安全性など)の実装の面では、リレーショナルデータベースには及ばない点があるのも事実であり、このトレードオフを考慮してシステム設計の検討を行う必要があります。また、企業アプリケーションでの利用を考えると、データモデルが固定されないという点は企業データのガバナンスをどのように保持していくのかという課題に直結することも考えられます。しかし、Webというプラットフォームの上で”ドキュメント”をありのまま格納することにより、スケーラビリティを確保し、また分散システムに適応できるデータストアとしてのCouchDBは、今後、Webで利用されるデータベースの有力な1つの選択肢となることでしょう。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Open source
ArticleID=432811
ArticleTitle=Web 時代の非リレーショナルデータベース: 第 5 回 Apache CouchDB の最新機能を知り、適用の勘所を掴む
publish-date=10092009