2010/11/08

CouchDB + Ruby 1.9の組み合せとJSONライブラリ

CouchDBへのアクセスには、Rubyに限らず、JSONライブラリを使いましょうというお話しです。

CouchDBには 言語別の簡単解説があって、どれをみても簡単に始めることができそうです。

ただしJSONを文字列として作成していて、実際に使う場合にはハッシュへの変換をするでしょうから、ハッシュとやりとりをする常套句も知りたいところです。

CouchDBのRuby用ガイドには「DBの作成」、「DBの削除」、「Documentの作成」、「Documentの参照」の4つのパターンについてだけ触れられていますが、「Documentの削除」や「Documentの更新」については触れられていません。

JSONを扱った文書には載っているんでしょうけれど、CouchDBのガイドに載っていないところをまとめておきます。

環境

rubyは自前でコンパイルし、/usr/local/bin/ruby1.9 に配置(./configure --prefix=/usr/local/stow/ruby-1.9.2p0 ; make install ; cd /usr/local/stow ; xstow ruby-1.9.2p0)しています。

  • OS: Ubuntu 10.04 LTS 64bit版
  • CouchDB: 0.10.0-1ubuntu2
  • Ruby: ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]

また CouchDBのRuby用ガイドにあるCouchモジュールは couchdb.rb の名前で別ファイルに保存しています。

ファイル毎の文字コード設定

まずは日本語などのnon-asciiな言語を文字列として扱うファイルに、encoding:行を追加し、正しく設定しましょう。

各スクリプトの先頭2行

#!/usr/local/bin/ruby1.9 -I.
# -*- encoding: UTF-8 -*-

パッケージを使っていて、/usr/bin/ruby1.9 があるかもしれません。 パスは適宜変更してください。

また、"-I."の部分はカレントディレクトリにあるcouchdb.rbを参照するためで、これも環境に合わせて変更してください。

ポイントは2行目で、非ASCII文字を含むRubyスクリプトファイルにencoding行がない場合には次のようなエラーを表示します。

./json_test.rb:20: invalid multibyte char (US-ASCII)

コマンドラインでencodingを指定することもできますが、ファイル毎に独立しているコメント形式での指定の方が良さそうだなと感じています。

JSONデータ構造の作成

CouchDBのサンプルでは、文字列としてJSONのデータ構造を作成していますが、実際には1.9に標準添付されているjsonライブラリを使う事になると思います。

1.8用には標準添付ではないですが、UbuntuなどDebian系では、"libjson-ruby1.8"パッケージで提供されています。

ハッシュからJSON文字列の作成

ハッシュを作成して、to_jsonメソッドで変換するのが一番簡単かなと思います。

JSON文字列の作成

#!/usr/local/bin/ruby1.9
# -*- encoding: UTF-8 -*-
require 'json'

json = Hash.new
json['name'] = 'foo bar'
json['age'] = '16'
json['address'] = {:city => 'AizuWakamatsu', :pref=>'Fukushima'}

p json.to_json
JSON文字列からハッシュへの変換

受け取ったJSONはハッシュへ変換するのが簡単でしょう。

JSONからハッシュの作成

#!/usr/local/bin/ruby1.9
# -*- encoding: UTF-8 -*-
require 'json'

json_str = "{\"name\":\"foo bar\",\"age\":\"16\",\"address\":{\"city\":\"AizuWakamatsu\",\"pref\":\"Fukushima\"}}"
json = JSON.parse(json_str)
json.each do |k,v|
  print "#{k}\t: #{v}\n"
end

データについて必要な知識はこれぐらいで十分そうです。

Documentの削除

既存Documentの削除には rev をURLに追加する事が必要です。

そのため、削除するためには最初にGETメソッドでDocumentにアクセスする必要があります。

また、サンプルのCouchクラスを使う場合、存在しないDocumentへのアクセスには例外が発生します。 そのため通常は文書が存在しないかもしれないわけで、begin, rescueによる例外のハンドリングが必要になるでしょう。

Documentの削除を行ない、新規ドキュメントを準備するコード

#!/usr/local/bin/ruby1.9 -I.
# -*- encoding: UTF-8 -*-

require 'couchdb'
require 'json'

couch = Couch::Server.new("localhost", "5984")
url = "/example/delete_doc"
begin
  res = couch.get(url)
  json = JSON.parse(res.body)
  rev = json['_rev']
  couch.delete("#{url}?rev=#{rev}")
rescue
  print $! 
  print "[info] delete operation was canceled.\n"
end

couch.put(url,"{}")
json = JSON.parse(res.body)
rev = json['_rev']
## put your code.

Documentの更新

既存Documentへの更新を行なうためには、やはり rev が必要ですが、今度はJSONのデータ構造に入れます。

先ほどの削除では最後に空のDocumentを作成したので、そういった空のDocumentにデータを追加するのは次のようなコードになります。

作成済みDocumentにデータを追加する

#!/usr/local/bin/ruby1.9 -I.
# -*- encoding: UTF-8 -*-

require 'couchdb'
require 'json'

couch = Couch::Server.new("localhost", "5984")
url = "/example/update_doc"
begin
  res = couch.get(url)
  json = JSON.parse(res.body)
  rev = json['_rev']
  couch.delete("#{url}?rev=#{rev}")
rescue
  print $! 
  print "[info] delete operation was canceled.\n"
end

couch.put(url,"{}") ## create new document
res = couch.get(url)
json = JSON.parse(res.body)
rev = json['_rev']

## prepare the json data structure
json = Hash.new
json['name'] = "J村太郎"
json['_rev'] = rev
couch.put(url, json.to_json) ## update document now.

## check contents of the document
json = JSON.parse(couch.get(url).body)
json.each do |k,v|
  print "#{k}\t: #{v}\n"
end

2010/11/09追記:
couch.put()のURLに不要な _rev=#{rev} を含めていたので修正しました。

困ったところ

JSONに慣れていないこともあって、Rubyを使っていろいろなデータをCouchDBに格納していたら困った事が起きました。

Rubyのto_jason_rawメソッドについて

RubyのJSONモジュールには、String型にto_json_rawというメソッドが準備されています。

Ruby JSON Implementationによれば、このメソッドはUTF-8ではない文字コードを格納するために使うべし、となっています。

しかし、このraw_encodedなJSON表現をDocumentとして格納してしまうと、CouchDBとRubyとの組み合せでは、あまり嬉しうない事になります。

to_json_rawを新規ドキュメントに

#!/usr/local/bin/ruby1.9 -I.
# -*- encoding: UTF-8 -*-

require 'couchdb'
require 'json'

couch = Couch::Server.new("localhost", "5984")
url = "/example/update_problem_doc"
begin
  res = couch.get(url)
  json = JSON.parse(res.body)
  rev = json['_rev']
  couch.delete("#{url}?rev=#{rev}")
rescue
  print $! 
  print "[info] delete operation was canceled.\n"
end

json = '{"name":"自営村次郎","age":"20"}'
p json.to_json_raw
couch.put("#{url}", json.to_json_raw)

json = JSON::Parser.new(couch.get(url).body).parse
JSON.parse(json).each do |k,v|
  print "#{k}\t: #{v}\n"
end

このコードを一番最初に実行した結果は次のようになります。

404:Object Not Found
METHOD:GET
URI:/example/update_problem_doc
{"error":"not_found","reason":"deleted"}
[info] delete operation was canceled.
"{\"json_class\":\"String\",\"raw\":[123,34,110,97,109,101,34,58,34,232,135,170,229,150,182,230,157,145,230,172,161,233,131,142,34,44,34,97,103,101,34,58,34,50,48,34,125]}"
name	: 自営村次郎
age	: 20

この時点で、通常はDocumentを特定するための _id_rev も表示されるはずですが、内容物だけに変換されています。

調べてみましたが、元々複数のデータを含むJSON表記を一組のJSON表記に変換してしまう特殊表現なので、 json_classraw フィールド以外が消えてしまうのは仕様なようです。

JSONを扱う際のRuby 1.9と1.8の違いについて

Ruby 1.8の時代にEUC-JPなどのデータをJSONで扱う場合には、to_json_rawメソッドを使ったのかもしれませんが、Ruby 1.9では不要です。

Ruby 1.9を使う限りはコードをEUC-JPで書いても、ハッシュを.to_jsonで変換する段階でUTF-8に適切に変換してくれます。 ハッシュに変換する時にはUTF-8からEUC-JPに変換もしてくれるので、ファイルのencodingを正しく設定していれば困る事はないでしょう。

ネットワーク通信を行なう時には、デフォルトエンコーディングがファイルのエンコーディングになりますが、それでもEncodingクラスを使えば大丈夫そうです。テストはしていないので憶測ですが、きっと大丈夫。

添付クラスやメソッドの違い以外に、Ruby 1.8と1.9の違いを意識することはないのですが、m17n周りについてはRuby 1.9をもう少し勉強する必要と意義がありそうです。

0 件のコメント: