japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2009年7月4日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2008年10月10日 10:00

XQueryの制御構造の活用

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!
  • この記事をokyuuへインポート

XQueryの特徴

 以前、データベースプログラマである同僚の一人が、初期のXQueryの実装をしばらく使ってみて、この言語をニコニコ言語と評しました。それは言語のわかりやすさと関係するのかと訊いたところ、彼はハンガリー訛りの英語でこう答えました。「まさか、言語自体は難物だよ。だけど、ニコニコマーク (: :) がコメントの区切り記号になっているから、たとえデータベースがずたずたでもニコニコしてしまうわけさ」。

 彼の意見はこの言語をあからさまに非難するものではありませんが、XQueryについてのある事実、つまりXQueryの構造は一般の言語に似ているが、その違いの部分で大きくつまずく場合があるということをよく示しています。XQueryは習得の困難な言語ではありませんが、どうしてうまく動かないか知ろうという気にさせる言語でもあります。

XQueryの制御構造

 XQueryの制御構造はFLOWRという風変わりな頭字語で呼ばれてきました。これは、その言語で使われる特に重要なXQuery構造(すべてではない)の略称で、FLOWR自体は次の5つの操作を表します。

  • For
  • Let
  • Order by
  • Where
  • Return
 このうちの4つについては、SQLに類似の構造があります。

  • SELECT
  • SET
  • ORDER BY
  • WHERE
 これらの用語は集合に項目を割り当てる、または集合から項目を取り出すために使われます。

XQueryの使いどころ

 XQueryは''集合操作''言語です。単一のスカラー値よりも情報の集合を操作することに主眼が置かれます。また、集合操作言語という点で、その仕様に組み込まれているXPath 2.0言語を置き換えるよりも、それを拡張することを目指しています。実際、突き詰めていくと、XQueryの大部分はXPathの制御言語をラップする仕組みの1つに過ぎず、そのやり方はXSLTがXPathにテンプレート言語を結び付けるのとどこか似ています。

 こうした理由により、XQueryを操作するとき、この言語を最も効果的に使うには、最初にXPath 2内で可能な限り多くのことを行い、XPath 2ではもう立ち行かなくなったところでXQueryのコマンド構造を利用するという方法をとるべきです。

 たとえば、従業員レコードのコレクションで構成されるXMLデータソースがあると仮定します。現在、それらはファイル「employees.xml」に入っています(リスト1を参照)。

リスト1 Employees.xml
<?xml version="1.0" encoding="UTF-8"?>
<employees>
    <employee id="be129">
        <firstname>Jane</firstname>
        <lastname>Doe</lastname>
        <title>Engineer</title>
        <division>Materials</division>
        <building>327</building>
        <room>19</room>
        <supervisor>be131</supervisor>
    </employee>
    <employee id="be130">
        <firstname>William</firstname>
        <lastname>Defoe</lastname>
        <title>Accountant</title>
        <division>Accts Payable</division>
        <building>326</building>
        <room>14a</room>
    </employee>
    <employee id="be131">
        <firstname>Jack</firstname>
        <lastname>Dee</lastname>
        <title>Engineering Manager</title>
        <division>Materials</division>
        <building>327</building>
        <room>21</room>
    </employee>
    <employee id="be132">
        <firstname>Sandra</firstname>
        <lastname>Rogers</lastname>
        <title>Engineering</title>
        <division>Materials</division>
        <building>327</building>
        <room>22</room>
    </employee>
    <employee id="be133">
        <firstname>Steve</firstname>
        <lastname>Casey</lastname>
        <title>Engineering</title>
        <division>Materials</division>
        <building>327</building>
        <room>24</room>
    </employee>
    <employee id="be135">
        <firstname>Michelle</firstname>
        <lastname>Michaels</lastname>
        <title>COO</title>
        <division>Management</division>
        <building>216</building>
        <room>264</room>
    </employee>
</employees>
 このとき、各従業員に順にコンテキストを設定するにはfor文を使用します。次のコードは、リスト中の順番で各従業員を出力する単純なXQueryスクリプトです。

for $employee in doc("employees.xml")/employees/employee
return $employee[/s]
 この例では、従業員要素のシーケンスが、対応する各従業員の本体と共に返されます。もちろん、このシーケンスが正確にどうレンダリングされるかは、使用するXQueryの実装によって異なります。たとえば、Saxon 9のXQueryエンジンを使用した場合、出力はリスト2のようになります。

リスト2 XQueryスクリプトforSeq1.xqをSaxonで出力した例
result:sequence xmlns:result="http://saxon.sf.net/xquery-results"
                 xmlns:xs="http://www.w3.org/2001/XMLSchema"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <result:element>
      <employee id="be129">
        <firstname>Jane</firstname>
        <lastname>Doe</lastname>
        <title>Engineer</title>
        <division>Materials</division>
        <building>327</building>
        <room>19</room>
        <supervisor>be131</supervisor>
      </employee>
   </result:element>
   <result:element>
      <employee id="be130">
        <firstname>William</firstname>
        <lastname>Defoe</lastname>
        <title>Accountant</title>
        <division>Accts Payable</division>
        <building>326</building>
        <room>14a</room>
      </employee>
   </result:element>
   </result:element>
    <!-- more results -->
</result:sequence>
 一方、eXist XQueryエンジンを使用した場合は、コンテナやクロージャを持たない要素ノードのシーケンスが返されます。その主な理由は、Saxonでは出力がXMLオブジェクトになるものと仮定されるのに対し(そのために何らかのコンテナが必要となる)、XQueryではそのような仮定がないことです。クエリ全体をXMLコンテナに入れれば、このラッパー問題を回避できます。

<employee_set>{
for $employee in doc("employees.xml")/employees/employee
return $employee}
</employee_set>
 Saxonの場合、この出力はよく似ていますが、完全に同じにはなりません(リスト3を参照)。

リスト3 ForSeq2.xqの結果
<result:sequence xmlns:result="http://saxon.sf.net/xquery-results"
                 xmlns:xs="http://www.w3.org/2001/XMLSchema"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <result:element>
      <employee_set>
         <employee id="be129">
            <firstname>Jane</firstname>
            <lastname>Doe</lastname>
            <title>Engineer</title>
            <division>Materials</division>
            <building>327</building>
            <room>19</room>
            <supervisor>be131</supervisor>
         </employee>
         <employee id="be130">
            <firstname>William</firstname>
            <lastname>Defoe</lastname>
            <title>Accountant</title>
            <division>Accts Payable</division>
            <building>326</building>
            <room>14a</room>
         </employee>
          <!-- more employees -->
      </employee_set>
   </result:element>
</result:sequence>
 for $item in $seqという式は、やや誤解を招く可能性があります。基本的にfor文はシーケンス内を反復処理するので、$item変数にはシーケンス内の各項目への内部ポインタが順に渡され、その$itemのコピーが渡されることはありません。つまり、$itemコンテキスト変数は、基本となるXML(または関連する)データモデル内の構造を参照していることと、その結果がこのコンテキストに基づくシーケンスになるという点において、「ライブ(live)」であると言えます。

 たとえば次の式では、

for $employee in doc("employees.xml")/employees/employee 
    order by $employee/lastname ascending
    return $employee
 従業員のリストが従業員の姓の順に返されます。要するに、order by文はリストを指定の条件で並べ替えた仮想的なシーケンスを作ります。

for $employee in doc("employees.xml")/employees/employee order by $employee/lastname ascending return $employee
 上記のうち、太字部分の式が仮想的なシーケンスを表します。

letコマンドの利用

 誰がどう考えても、このような複雑な式を繰り返し入力するのは面倒です。幸い、letコマンドを使用して、このシーケンスを保持する一時変数を作成することができます。

let $sorted-seq := for $employee in doc("employees.xml")/employees/employee 
    order by $employee/lastname ascending 
    return $employee
 この文は少々わかりにくいかもしれません。一般のプログラミング言語と同じ感覚で、letでは単一のスカラー値しか保持できないと考えてしまうと、この文の意味がすぐにはわからないでしょう。しかし、let文ではスカラー値だけでなくシーケンスも(さらに高度なデータ構造も)保持できるということを知れば、この式の意味合いがよくわかるはずです。また、作成されたシーケンスはXML構造内の特定の要素をポイントしているので、この並べ替えられたシーケンスは実体的にはポインタのシーケンスに過ぎず、(普通は巨大となる)XML構造そのものではありません。

 これにより、段階的なフィルタリング機構を驚くほど低コストで手際よく作成することができます。たとえば、従業員を姓でソートし、そのソート済みリストのレコード11から20までを出力するような式を作成するものとします。次のコードは、その1つの方法を示しています。

let $employees :=  doc("employees.xml")/employees/employee
let $sorted-employees := for $employee in $employees 
    order by $employee/lastname ascending 
    return $employee
let $paged-employees := subsequence($sorted-employees,10,10)
return $paged-employees
 この例の各let代入文では、実際にはノードのシーケンスが処理されています。具体的には、「employees.xml」ドキュメント内のノードの初期集合、同じコンテンツのソート済みシーケンス、そしてソート済み従業員リストから取り出したサブシーケンスです。いずれのケースについても、ここでは要素のポインタだけが抽出されます。このパラダイムは効率的なクエリを作成するのに非常に有効です。XMLデータの大きなブロックを移動したりXMLデータベースの配置を変更したりしないで、ポインタのリストを操作するだけで済むため、操作が桁違いに高速化されるからです。

 ただし、このやり方には1つ注意すべき点があります。このポインタ操作が成立するためには素の結果(たとえば$employee)が返されることが前提になることです。結果を変更するようなものがあれば、それは結局新しい情報ノードとなり、たとえ無意味な変更であってもXQueryエンジンはそれらのノードを効率的に逆参照する必要があります。

 上記のコードはまったく同じ結果を返しますが([]で括られた結果は、XQueryの式を評価して、ストリーム内で結果を置き換えることを意味する)、1つ目のreturn文で新しいコンテンツが作成されるため、かなりコストがかかり、ずっと低速の操作になります。

 一般に、データセットをできるだけ小さなシーケンスに絞り込むまでは、新しいコンテンツを作成するのを控えるべきです。実際、かなり大きくなる可能性のあるデータセットに対してクエリを実行するときは、検索結果のフィルタリングと表示のために案外普通の操作パターンが使われるものです。

  1. リソースの初期コレクションを取得し、それを作業変数に格納する。
  2. このコレクションの項目をフィルタリングして、関係する項目だけを取り出す。
  3. フィルタリングしたデータセットをソートする
  4. ソート済みのフィルタリングされたデータセットをページ単位で処理して加工可能なサブセットを取り出す。
  5. ページ単位のコンテンツを出力要件に応じて変換する。
  6. 変換されたコンテンツを出力する。
 データの取得は複数の場面で起こります。doc()関数は、絶対URLか相対URLを引数に取り、URLのコンテンツをXMLドキュメントとして解析しようとします。一方、collection()関数は、外部ソースからノードのコレクションを取得します。そのコレクションに親要素が存在する必要はありません。この違いはXMLファイルを取得するときはあまり意味を持ちませんが、多くのXMLデータベースは(単一の要素ではなく、コレクションに対応するURIを用いて)コレクションの概念に基づきセットアップされているので、collection()要素はXMLデータベース内から呼び出すときに特に役立ちます。

 eXistデータベースに個々の従業員のコレクションも作成できます(具体的な手順については、ここでは述べません)。そして作成したコレクションを特定のパス(通常はdb/employeesなどの形式)に割り当て、このコレクション内のすべての項目を次のようにして参照できます。

let $employees :=  collection("/db/employees")
 XQuery for Java(xqj)表記では次のようになります。

let $employees :=  collection("xmldb:exist:///db/employees")
 ここで、xmldb:exist:///は、使用プロトコルがxmldb:で、参照先サーバーがeXistであることを示しています。また、3重のスラッシュ(///)はプロトコルの完全パスの略記法であり、通常は次のような形式になります。

let $employees :=  collection("xmldb:exist://localhost:8080/db/employees")
 内部的には、取得されたコレクションは少なくともクエリに関する限り、シーケンスとして扱われます(本稿の範囲を超えますが、アップデートでは区別されます)。つまり、コレクションの結果に対してクエリを実行するときも、ドキュメントから取り出したXPathシーケンスの結果に対してクエリを実行するときも、コンテンツを同じ方法で操作することになります。

フィルタリング

 フィルタリングとは初期コレクションを絞り込んで重要なレコードだけを処理できるようにすることを意味します。たとえば、特定の部署(たとえば、"Materials")に所属する従業員レコードだけを取り出すものとします。もちろん、この操作は取得プロセスの中で実行できます。

let $employees :=  collection("/db/employees[division = 'Materials']")
 一方、これを2つの別のステップに分けると、設計とパフォーマンスの両面でメリットがあります。

let $employees :=  collection("/db/employees")
let $filtered-employees := for $employee in $employees[division = 'Materials']" return $employee
 XQuery WHEREコマンドも使用でき、これで評価の述部が分離されます。

let $employees :=  collection("/db/employees")
let $filtered-employees := for $employee in $employees
    where $employee[division = 'Materials'] 
    return $employee
 どちらが有利でしょうか? 述部([]内の式)が比較的小さく、自己完結的なときは、シーケンスでXPathを使用した方が通常は高速です。しかし、式が複雑なとき、複数の変数が含まれているとき、あるいは、この操作にSORT BYが付随するときは、WHERE句を使用した方が通常は効率的で、可読性も高くなります。理屈上は中心となるFLOWRの各演算子があれば何とかなるはずですが、それらの式を組み合わせるだけでは手に負えないこともかなりあります。そのような場合に、IF ... THEN ... ELSE構造を使用します。

if ($condExpr) then $resultExprTrue else $resultExprFalse
 この文のthenelseにはどちらにも暗黙のreturnが関連付けられており、IF文を用いてかなり複雑なスクリプトを作成できます。たとえば、テーブルに特定セクションの従業員をリストするとき、セクションに従業員がいなければ、ステータスメッセージを表示するものとします。このようなケースでIF...THEN...ELSE文は非常に役立ちます(リスト4を参照)。

リスト4 IF文を用いた複雑なスクリプトの作成
let $employees := doc('employees.xml')/employees/employee
let $divisions := ('Materials','AcctsPayable','Operations')
let $results := <html>
    <head>
         <title>Division Roster</title>
     </head>
     <body>
{
  for $division in $divisions return
    if (not(empty($employees[division = $division]))) then
        <div>
        <h2>{$division}</h2>
        <table>
            <tr>
                <th>Last Name</th>
                <th>First Name</th>
                <th>Title</th>
            </tr>
            {for $employee in $employees[division = $division] order by $employee/lastname ascending return
                <tr>

                    <td>{string($employee/lastname)}</td>
                    <td>{string($employee/firstname)}</td>

                    <td>{string($employee/title)}</td>
                </tr>
            }
       </table>
       </div>
   else <h2>There are no employees in the {string($division)} division.</h2>
    }
    </body>
</html>
return $results    
 条件文があって、その条件が真か偽のときだけ出力を得たい場合は、空のシーケンス(()と表記)を出力に使用します。

if ($cond) then $output else ()
 条件が偽と評価されれば、空のシーケンスが返され、これは空白の出力となります。

 IF...THEN...ELSE文を結果ブロック内に入れ子にすることもできますが、複数の文を入れ子にすると、式がかなり複雑になることがあります。変数$hの値に基づいて異なるヘッダースタイルを作成することを考えます(この変数は1?6の値を取るものとします)。埋め込みのIF文を使えば、次のようなスイッチを作成できます。

let $title := "This is a test."
let $result := if ($h = 1) then <h1>{$title}</h1>
else if ($h = 2) then <h2>{$title}</h2>
else if ($h = 3) then <h3>{$title}</h3>
else if ($h = 4) then <h4>{$title}</h4>
else if ($h = 5) then <h5>{$title}</h5>
else <h6>{$title}</h6>
return $result
 あるいは、element文を利用して要素を直接作成することもできます。

let $title := "This is a test."
let $result := element {concat("h",$h)}{$title}
 element文は、最初の式を新しく作成される要素の名前として扱い、次の括弧内の式を要素のコンテンツとして扱います。このようにXQueryでは、同じ作業をいくつもの異なる方法で実行できます。

 XQueryは複雑になりがちなので、case値に基づいて特定の出力を得られる次のようなswitch文を使いたいところです。

switch($expr){
    case $expr1:  $result1
    case $expr2: $result2
    default: $fallthruResult
     }
 しかし、XQueryには単純なスイッチはありません。XQueryに用意されているのは型スイッチであり、特定の変数のデータ型に基づいてアクションを実行できます。型スイッチのアイデアは、当初、XMLドキュメント内の要素を識別し、その要素に基づき何らかの処理を行う手段として考え出されました。

typeswitch($context)
    case $a as type1 return $expr1
    case $a as type2 return $expr2
    case $a as type3 return $expr3
    default $a return $fallthruResult
 この方法はやや直観的でないかもしれませんが、例を見ればわかるでしょう。従業員レコードの各ノードに特別なフォーマットを適用するものとします。これを行うために次のようなtypeswitch制御構造を使います。

<div>
{for $employee in doc("employees.xml")/employees/employee return 
  for $node in $employee/*
  return typeswitch($node)
    case $a as element (firstname) return <span>{string($a)} </span>
    case $a as element (lastname) return <span>{string($a)}</span>
    case $a as element (title) return <div>{string($a)}</div>
    case $a as element (division) return <div><b>{string($a)}</b></div>
    default $a return <div>{string($a)}</div>
}
</div>
 typeswitchの問題は、言語のエッジケースを解決するのには有効でも、文字列トークンに基づいて異なるパスを選択するような一般的な条件選択には向いていないことです。幸い、多少手を加えることで、typeswitchを従来のswitchに近づけることができます。これはXQueryの式を用いて一時要素にトークンを返すという手法です。これにより、従業員の所属する部門に基づいて、異なる出力を生成できます(リスト5を参照)。

リスト5 XQuery式の使用
<div>{
let $employees := doc("employees.xml")/employees/employee
let $output := for $employee in $employees return 
    let $division := element {string($employee/division)}{}
    return typeswitch($division)
 case $a as element (Materials) return
    <div>
          <h1 class="materials_title">Materials Section</h1>
          <div class="name">{concat($employee/firstname,' ',$employee/lastname)}</div>
          <div class="title">{string($employee/title)}</div>
          <div class="manager">{
              let $manager := $employees[@id = string($employee/supervisor)]
              return concat($manager/firstname,' ',$manager/lastname)
              }</div>
               </div>
 case $a as element (AcctsPayable) return
    <div>
          <h1 class="acctspayable_title">Accounts Payable</h1>
          <div class="name">{concat($employee/firstname,' ',$employee/lastname)}</div>
          <div class="title">{string($employee/title)}</div>
          <div class="manager">{
              let $manager := $employees[@id = string($employee/supervisor)]
              return concat($manager/firstname,' ',$manager/lastname)
              }</div>
               </div>
default return
    <div>
        <h1>Warning!</h1>
        <p>{concat($employee/firstname,' ',$employee/lastname)} is not in a known division</p>
               </div>
return $output
}</div>
 このルーチンで重要なのは次の1行です。

    let $division := element {string($employee/division)}{}
 式element {string($employee/division)}{}は一見すると暗号のようですが、1つ1つ分解すればすぐ理解できます。最初の{}内の式はMaterialsAcctsPayableというような部門の名前となります。次の空の{}内の式は要素が空であることを示します。これで<Materials /><AcctsPayable />という形式の要素が作成されます。この要素から、さまざまなケースに展開していくことができます。次に例を示します。

case $a as element (Materials) return
  <div>
    <h1 class="materials_title">Materials Section</h1>
    <div class="name">{concat($employee/firstname,' ',$employee/lastname)}</div>
    <div class="title">{string($employee/title)}</div>
    <div class="manager">{
      let $manager := $employees[@id = string($employee/supervisor)]
      return concat($manager/firstname,' ',$manager/lastname)
      }</div>
  </div>
 この出力は資材(material)の種類に応じて適切な形式に変換されます。一致するものがないデフォルトのケースでは、特定の従業員レコードに問題があることを示す警告メッセージが発せられます。このケースでは$a変数に部門要素のポインタが設定されますが、上の例ではこれは使われておらず、caseルーチンの単なるプレースホルダーとなっています。

 XQuery 1.1作業草案では、他の制御構造についても言及されています。たとえば、GROUP BY演算子は、グループ選択子で結果を集約できるようにします。また、WINDOW句は、特定のシーケンスのサブシーケンスに対して集合操作を容易に行えるようにします。さらにXQuery Scripting Extensions(XqueryScript)では、より本格的なスクリプティングロールの中でXQueryを簡単に使えるようにするための制御構造が提供されます。しかし、これらの草案はどちらもまだ開発途上にあり、現在のところ、これらの機能をサポートする商用またはオープンソースのXQuery実装は存在しません。

XQueryと制御構造

 制御構造は必ずしも魅力的なものではありません。実際、それらは鉄筋を支える型枠のようなものですが、建築における型枠がそうであるように、XQueryアプリケーションという構造物の必須の要素でもあります。これらの構造の操作方法を理解するかどうかで、実用的で柔軟性の高いアプリケーションを作成できるか、毎回書き直すしかない1回限りのコードばかりを作成するかが決まってきます。

著者紹介

Kurt Cagle(Kurt Cagle)
ライター、情報アーキテクト、XML News NetworkとMetaphorical Webのウェブマスター。カナダ、ブリティッシュコロンビア州のビクトリア在住。
このエントリーを含むはてなブックマーク この記事をクリップ!
BuzzurlにブックマークBuzzurlにブックマーク Yahoo!ブックマークに登録
この記事をokyuuへインポート
最新トップニュース
データメーション
【データメーション】
中国が「Green Dam」フィルタ規制を撤回(7月1日)
Graphic Design Forum
【Graphic Design Forum】
Chris Dickman(6月25日)
プライバシー ジャパン・インターネットコム版
【プライバシー ジャパン・インターネットコム版】
グーグル・ストリートビューの問題について総務省の見解(6月23日)
エンジニアの独り言
【エンジニアの独り言】
システムを「使う」時代のエンジニアに求められるもの(6月2日)
最新ハイテク講座
最新ハイテク講座
電気は家庭でつくる時代へ!燃料電池「エネファーム」(7月3日)
アクセス解析で見るWebマーケティング
アクセス解析で見るWebマーケティング
決定力を探るアクセス解析(7月3日)
百式のネットビジネス研究
百式のネットビジネス研究
ファーストフードを高級っぽく盛り付けて紹介している「Fancy Fast Food」(7月3日)
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
ビデオリサーチインタラクティブ調査(月間インターネットオーディエンスデータ)(7月2日)
成約率、反応率を上げる Web 文章術
成約率、反応率を上げる Web 文章術
言葉がダイレクトにキャッシュを生む(7月2日)
不況時代の Web ビジネス最適化講座
不況時代の Web ビジネス最適化講座
アクセス解析エキスパートここだけの話、Web コンシェルジュの“勉強法”こっそり教えます(7月2日)
「Webからの脅威」―その傾向と最新対策
「Webからの脅威」―その傾向と最新対策
不正プログラムの分類(7月1日)
DevX
DevX
JavaScriptとDOMによる動的なWebページの作成(6月30日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
今のままで大丈夫?3匹の子ブタ的キャリア危険度診断(6月30日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
Web サイトは「無駄な穴のたくさん開いたじょうご」〜サイト成果向上の基本的な考え方(6月30日)
Copyright 2009 Japan Internet.com K.K. All Rights Reserved.http://www.internet.com/