RubyOnRails を使ってみる 【第 9 回】 Rails で XML-DB にチャレンジ

著者:かん つねのり as モバイル通信兵

はじめに

ジュンク堂書店のコンピュータコーナーを歩いていると、平積みされている本に見覚えのある名前を見かけました。 手にとって開いてみると、一人は DB2 の大家、もう一人は IBM のエバンジェリスト様ぢゃないですか!(-_-)!。 内容は XML データベース (以下、XML-DB) とその照会言語である XQuery についてでした。 筆者は DB2 の仕事が長かったので、DB2 が XML に対応したことは知っており、勉強するいい機会でしたので、そのままレジへ直行しました。 ただ、本に書いてあるとおりにやっても面白くないと思い、Rails 版を作ってみようと思ったのが、この企画のきっかけです (Java ではやる気が出なかったのもあります)。

XML はそれに対する賛否両論はあるにせよ、普及してきているのは事実です。 普及しているのであれば、それに対応できるよう間口を広くしておくのは悪くない考えだと思います。 Ruby on Rails は DB を扱う Web アプリケーション開発者にとって福音です。 そして、XML-DB を扱えれば、Ruby on Rails の可能性はさらに広がっていくと思います。

この記事では Rails から DB2 にアクセスして XML を処理することを主体に書いています。 よって、XML や DB2 については紹介程度しか記述していません。 しかし、それらの知識なしでは、実際に開発するのは困難でしょう。 その場合は「XQuery+XML データベース入門」(日経 BP 社) を読まれることをお薦めします。 では、Ruby と Rails が発展していくことを願って、Let's enjoy, Yourself! (^o^)

XML について

特徴

XML とは「拡張可能 (eXtensible) なタグで印し付け (Markup) した言語 (Language)」という意味で、表記法です。

<?xml version="1.0" encoding="UTF-8"?>
<Recipe number="0">
    <recipename>ホワイトレディー</recipename>
    <style>ショート</style>
    <glass>カクテルグラス</glass>
    <ingredients>
        <ingredient>
            <name>ドライジン</name><measure unit="proportion">2/4</measure>
        </ingredient>
        <ingredient>
            <name>ホワイトキュラソー</name><measure unit="proportion">1/4</measure>
        </ingredient>
        <ingredient>
            <name>レモンジュース</name><measure unit="proportion">1/4</measure>
        </ingredient>
    </ingredients>
    <instruments>シェーカー</instruments>
    <procedure>材料をシェーカーに入れて、シェークする。</procedure>
    <taste>酸味</taste>
    <taste>甘み</taste>
    <strength>strong</strength>
    <photo>whitelady.jpg</photo>
</Recipe>

カクテルのレシピが XML の表記法に従って記述されています。 このようなデータを XML 文書といいます。 見て分かるようにテキストで記述され、要素 (カクテルの名前とかグラス) は名前を振ったタグで前後を区切られています。 また、タグを入れ子にすることによって階層化して表現することが可能です (カクテルの材料)。 要素を繰り返してもかまいません (カクテルのテイスト)。 どのようなタグで印し付けするかは決められておらず、ユーザが任意で決めることが可能です。

用途

XML の用途としては以下のものがあります。

  • ワープロなどドキュメントの保存形式 (ODF は XML ベースのフォーマット)
  • 企業間のデータ交換の形式 (ebXML (商取引), HL7 version 3.0 (電子カルテ))
  • コンピュータ間の連携 (SOAP, RSS)

ODF はオープンソースのオフィスソフト OpenOffice.org で使われていますし、RSS はブログの更新情報を配信するフォーマットとしておなじみです。

利点

見やすい

データがバイナリではなくテキストであるため、テキストエディタで参照・更新が可能です。

構造化データを表現可能

タグを入れ子にすることによって階層化を表現するため、ツリー形式の構造化データを文字列のみで表現できます。 CSV 等の表形式では表現しにくい形式です。

構造が理解しやすい

データそのものに属性名を振ったタグがあり、それがデータの構造に合わせて配置されているため、自己記述型で構造が理解しやすくなります。 CSV のようにデータだけですと、属性を定義しているものがないと理解しにくくなります (純粋なデータという意味で CSV に列名の行がないという前提ですが)。

変化に柔軟に対応

構造 (親子関係・項目の数など) が変化しても、データのタグを増やしたり入れ子にすればいいだけなので、柔軟に対応できます。 リレーショナルデータベース (以下、RDB) の場合、列の追加が必要になったり、テーブル設計の変更を迫られる場合があります。

XML-DB について

特徴

XML-DB は XML 文書を格納することができるデータベースです。 照会言語として XPath, XQuery で検索できるものが主流になりつつあります。

利点

XML 文書の一番簡単な管理の方法は、ひとつずつテキストファイルで管理することです。 しかし、これでは XML 文書の数が増えてくるにつれて、欲しい XML 文書を探すのに手間と時間がかかります (これは XML 文書に限らず、データ一般に通じます)。 効率的かつ高速に XML 文書を管理したいというニーズを元に、XML に対応したデータベースが登場しました。 XML-DB の利点は以下になります (RDB の利点と似ています)。

効率的な照会

XPath, XQuery といった照会言語を利用して XML 文書を照会するため、プログラマが独自に照会プログラムを開発する必要がなく、API (アプリケーション・プログラミング・インターフェース) を定型化でき、効率化を図れます。

高速な照会

RDB と同様、データベース管理システム (以下、DBMS) の機能で高速な照会が可能になります。 検索条件に指定する要素にインデックスを付与することにより、照会時にインデックスを利用できます。 また、オプティマイザによって、XML 文書の構造や件数に応じたコストの低いアクセスパスが選択されます。 パフォーマンスチューニング作業の多くが、プログラマからデータベース管理者の仕事になります (データベース管理者がいればの話ですが)。

種類

XML-DB は開発された経緯から大きく 2 つの種類に分かれます。

ネイティブ XML データベース

XML 文書を扱うために新規開発されたデータベースです。 Tamino, NeoCore, TX1 などがあります。

ハイブリッドデータベース

元々 RDB から派生し、XML 文書も格納できるようにしたデータベースです。 Oracle, SQLServer, DB2 などがあります。

使用するソフトウェア

Ruby

説明不要ですね (^_^;)。

Ruby on Rails

今日の主役です (^o^)。

DB2 V9.1 Express-C

開発者・パートナー・ユーザが無償で評価・開発・使用できる DB2 のエディションです。 実績のある中小・中堅企業向けの DB2 Express Edition for Linux and Windows とほぼ同等の機能が提供されています。 DB2 V9.1 は、XML データとリレーショナル・データの両方を管理するハイブリッドデータベース管理システムです。

DB2 on Rails

DB2 用の Rails アダプタです。 無償でダウンロードできます。

REXML

100% Ruby で実装された XML パーサです。

インストール

環境としては Windows と Linux を選択することができますが、ここでは Windows 環境でのインストールについて記述します。

DB2 V9.1 Express-C

雑誌の付録か IBM のダウンロードサイトからインストーラを入手してください (DB2 on Rails をダウンロードすると DB2 (344MB) も一緒にダウンロードされますが、DB2 の導入ウィザードが英語で、稼働確認もしていないので推奨しません。 二度手間ですが、本の付録か日本 IBM が案内する正式サイトからのインストールを推奨します)。 インストーラを起動するとウィザードが起動します。 DB2 はマルチリンガル対応ですので日本語が表示されます。 導入フォルダを慣例的に "C:\sqllib" にすること以外は、デフォルトで問題ないと思います。 導入するだけなら特に難しいことはありません。

Ruby

Ruby が "C:\Ruby" にインストールされていないと、DB2 on Rails をインストールさせてもらえません。 よって、Instant Rails が使えませんでした (導入フォルダを変更すればいいだけかもしれませんが)。 筆者は One Click Ruby Installer を使いました。 また、DB2 on Rails の前提条件にバージョンが 1.8.4 と指定されていたので、それ以降のバージョンで正常稼働するかどうかは確認していません (多分、大丈夫とは思いますが (^_^;))。

Ruby on Rails

バージョンに 1.1.6 が指定されています。 導入用バッチプログラムにはご丁寧に "call gem install rails --version 1.1.6 --include-dependencies" と書かれています。 Rails は 1.2.x がリリースされていますが DB2 on Rails が正常稼働するかどうかは確認していません (多分、だめなような気がします (>_<))。

DB2 on Rails

ダウンロードサイトからインストーラを入手してください。 インストーラを起動するとウィザードが起動しますので、それに従ってインストールしてください。

環境設定

データベースの生成

ここからは若干 DB2 の知識があると助かります。 スタートメニューから IBM DB2 DB2COPY (デフォルト) →コマンド行ツール→コマンドウィンドウを選択すると、DOS 窓と似たウィンドウが開きます。 これをコマンドウィンドウといい、DB2 コマンドが入力可能です (筆者の周りには GUI 管理ツールを使う人がいませんでした)。 以下のコマンドを入力して DB2 を始動させましょう。

db2start

DB2 のサービスが稼働してなければ、

SQL1063N  DB2START の処理が正常に終了しました。

と表示されますし、もし稼働していれば、

SQL1026N  データベース・マネージャはすでにアクティブになっています。

と表示されます。 Windows のサービスからでも DB2 の始動と停止は可能です。

以下のコマンドを入力してデータベースを作成してください。

db2 create database softd_d on c using codeset utf-8 territory jp

これで softd_d という名前のデータベースが作成できました。

Rails アプリケーションの生成

以下のコマンドを入力して Rails アプリケーションを生成します。

rails softdir
cd softdir
ruby script/generate controller Bar index
ruby script/generate model Cocktail

ソースコード

ソースコードは以下からダウンロードしてください。 softdir.zip

REXML の設定

REXML を使えるようにするため、app\controllers\application.rb を編集します。

require 'rexml/document'

# REXML を include すれば、"REXML::"のプレフィックスを省略できる。
include REXML

class ApplicationController < ActionController::Base
  before_filter :specification_encode

  def specification_encode
    @headers["Content-Type"] = "text/html; charset=UTF-8"
  end

end

データベース設定ファイル

config\database.yml を以下のように編集します (但し、ユーザ名とパスワードは導入時に指定したものを設定してください)。

development:
  adapter: ibm_db2
  database: softd_d
  username: db2admin
  password: db2admin
  schema:
  encoding: utf8

テーブルの生成

モデルを生成した時点で db\migrate\00x_create_cocktails.rb というファイルが作成されているので、次のように編集します。

class CreateCocktails < ActiveRecord::Migration
  def self.up
    create_table :cocktails do |t|
       t.column "recipe", :xml
       t.column "stop_flg", :boolean, :default => false, :null => false
       t.column "created_on", :datetime, :null => false
       t.column "updated_on", :datetime
    end
  end

  def self.down
    drop_table :cocktails
  end
end

XML を格納する列の属性を "xml" と指定するのがポイントです。 編集が終わったら、以下のコマンドでテーブルを生成します。

rake db:migrate

できあがったテーブルは、次の create 文を実行したものと同じです。

CREATE TABLE "DB2ADMIN"."COCKTAILS"  (
		  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (
		    START WITH +100
		    INCREMENT BY +1
		    MINVALUE +100
		    MAXVALUE +2147483647
		    NO CYCLE
		    CACHE 20
		    NO ORDER ) ,
		  "RECIPE" XML ,
		  "STOP_FLG" SMALLINT NOT NULL WITH DEFAULT 0 ,
		  "CREATED_ON" TIMESTAMP NOT NULL ,
		  "UPDATED_ON" TIMESTAMP )
		 IN "USERSPACE1" ;
ALTER TABLE "DB2ADMIN"."COCKTAILS"
	ADD PRIMARY KEY
		("ID");

上記のテーブル定義は DB2 コマンドウィンドウより次のコマンドで取得できます。

db2look -d softd_d -e -o softd_d.ddl -i db2admin -w db2admin

データのインポート

まず、"recipe" というフォルダを作成し、その中にインポートする XML 文書をレコード単位にテキストエディタで作成します (サンプルファイル名は recipe0.xml)。

<?xml version="1.0" encoding="UTF-8"?>
<Recipe number="0">
    <recipename>ホワイトレディー</recipename>
    <style>ショート</style>
    <glass>カクテルグラス</glass>
    <ingredients>
        <ingredient>
            <name>ドライジン</name><measure unit="proportion">2/4</measure>
        </ingredient>
        <ingredient>
            <name>ホワイトキュラソー</name><measure unit="proportion">1/4</measure>
        </ingredient>
        <ingredient>
            <name>レモンジュース</name><measure unit="proportion">1/4</measure>
        </ingredient>
    </ingredients>
    <instruments>シェーカー</instruments>
    <procedure>材料をシェーカーに入れて、シェークする。</procedure>
    <taste>酸味</taste>
    <taste>甘み</taste>
    <strength>strong</strength>
    <photo>whitelady.jpg</photo>
</Recipe>

XML 文書を作成するのが面倒であれば、「XQuery+XML データベース入門」(日経 BP 社) にサンプルが付いていますので、それをご利用ください。

次に、インポートする XML 文書以外のデータをテキストエディタで作成します (サンプルファイル名は recipe.del)。

"0",<XDS FIL='Recipe0.xml'/>,0,"2006-11-24-22.42.08.000000","2006-11-24-22.42.08.000000"

以下のコマンドで DB2 に接続し、インポートします (recipe フォルダと同じ階層で実行してください)。

db2 connect to softd_d user db2admin using db2admin
db2 import from recipe\recipe.del of del xml from recipe modified by identityignore replace into cocktails

select して正しくインポートされているかを確認してください。

db2 select * from cocktails

scaffold

scaffold して Rails からテーブルにアクセスできるかを確認しましょう。

ruby script/generate scaffold Cocktail
ruby script/server

ブラウザからテーブルを参照できるかを確認してください。

XQuery による照会

コマンドによる照会 (1)

コードを実装する前に、まずコマンドで、一覧を表示する簡単な XQuery について説明しましょう。

db2 "xquery for $recipe in db2-fn:xmlcolumn('COCKTAILS.RECIPE') order by fn:abs($recipe//@number) return <Recipe> {$recipe//@number,$recipe//recipename/text()} </Recipe>"

$recipe〜('COCKTAILS.RECIPE') で XML 文書が格納されている列を指定しています。 order by は SQL 同様に出力順序を指定しています。 return〜は照会したい要素を指定します。

--------------------------------------------------------------------
<Recipe number="0">ホワイトレディー</Recipe>
<Recipe number="1">マンハッタン</Recipe>
<Recipe number="2">ドライマンハッタン</Recipe>
<Recipe number="3">ニューヨーク</Recipe>
<Recipe number="4">ゴッドファーザー</Recipe>

番号順に番号とカクテルの名前が出力されます。

Rails におけるサンプルコード (1)

先ほどのコマンドをコードに実装すると、次のようになります。

 # File app/controllers/bar_controller.rb, line 19
19:   def index
20:     @cocktails = Cocktail.recipe_items
21:   end

 # File app/models/cocktail.rb, line 3
3:   def self.recipe_items
4:     find_by_sql(
5:       "xquery
6:         for $recipe in db2-fn:xmlcolumn('COCKTAILS.RECIPE')
7:         order by fn:abs($recipe//@number)
8:         return <Recipe> {$recipe//@number,$recipe//recipename/text()} </Recipe>" )
9:   end

コントローラからモデルを呼んで、find_by_sql を使います。 find_by_sql は本来 SQL を直書きしたいときに使うものですが、SQL 文の代わりに XQuery 文を書いています。 照会結果をビューに渡して、REXML でパースして表示させています (index.rhtml)。

<h1>カクテルリスト</h1>

<table cellpadding="5" cellspacing="0">
  <tr>
    <th>No.</th>
    <th>カクテル</th>
  </tr>
<% for cocktail in @cocktails %>
  <tr valign="top" class="ListLine<%= cycle(0, 1) %>">
    <% doc = Document.new cocktail["1"] %>
    <td><%=h num = XPath.match(doc,"//@number")[0].value %></td>
    <td width="60%"><%=h XPath.match(doc,"//text()")[0].value %></td>

    <td><%= link_to 'レシピ詳細', :action => 'show_by_ingredient', :id => num %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to '新しいカクテル', :action => 'new' %>

コマンドによる照会 (2)

次はもう少し複雑な XQuery 文をコマンドで出してみましょう。

db2 "xquery let $ingnames := fn:distinct-values(db2-fn:xmlcolumn('COCKTAILS.RECIPE')//ingredient/name/text()) for $ing in $ingnames order by $ing return <Ingredient>{$ing ,<Recipes>{ for $recipe in db2-fn:xmlcolumn('COCKTAILS.RECIPE')[//ingredient/name = $ing] return <Recipe> {$recipe//@number, $recipe//recipename/text()}</Recipe> } </Recipes> }</Ingredient>"

これはカクテルの材料ごとにカクテルの番号と名前を照会する XQuery 文です。 let $ingnames〜で材料の一覧を変数にセットしています。 for $ing in $ingnames〜で材料ごとにループします。 [//ingredient/name = $ing] で材料に一致する検索条件としています。

--------------------------------------------------------------------
<Ingredient>アプリコットブランデー<Recipes><Recipe number="15">パラダイス</Recipe></Recipes></Ingredient>
<Ingredient>アマレット<Recipes><Recipe number="4">ゴッドファーザー</Recipe><Recipe number="11">フレンチコネクション</Recipe></Recipes></Ingredient>
<Ingredient>アロマチックビターズ<Recipes><Recipe number="1">マンハッタン</Recipe><Recipe number="2">ドライマンハッタン</Recipe></Recipes></Ingredient>

材料ごとに番号とカクテルの名前が出力されます。

Rails におけるサンプルコード (2)

このコマンドをコードに実装すると次のようになります。

 # File app/controllers/bar_controller.rb, line 23
23:   def ingredient
24:     @cocktails = Cocktail.ingredient_items
25:   end

 # File app/models/cocktail.rb, line 26
26:   def self.ingredient_items
27:     find_by_sql(
28:       "xquery
29:         let $ingnames := fn:distinct-values(db2-fn:xmlcolumn('COCKTAILS.RECIPE')//ingredient/name/text())
30:         for $ing in $ingnames
31:         order by $ing
32:         return <Ingredient>{$ing ,<Recipes>{
33:           for $recipe in db2-fn:xmlcolumn('COCKTAILS.RECIPE')[//ingredient/name = $ing]
34:           return <Recipe> {$recipe//@number, $recipe//recipename/text()}</Recipe> } </Recipes> }</Ingredient>" )
35:   end

XQuery 文は複雑ですが、コードの書き方自体は最初と変わりません。 コントローラからモデルを呼んで、find_by_sql から XQuery 文を書いています。 その後、ビューで XML をパースしています (ingredient.rhtml)。

<h1>材料別カクテルリスト</h1>
<% for cocktail in @cocktails %>
  <% doc = Document.new cocktail["1"] %>

  <h2><%=h XPath.match(doc,"//text()")[0].value %></h2>

  <% x = XPath.match(doc,"//Recipes/Recipe/@number") %>
  <% y = XPath.match(doc,"//Recipes/Recipe/text()") %>

  <table>
    <tr>
      <th>No.</th>
      <th>カクテル</th>
    </tr>
    <% x.zip(y){|a,b| %>
    <tr>
      <td> <%=h a %></td>
      <td> <%=h b %></td>
      <td><%= link_to 'レシピ詳細', :action => 'show_by_ingredient', :id => a %></td>
    </tr>
    <% } %>
  </table>

<% end %>
<%= link_to 'カクテル一覧', :action => 'index' %>

XML 関数による照会

コマンドによる照会

次は通常の SQL 文で where 条件に xmlexists 関数を使った例です。

db2 select id,recipe from cocktails where xmlexists('$r/Recipe[@number = \"0\"]' passing COCKTAILS.RECIPE as \"r\")"

xmlexists で number 属性が "0" のものを条件にしています。 passing〜で XML 文書の列を指定しています。

--------------------------------------------------------------------
180 <Recipe number="0"><recipename>ホワイトレディー</recipename><style>ショート</style><glass>カクテルグラス</glass><ingredients><ingredient><name>ドライジン</name><measure unit="proportion">2/4</measure></ingredient><ingredient><name>ホワイトキュラソー</name><measure unit="proportion">1/4</measure></ingredient><ingredient><name>レモンジュース</name><measure unit="proportion">1/4</measure></ingredient></ingredients><instruments>シェーカー</instruments><procedure>材料をシェーカーに入れて、シェークする。</procedure><taste>酸味</taste><taste>甘み</taste><strength>strong</strength><photo>whitelady.jpg</photo></Recipe>

id 列と XML 文書が格納されている recipe 列が出力されます。

Rails におけるサンプルコード

コードに実装すると次のようになります。

 # File app/controllers/bar_controller.rb, line 27
27:   def show_by_ingredient
28:     @cocktail = Cocktail.ingredient_to_recipe(params[:id])
29:     render :template => 'bar/show'
30:   end

 # File app/models/cocktail.rb, line 11
11:   def self.ingredient_to_recipe(id)
12:     rs = find_by_sql([
13:       "select * from cocktails
14:         where xmlexists('$r/Recipe[@number = \"?\" ]'
15:                 passing COCKTAILS.RECIPE as \"r\")" ,id.to_i ])
16:     return rs[0]
17:   end

カクテルの一覧から任意のカクテルをクリックすると、ビューからカクテル番号をパラメータとして受け取ります。 それを引数としてモデルに渡します。 find_by_sql を使いますが、照会結果がユニークである前提のため、配列の 1 つめのみを戻り値にしています。 その後、ビューで XML をパースしています (show.rhtml)。

<h1>カクテルのレシピ</h1>

<div id="mydiv"><p><%= link_to_remote 'スキーマを確認', :update => 'mydiv', :url => {:action => 'va lidate', :id => @cocktail} %></p></div>
<% doc = Document.new @cocktail.recipe %>
<p><b>No.</b> <%=h XPath.match(doc,"/Recipe/@number")[0].value %></p>
<p><b>カクテル:</b> <%=h XPath.match(doc,"//recipename/text()")[0].value %></p>
<p><b>スタイル:</b> <%=h XPath.match(doc,"//style/text()")[0].value %></p>
<p><b>グラス:</b> <%=h XPath.match(doc,"//glass/text()")[0].value %></p>

<% x = XPath.match(doc,"//ingredient/name/text()") %>
<% y = XPath.match(doc,"//ingredient/measure/text()") %>
<% z = XPath.match(doc,"//ingredient/measure/@unit") %>

<p><b>材料</b></p>
<table>
<% x.zip(y,z){|a,b,c| %>
<tr>
<td><b>名前:</b> <%=h a %></td>
<td><b>分量:</b> <%=h b %></td>
<td><b>単位:</b> <%=h c %></td>
</tr>
<% } %>
</table>

<p><b>道具:</b> <%=h XPath.match(doc,"//instruments/text()").collect{|attr| attr.value}[0] %></p>
<p><b>作り方:</b> <%=h XPath.match(doc,"//procedure/text()").collect{|attr| attr.value}[0] %></p>

<% for taste in XPath.match(doc,"//taste/text()") %>
<p><b>味:</b> <%=h taste %></p>
<% end %>

<p><b>強さ:</b> <%=h XPath.match(doc,"//strength/text()")[0].value %></p>
<p><b>写真:</b> <%=h XPath.match(doc,"//photo/text()")[0].value %></p>

<p><b>中止フラグ:</b> <%=h @cocktail.stop_flg %></p>
<p><b>作成日時:</b> <%=h @cocktail.created_on.strftime("%Y/%m/%d %H:%M:%S") %></p>
<p><b>更新日時:</b> <%=h @cocktail.updated_on.strftime("%Y/%m/%d %H:%M:%S") %></p>

<%= link_to '修正', :action => 'edit', :id => @cocktail %> |
<%= link_to 'カクテルリスト', :action => 'index' %>

注意する点としては、「道具」と「作り方」の要素の取得方法が他と違っています。 通常の要素は、XPath.match(doc,"//recipename/text()")[0].value で取得しています。 しかし、これですと XML 文書が XML スキーマに準拠していない場合、具体的には要素のタグがない場合にエラーになります。 今回使っている XML 文書は、「道具」と「作り方」が XML スキーマに準拠していないものがあります。 よって、準拠していないと想定される要素については、XPath.match(doc,"//instruments/text()").collect{|attr| attr.value}[0] で取得しています。 XML スキーマに準拠していない XML 文書の例を提示します。

<?xml version="1.0" encoding="UTF-8"?>
<Recipe number="4">
    <recipename>ゴッドファーザー</recipename>
    <style>ロック</style>
    <glass>オールドファッションドグラス</glass>
    <ingredients>
        <ingredient>
            <name>ウィスキー</name><measure unit="proportion">3/4</measure>
        </ingredient>
        <ingredient>
            <name>アマレット</name><measure unit="proportion">1/4</measure>
        </ingredient>
    </ingredients>
    <procedure>材料をグラスに直接入れて軽くステアする。</procedure>
    <taste>甘み</taste>
    <strength>strong</strength>
    <photo>godfather.jpg</photo>
</Recipe>

要素「道具」のタグである <instruments> が存在しません。 XPath.match(doc,"//instruments/text()")[0].value で取得するとエラーになります。

XML 関数による検証

XML スキーマとは

先の XML 文書についての説明では、タグに振る名前や要素の数をユーザが自由に設定できると言いました。 しかし、文書構造に決めごとがないとデータ交換するのに不便な場合があります。 また、XML 文書の内容を一定のルールに従わせたい場合もあります。 XML では XML 文書のルールを定義することができ、定義内容を XML スキーマといいます。 XML スキーマの形式には DTD (Document Type Definition) と XML Schema の 2 種類がありますが、XML で記述されている XML Schema が支持されてきています。 サンプルアプリケーションも XML Schema を使っています。

XML Schema の定義

次は XML Schema でカクテルの XML 文書についてルールを定義しています。

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="instruments" type="xsd:string"/>
  <xsd:element name="glass" type="xsd:string"/>
  <xsd:element name="Recipe">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element ref="recipename" minOccurs="1"/>
        <xsd:element ref="style"/>
        <xsd:element ref="glass"/>
        <xsd:element ref="ingredients" minOccurs="1"/>
        <xsd:element ref="instruments"/>
        <xsd:element ref="procedure" minOccurs="1"/>
        <xsd:element maxOccurs="unbounded" ref="taste"/>
        <xsd:element ref="strength"/>
        <xsd:element ref="photo"/>
      </xsd:sequence>
      <xsd:attribute name="number" type="xsd:string"/>
      |
      中略
      |
</xsd:schema>

instruments や glass は string 属性であることや、recipename や ingredients は最低 1 つ必要であることを記述してあります。 DB2 では XML Schema の定義ファイルを以下のコマンドで定義することができます。

db2 register xmlschema http://recipe from recipe.xsd as xsr.recipe
db2 complete xmlschema xsr.recipe

recipe.xsd が XML Schema の定義ファイル名です。 xsr.recipe という名前で DB2 で管理されます。

コマンドによる検証

xmlvalidate 関数を使った照会と、XML スキーマに準拠している場合の照会結果です。

db2 select xmlvalidate(recipe according to xmlschema id xsr.recipe) from cocktails where id = 180
--------------------------------------------------------------------
<Recipe number="0"><recipename>ホワイトレディー</recipename><style>ショート</style><glass>カクテルグラス</glass><ingredients><ingredient><name>ドライジン</name><measure unit="proportion">2/4</measure></ingredient><ingredient><name>ホワイトキュラソー</name><measure unit="proportion">1/4</measure></ingredient><ingredient><name>レモンジュース</name><measure unit="proportion">1/4</measure></ingredient></ingredients><instruments>シェーカー</instruments><procedure>材料をシェーカーに入れて、シェークする。</procedure><taste>酸味</taste><taste>甘み</taste><strength>strong</strength><photo>whitelady.jpg</photo></Recipe>
1 レコードが選択されました。

通常の照会結果と変わりません。 しかし、XML スキーマに準拠していない場合は、以下の照会結果になります。

db2 select xmlvalidate(recipe according to xmlschema id xsr.recipe) from cocktails where id = 184
--------------------------------------------------------------------

SQL16196N  XML 文書に正しく指定されていない要素 "procedure"が含まれています。理由コード = "31"。  SQLSTATE=2200M

データが select されず、メッセージが返ってきます。 これを insert や update の SQL 文に応用すると、XML スキーマに準拠していない XML 文書をデータベースに登録させないようにすることも可能です。

Rails におけるサンプルコード

XML スキーマによる検証をコードに実装すると次のようになります。

 # File app/controllers/bar_controller.rb, line 52
52:   def validate
53:     if Cocktail.validate_recipe?(params[:id])
54:       flash[:notice] = 'スキーマに準拠しています(^_^)v'
55:     else
56:       flash[:notice] = 'スキーマに準拠していません(T_T)'
57:     end
58:       render :layout => false
59:   end

 # File app/models/cocktail.rb, line 19
19:   def self.validate_recipe?(id)
20:     rs = find_by_sql([
21:       "select xmlvalidate(recipe according to xmlschema id xsr.recipe) from cocktails
22:         where id = ?",id.to_i ])
23:     return rs[0]
24:   end

前章の XML 関数による照会と同様、カクテル番号を引数にしてモデルに渡します。 照会結果にデータがセットされているかどうかを判断基準にしています。

データの登録・更新

XML 文書の登録・更新については変わったものはありません。 XQuery にしても XML 関数にしても今のところ検索・参照機能しかありません。 よって、ビューから受け取った項目を REXML で XML 文書を作成して、ActiveRecord の通常のやり方で登録・更新します。

Rails におけるサンプルコード

更新のコードは次のようになります。

    # File app/controllers/bar_controller.rb, line 40
40:   def update
41:     y = params[:cocktail]
42:     y["recipe"] = recipe_create(params[:recipe_xml])
43:     @cocktail = Cocktail.find(params[:id])
44:     if @cocktail.update_attributes(y)
45:       flash[:notice] = 'Cocktail was successfully updated.'
46:       redirect_to :action => 'show', :id => @cocktail
47:     else
48:       render :action => 'edit'
49:     end
50:   end

XML に格納するデータとそうでないデータを別々のハッシュにしてコントローラにデータを渡しています。 XML のハッシュから以下のメソッドによって XML 文書を生成し、cocktail ハッシュの recipe キーに設定します。 後は ActiveRecord の通常のやり方で更新します。

    # File app/controllers/bar_controller.rb, line 63
63:   def recipe_create(x)
64:     doc = REXML::Document.new()
65:     doc << REXML::XMLDecl.new('1.0','UTF-8')
66:
67:     e0 =  REXML::Element.new('Recipe')
68:     e0.attributes['number'] = x['number']
69:     e0.add_element('recipename').add_text(x['recipename'])
70:     e0.add_element('style').add_text(x['style'])
71:     e0.add_element('glass').add_text(x['glass'])
72:     e1 = e0.add_element('ingredients')
73:     s = x['name'].values
74:     t = x['measure'].values
75:     u = x['unit'].values
76:     s.zip(t,u){|a,b,c|
77:       e2 = e1.add_element('ingredient')
78:       e2.add_element('name').add_text(a)
79:       e2.add_element('measure', {'unit' => c}).add_text(b)
80:     }
81:     e0.add_element('instruments').add_text(x['instruments'])
82:     e0.add_element('procedure').add_text(x['procedure'])
83:     x['taste'].each{ |key,value| e0.add_element('taste').add_text(value)}
84:     e0.add_element('strength').add_text(x['strength'])
85:     e0.add_element('photo').add_text(x['photo'])
86:
87:     doc << e0
88:     return doc.to_s
89:   end

階層を下げて要素を設定するために、add_element した要素を変数に設定しています (e1 と e2)。 最後に、できた XML 文書 (doc) を文字列にして戻り値にしています。

おわりに

今回、サンプルアプリケーションを作っていて、以下のことを感じました。

XML-DB について

Rails で XML-DB を扱うには、RDB と XML との両方が処理できるハイブリッドデータベースが使いやすいです。 当たり前ですが Rails は RDB を簡単に使える仕組みを提供しているため、RDB を派生させたハイブリッドデータベースはその延長線上で利用できます。 実際、Rails 用のアダプタがあるのは RDB しかなく、ネイティブ XML データベースは今のところ利用できません。

パースについて

パース (解析) 等の XML の処理を REXML で行うか、DB2 で行うかが検討事項です。 どちらでもパースはできるのですが、一長一短です (言い方を変えれば、逃げ手はあると言うことですが)。

ビューについて

今回、ビューでパースしているので見苦しいソースになってしまいました。 ビューにはできるだけロジックを入れたくないとは思っていたのですが……(>_<)。 ビューをすっきりさせたい (moriq さんにリファクタリングして貰って、大分改善はできたのですが……(^_^;))。

情報

参考図書

XQuery+XML データベース入門 (日経 BP 社)

XQuery, XML の概要、DB2 の利用方法が記載されており、Java のサンプルコードと DB2 V9.1 Express-C が入った CD-ROM が付録に付いてきます。

Ruby de XML (オーム社)

REXML の使い方が記載されています。

URL

DB2 V9.1 Express-C

DB2 Express-C の説明・ダウンロード方法を案内しています。

DB2 on Rails

モジュールのダウンロード・導入手順を案内しています。

著者について

かん つねのり

  • ハンドルネームは「モバイル通信兵」、連絡先は kan.t あっと plum.plala.or.jp
  • 兵庫県神戸市在住のフリーランス SE
  • 最近、ESB (エンタープライズ・サービス・バス) に興味がでてきて、Java と格闘中
  • 休日はフィットネスクラブでファイティングエクササイズ
更新日時:2007/03/01 07:43:34
キーワード:
参照:[Rubyist Magazine 0018 号] [0018 号 巻頭言] [各号目次] [prep-0018]