CouchDBでRelaxアプリケーション開発 :: 10分で作る FeedReader -> それ"データベース"でできるよ編の準備
タイトルは釣りですが、まじめにこれは釣りたいww 10分でライブデモができるようになるには事前の準備が必要です。
いろいろCouchDBで遊べそうなのはわかった(何がどうわかったのかは、あとでちゃんとした文書で公開します....そのうち)ので、アプリケーション開発にチャレンジです。
目下のところ、WebJourneyくんはBlog Widgets + HTML Widgets + Feed Widgets という用途でしか使っていないので、とりあえずFeedReaderをCouchDBだけで普通に作るには、というのをチャレンジしてみようと思います。
CouchApp は CouchDBだけでアプリケーションを開発する場合のユーティリティライブラリ(テンプレートエンジン等)が含まれます。JavaScriptライブラリと、一部のコマンドラインタスクを実行するためのPythonライブラリです*1。
Python は Mac OS X に標準でついている 2.5(/System/Library/Frameworks/Python.framework/Versions/2.5/Resources/Python.app/Contents/MacOS/Python) を使います。easy_install をアップデートしてから、couchappをインストールします。
$sudo easy_install -U setuptools $sudo easy_install couchapp
以上です。このエントリ起票時点で0.2が入ります。ちなみに、OS X だと http://mac.softpedia.com/progDownload/CouchApp-Download-49777.html とかあったりするんですがね。普通に easy_install のほうがイイと思います。
さて、でプロジェクトの作成。プロジェクトの名前は、relax_readerとでもしておきましょうか。
$ couchapp generate relax_reader (snip) [Errno 13] Permission denied: '/Users/yssk22/.python-eggs/simplejson-2.0.9-py2.5-macosx-10.5-i386.egg-tmp' (snip)
というわけでいきなりやると、こけます。sudo easy_install で使ったegg cacheが残っているようなので、削除しておきましょう。
$ sudo rm -fr ~/.python-eggs $ couchapp generate relax_reader Generating a new CouchApp in /Users/yssk22/project/relax_reader
OKです。ちなみに、なんかどっかでみたようなRailsにインスパイアされてない?ってな具合に、ディレクトリができあがります。
$ cd relax_reader $ ls -al total 8 drwxr-xr-x 10 yssk22 staff 340 5 12 05:29 . drwxr-xr-x 20 yssk22 staff 680 5 12 05:29 .. -rw-r--r-- 1 yssk22 staff 2 5 12 05:29 .couchapprc drwxr-xr-x 5 yssk22 staff 170 5 12 05:07 _attachments drwxr-xr-x 3 yssk22 staff 102 5 12 05:07 foo drwxr-xr-x 4 yssk22 staff 136 5 12 05:07 lib drwxr-xr-x 3 yssk22 staff 102 5 12 05:07 lists drwxr-xr-x 3 yssk22 staff 102 5 12 05:07 shows drwxr-xr-x 3 yssk22 staff 102 5 12 05:07 vendor drwxr-xr-x 3 yssk22 staff 102 5 12 05:07 views
CouchDBを深く知っていると、
- views に MapReduceのjs
- listsには一覧表示用のjs
- shows には 1ドキュメント表示用のjsを置く
ということは想像つきますね。ほかのディレクトリはおいおい。
で、どうやら views などをみるとexampleというのができているので、このままでもHello World相当のことはやってくれるんじゃね?的な期待をもって、アプリケーションをCouchDBにアップロードします。CouchDBは単なるデータベースではありません。アプリケーションランタイムを備えたデータストアです(これ重要)。ストアドプロシージャーとは全然違います。アプリケーションでありプロシージャーではないです。
で、アップロードどうやるの?っていう話ですが、couchapp のサブコマンド push を使います。
$ couchapp push http://localhost:5984/relax_reader Pushing CouchApp in /Users/yssk22/project/relax_reader to design doc: http://localhost:5984/relax_reader/_design/relax_reader Visit your CouchApp here: http://localhost:5984/relax_reader/_design/relax_reader/index.html
これで、データストアが作成され、アプリケーションが投入され、UIまで登録されました、と。
_design/relax_reader には、サンプルのアプリケーションが含まれているんですが、lists という一覧を生成するデザインドキュメントに feed というのがあります。。。。
って、つまり何も作らんでもFeedReaderになりますよってことで...
せっかくなのでデザインドキュメントの解説。
function(head, row, req) { respondWith(req, { html : function() { if (head) { return '<html><h1>Listing</h1> total rows: '+head.row_count+'<ul/>'; } else if (row) { return '\n<li>Id:' + row.id + '</li>'; } else { return '</ul></html>'; } }, xml : function() { if (head) { return {body:'<feed xmlns="http://www.w3.org/2005/Atom">' +'<title>Test XML Feed</title>'}; } else if (row) { // Becase Safari can't stand to see that dastardly // E4X outside of a string. Outside of tests you // can just use E4X literals. var entry = new XML('<entry/>'); entry.id = row.id; entry.title = row.key; entry.content = row.value; return {body:entry}; } else { return {body : "</feed>"}; } } }) }
肝心なところは、
var entry = new XML('<entry/>'); entry.id = row.id; entry.title = row.key; entry.content = row.value; return {body:entry};
これは、CouchDBのMapReduceの結果生成される (ID, Key, Value) ペアのリストをFeedとして返します、っていう話。
なので、Feed の1entryを1つのJSONドキュメントに直して、CouchDB に放り込んであげて、MapReduceを書けてあげれば、以上終了です。
ということで、コマンドラインで動く script/crawl.rb でも作りましょうか。クロールする部分はCouchDBとは全く関係のない外部プロセスなので、どんなスクリプトでもイイです。単に、Rubyのfeed-normalizer的なライブラリのPython/JavaScript 実装を知らないだけです。
#!/usr/bin/env ruby require 'rubygems' require 'feed-normalizer' require 'json' # クロール対象ターゲット # もちろん、これを事前にCouchDBに登録できるようにしておくのがいわゆるFeedReaderだけれど TARGETS = [ "http://d.hatena.ne.jp/yssk22/rss", "http://damienkatz.net/atom.xml" ] docs = [] TARGETS.each do |uri| feed = FeedNormalizer::FeedNormalizer.parse(open(uri)) # Feed は登録 docs << { :_id => feed.url, :doc_type => "Feed", :title => feed.title, :description => feed.description, :last_updated => feed.last_updated.utc.strftime("%Y/%m/%d %H:%M:%S +0000"), } # entries も別ドキュメントで feed.entries.each do |entry| docs << { :_id => entry.url, :doc_type => "Entry", :title => entry.title, :description => entry.description, :last_updated => entry.last_updated.utc.strftime("%Y/%m/%d %H:%M:%S +0000") } end end # 送信 Net::HTTP.start("localhost", 5984) do |http| res = http.post("/relax_reader/_bulk_docs", { :docs => docs }.to_json) puts res.inspect end
で、
$ ruby script/crawl.rb
とすればOK。
次にMapReduce。Feed 用のFeedと Entry用のFeedと、FeedとEntryをごちゃ混ぜにしたFeedを作ることにします。
views/for_feeds, views/for_entries, views/for_feeds_and_entries とディレクトリを3つ作ってmap.jsをそれぞれ記述。reduceはいらないと思う。
// views/for_feeds/map.js function(doc){ if( doc.doc_type === "Feed" ){ emit(doc.title, doc.description); } }
// views/for_entries/map.js function(doc){ if( doc.doc_type === "Entry" ){ emit(doc.title, doc.description); } }
// views/for_feeds/map.js function(doc){ emit(doc.title, doc.description); }
で、再度couchapp push。ちなみにcouchapp は generate で作った COUCHAPP_ROOT 配下にあるファイルをすべからくCouchDBに放り込みます。なので、、、svnやgitもCouchDBのrevisionシステムの原理原則からいうといらなくなってしまいますww
ソースコードも過去の履歴も、本番環境も、全部データベースにあるから!え、テスト環境?それは、適当にデータベースをレプリカしてどっかでやってくれ!。。うまくいったらこっちに再レプリカして更新してくれよ!
余談ですが、これは本当にLotus Notes アプリケーションみたい(実際のNotesアプリ開発は知らないんですけれど、Notesユーザーとしてはそう見えてしまいます)。10年後に起こる問題が予想できてしまう...
という話はおいといて*2、
$ couchapp push http://localhost:5984/relax_reader Pushing CouchApp in /Users/yssk22/project/relax_reader to design doc: http://localhost:5984/relax_reader/_design/relax_reader Visit your CouchApp here: http://localhost:5984/relax_reader/_design/relax_reader/index.html
で、あとは Accept: application/xml にしたGETリクエストをCouchDBにおくるのです。が、明示的に指定しなくてもCouchDB が Content Negotiation をしてくれます。
http://localhost:5984/relax_reader/_design/relax_reader/_list/feed/for_feeds に対して Firefox でリクエストを送ると、HTMLがかえってきます。Firebug でみると、
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
な具合なので、text/htmlが優先されたようです。一方、コマンドラインからcurlを使って上記のURIにアクセスするとXMLがかえってきます。
$ curl -X GET -H "Accept: application/xml" http://localhost:5984/relax_reader/_design/relax_reader/_list/feed/for_feeds <feed xmlns="http://www.w3.org/2005/Atom"><title>Test XML Feed</title><entry> <id>http://damienkatz.net/</id> <title>Damien Katz</title> <content>Everybody keeps on talking about it Nobody's getting it done</content> </entry><entry> <id>http://d.hatena.ne.jp/yssk22/</id> <title>Web屋かもしれない人の日記 || WebJourney 開発ログ</title> <content>Web屋かもしれない人の日記 || WebJourney 開発ログ</content> </entry></feed>imac:util yssk22$
当然、Acceptをtext/htmlに切り替えると、Firefoxと同じ結果を取得できます。
$ curl -X GET -H "Accept: text/html" http://localhost:5984/relax_reader/_design/relax_reader/_list/feed/for_feeds <html><h1>Listing</h1> total rows: undefined<ul/> <li>Id:http://damienkatz.net/</li>
これは、すばらしい!!