最新のXML仕様を実践で覚える「XQueryチュートリアル」(3)
XQueryの関数を使う、定義する

XMLの本格利用に向けた重要な技術の1つがXMLデータベースの発展だ。そのカギを握るのは、問い合わせ言語XQueryの標準化である。現在、XQueryは標準化目前のところまできており、実際にXQueryの実装も登場している。本記事は、そのXQueryの実践を目的とした。

戌亥稔
ビーコンIT
2003/1/16


 XQueryではさまざまな関数を使って問い合わせを記述できる。関数にはXQueryの仕様であらかじめ定義された関数と、ユーザー定義関数を使う方法の2種類が用意されている。今回はそれを解説する。

本記事は、ソフトウェアAGおよびビーコンITが無償で公開しているXQueryプロセッサのQuipを利用して、XQueryの機能を実践解説している。Quipの入手方法などについては、第1回「XQueryを実体験してみる」を参照のこと。
また、 W3Cが発表したXQueryのワーキングドラフト最新版では、XQueryの表現式として従来のFLWR表現式がFLWOR表現式(同様にフラワーと読む)に変更された。これはFLWR表現式のFor、Let、Where、Returnに、Order byが加えられたものである。しかし、この記事で使用しているQuiP v2.2.1.1は2002年4月30日版のワーキングドラフトに対応しているため、本記事ではFLWRという表記のまま説明を続ける。

あらかじめ定義された関数を使う

 XQueryではXQuery 1.0 and XPath 2.0 Functions and Operations(原稿執筆時点でW3Cワーキングドラフト)仕様で定義された関数を使うことができる。ただし、XQueryの仕様そのものがまだワーキングドラフトなので、関数の名前や内容が変わる可能性がある。今回はリレーショナルでもよく使われるsum()関数やmax()関数を使って問い合わせを記述してみる。

 検索対象となるXML文書のProjects.xmlでは、それぞれのプロジェクトにどれだけの工数をかける予定であるかが、メンバーごとに記述されている。これに対してそれぞれのプロジェクトでトータルどれだけの工数がかかるかsum()関数で計算しよう。

<?xml version="1.0" encoding="UTF-8" ?>
<projects>
  <project code="200200020" start="2002/01/20" end="2002/03/31">
    <name>XMLによる文書管理システム</name>
    <members>
      <member>
        <name>川泉陽一</name>
        <manpower unit="hour">100</manpower>
      </member>
      <member>
        <name>中田聡</name>
        <manpower unit="hour">50</manpower>
      </member>
    </members>
  </project>
  <project code="200200025" start="2002/02/15" end="2002/03/25">
    <name>XMLによるB2Bシステム構築</name>
    <members>
      <member>
        <name>本岡欣也</name>
        <manpower unit="hour">100</manpower>
      </member>
      <member>
        <name>川泉陽一</name>
      <manpower unit="hour">50</manpower>
      </member>
    </members>
  </project>
  <project code="200200031" start="2002/03/15" end="2002/03/31">
    <name>モバイルシステムの構築</name>
    <members>
      <member>
        <name>川泉陽一</name>
        <manpower unit="hour">10</manpower>
      </member>
    </members>
  </project>
</projects>
リスト1(再掲) 検索対象となるファイルProjects.xml

for $p in document("Tutorial/data/projects.xml")//project
let $m := $p/members/member
return <projects> { <project> { $p/name } {$m} </project>
    , <totalmanpower>{sum($m/manpower)}</totalmanpower>
  } </projects>
[ example-2-4-1] Projects.xmlに対して、プロジェクトごとの工数をSum()関数で計算する

 上記のFLWR表現式により、プロジェクトごとに<manpower>タグの中の値をすべて足しこむことができる。

画面12 プロジェクト検索結果

 画面12は、example-2-4-1の問い合せの結果である。<totalmanpower>タグの内容として、そのプロジェクトごとのmanpowerの値がすべて合計されている。

 projects.xmlでは<manpower>のタグに、属性で単位を現す値(hourやday)が与えられている。XMLではこのように、属性で単位を与えて意味付けるような記述をする場合が多々ある。例えば、

currency="dollar"
currency="yen"

などのように貨幣の単位を属性として記述し、要素の値として金額を記述すれば、

<price currency="dollar>12.90</price>
<price currenct="yen">100</price>

のように、値がどの単位で示されているかを相手に知らせることができる。この場合は、単位が違うために単純にsum()関数で加えるだけでは正しい結果が得られない。この解決策については、「ユーザー定義関数を使う」と「条件分岐(if-then-else)を使う」のところで説明を行う。

 別の関数も練習してみよう。プロジェクトの概要として、最大工数、最小工数、合計工数、プロジェクトメンバーの人数、平均工数を表示する。

for $p in document("Tutorial/data/projects.xml")//project
let $m := $p/members/member,
  $mp := for $x in $m/manpower return (double($x/text()))
return <projects> { <project> { $p/name } {$m} </project> ,
    <manpowersummary>
      <max>{max($mp)}</max>
      <min>{min($mp)}</min>
      <total>{sum($m/manpower)}</total>
      <count>{count($mp)}</count>
      <avarage>{avg($mp)}</avarage>
    </manpowersummary>
  } </projects>
[ example-2-4-2] sum()、max()、min()、avr()、count()関数を使い、プロジェクトの概要として最大工数、最小工数などを表示するための問い合わせ

 問い合わせの中で注目すべき点は、3行目の$mp := for $x in …return(double($x/text())である。これは、<manpower>要素の値を全て数値型に変換している。というのも、数値型に変換しないと、ストリング値として関数を実行するためである。今回は試していないが、あらかじめ元データに対するスキーマをXML Schemaで作成し、問い合わせの先頭でそのスキーマファイルをインポートすることができるので、あらかじめスキーマファイルによって<manpower>を数値型にしておけば、この3行目を含むlet句は省略できるのではないかと思う。下記の画面13はexample-2-4-2の実行結果である。

画面13 プロジェクトの概要を表示する

ユーザー定義関数を使う

 XQueryではユーザー関数を定義することができる。プロジェクトごとの工数をSum()関数で計算するexample-2-4-1の内容を、ユーザー定義関数を使って書き換えてみよう。

 まず最初に次のような単純なユーザー定義関数を定義する。この関数では<member>要素を受け取り、そのmanpowerをsum()関数で加算している。

define function sum_mp(xs:string $m ) returns string
{
  let $n := $m
  return ( sum($m/manpower) )
}

 これを用いた問合せはexample-2-4-3のようになる。9行目のsum_mp()を呼び出すところでは、$mつまり、<member>要素を渡している。

define function sum_mp(xs:string $m ) returns string
{
  let $n := $m
  return ( sum($m/manpower) )
}
for $p in document("Tutorial/data/projects.xml")//project
let $m := $p/members/member
return <projects> { <project> { $p/name } {$m} </project>
    , <totalmanpower>{sum_mp($m)}</totalmanpower>
  } </projects>
[ example-2-4-3] ユーザー定義関数を使って、プロジェクトごとの工数を合計する

 これだけでは、ユーザー定義関数を使う必要性は見つけられない。そこで前回「XQueryのFLWR表現式を使いこなす」で紹介した、メンバーごとに所属しているプロジェクトを表示するexample-2-3-5を、ユーザー定義関数を使って書き換えてみる。メンバーごとにどのようなプロジェクトに参加をしているかの一覧に、メンバーごとの工数の合計も出してみることにしよう。定義する関数は下記のsum_mp2()である。

define function sum_mp2(xs:string $mn ) returns element
{
  let $n := for $m in document("Tutorial/data/projects.xml")//project/members/member[name = $mn]
    return $m/manpower/text()
  return ( <totalmanpower>{sum($n)}</totalmanpower> )
}

 sum_mp2()関数はパラメータとしてメンバー名が与えられ、リターン値として工数の合計を<totalmanpower> … </ totalmanpower>というタグ付きで返している。

 関数定義の結果のタイプ宣言が「returns element」となっており、returnにsがついている。というのも、FLWR表現式のreturnと明確に区別するためにsを付けなければならないからだ。また、パラメータや戻り値にはタイプが明記されている。現在のQuiPのバージョンでは、これらをIntegerやそのほか適当なタイプに変えても型違いのエラーとはならない。XQueryは強く型付けされた言語であることから、本来はチェックすべきであると思うが、将来のQuiPのバージョンで修正されるだろう。

 この関数を使った問い合わせは次のようになる。

define function sum_mp2(xs:string $mn ) returns element
{
  let $n := for $m in document("Tutorial/data/projects.xml")//project/members/member[name = $mn]
    return $m/manpower/text()
  return ( <totalmanpower>{sum($n)}</totalmanpower> )
}

let $in := document("Tutorial/data/projects.xml")
for $mn in distinct-values($in//member/name)
return
  <memberlist>{
    <member> {$mn/text()} </member> ,<projects> {
    for $p in document("Tutorial/data/projects.xml")//project[members/member/name = $mn]
    let $mp := $p/members/member[name = $mn]/manpower
    return <project> <name>{ $p/name/text() }</name> {$mp} </project>
      , sum_mp2($mn) } </projects>
  }</memberlist>
[ example-2-4-4] ユーザー定義関数を使い、メンバーごとに所属しているプロジェクトと工数の合計を表示する

 example-2-4-4の問合せを実行すると、画面14のようにメンバーごとの工数の合計を計算することができる。

画面14 メンバーごとの工数合計をとる

 ユーザー定義関数を使って記述したことで、問い合わせからは、関数の中で行っている処理を分離することができ、見やすくなった。このようにXQueryではユーザー定義関数を使って、問合せをシンプルにすることが可能である。

条件分岐(if-then-else)を使う

 メンバーごとに所属しているプロジェクトと工数の合計を表示するexample-2-4-4では、<manpower>要素の単位が何であっても単純にsum関数で加算するようになっていた。しかし、実際にサンプルデータには、<manpower>要素の単位が<manpower unit="hour">と<manpower unit="day">の2種類ある。example-2-4-4ではそれを無視して、単純に<manpower>の項目を加えただけであった。

 これを、単位が日の場合(unit="day"の場合)は、それに1日の標準時間(例:8時間)を掛けて時間に変換してから工数に加算するよう書き換えよう。単位を考慮したsum_mp3()関数は次のようになる。

define function sum_mp3(xs:string $mn ) returns element
{
  let $n := for $m in document("Tutorial/data/projects.xml")//project/members/member[name = $mn]
    return
      if ($m/manpower/@unit = "day")
      then
        $m/manpower/text() * integer("8")
      else
        $m/manpower/text()
  return ( <totalmanpower unit="hour">{sum($n)}</totalmanpower> )
}

 分かりやすくするため、出力される要素に<totalmanpower unit="hour">のように単位を属性として加える。全体のクエリーはexample-2-5-1のようになる。

define function sum_mp3(xs:string $mn ) returns element
{
  let $n := for $m in document("Tutorial/data/projects.xml")//project/members/member[name = $mn]
    return
      if ($m/manpower/@unit = "day")
      then
        $m/manpower/text() * integer("8")
      else
        $m/manpower/text()
  return ( <totalmanpower unit="hour">{sum($n)}</totalmanpower> )
}

let $in := document("Tutorial/data/projects.xml")
for $mn in distinct-values($in//member/name)
return
  <memberlist>{
    <member> {$mn/text()} </member> ,<projects> {
    for $p in document("Tutorial/data/projects.xml")//project[members/member/name = $mn]
    let $mp := $p/members/member[name = $mn]/manpower
    return <project> <name>{ $p/name/text() }</name> {$mp} </project>
    , sum_mp3($mn) } </projects>
}</memberlist>
[ example-2-5-1] If-then-elseを使い、単位が複数あっても計算が正しくできるようにする(属性での対応)

 example-2-4-4とexample-2-5-1の違いは、sum_mp2()をsum_mp3()として関数の中身を変更しただけである。関数をうまく使うことで、メンテナンス性を上げることができる。つまり、もしunit="month"という単位が加わった場合でも、sum_mp3()関数の中身を変更するだけでよくなるのだ。

 例えば、メンバー「中田聡」の工数の総合計は次の3つのプロジェクトの合計として得られる。

<name>XMLによる文書管理システム</name>
<manpower unit="hour">50</manpower>

<name>XMLによるB2Bシステム構築</name>
<manpower unit="day">5</manpower>

<name>Web Servicesプロジェクト</name>
<manpower unit="day">5</manpower>

 「XMLによるB2Bシステム構築」と「Web Servicesプロジェクト」はそれぞれ、5×8=40として計算されるので、総合計は130時間になり、<totalmanpower unit="hour">130</totalmanpower>となる。

 example-2-5-1の実行結果は、下記の画面15のようになる。

画面15 メンバーの追加

 上の例ではmanpowerの属性unitの値が“day”であるか“hour”であるかをチェックして、dayである場合には標準時間の8を掛けて足しこむという処理をした。今度はもうちょっとXMLらしいやりかたをしてみる。

 XMLの特徴として、要素をオプションとして使える。つまり上の例の場合、<manpowerbyday>要素と<manpowerbyhour>要素の2つを作成し選択できるようにする方法である。このとき、Projects.xmlの<manpower unit="hour"> … <manpower>はすべて<manpowerbyhour> … </manpowerbyhour>と書き換え、<manpower unit="day"> … <manpower>を<manpowerbyday> … </manpowerbyday>と書き換える。これをprojects2.xmlとする(画面16)。

画面16 Projects2.xml [Projects2.xml]

 この場合、example-2-5-1は<manpowerbyday>の要素が存在するかどうかのチェックを行い、その値に標準時間の8を掛けて加算する必要がある。

define function sum_mp3(xs:string $mn ) returns element
{
  let $n := for $m in document("Tutorial/data/projects2.xml")//project/members/member[name = $mn]
    return
      if ($m/manpowerbyday)
      then
        $m/manpowerbyday/text() * integer("8")
      else
        $m/manpowerbyhour/text()
  return ( <totalmanpower unit="hour">{sum($n)}</totalmanpower> )
}

let $in := document("Tutorial/data/projects2.xml")
for $mn in distinct-values($in//member/name)
return
  <memberlist>{
    <member> {$mn/text()} </member> ,<projects> {
    for $p in document("Tutorial/data/projects2.xml")//project[members/member/name = $mn]
    let $mp := $p/members/member[name = $mn]/*
    return <project> <name>{ $p/name/text() }</name> {$mp} </project>
    , sum_mp3($mn) } </projects>
}</memberlist>
[ example-2-5-2] If-then-elseを使い、単位が複数あっても計算が正しくできるようにする(要素の選択で対応)

 sum_mp3()関数の中のif-then-else文はif($m/manpowerbyday)と評価をしている。これは、<manpowerbyday>という要素が存在する場合は、then以下を実行し、それ以外の場合はelse以下を実行することを示している。

画面17 example2-5-2の結果

 次回はXML文書のジョインを中心に解説を行う予定だ。

参考:これまでに紹介した問い合わせファイルのリスト

Query名 説明
InstallTest.xquery インストールテスト
example-2-1-1.xquery Projects.xmlの中からすべてのプロジェクト名を取り出す
example-2-1-2.xquery プロジェクトの終了が遅い順番に並び替える
example-2-1-3.xquery プロジェクトの終了が遅い順番に並び替え、プロジェクト名のみ表示する
example-2-1-4.xquery プロジェクト名でソートし、プロジェクト名のみ表示する
example-2-2-1.xquery 2名以上メンバーが存在するプロジェクトを表示する
example-2-2-2.xquery 2名以上メンバーが存在するプロジェクトのプロジェクト名とメンバーの一覧を作る
example-2-2-3.xquery 2名以上メンバーが存在するプロジェクトのプロジェクト名とメンバーの一覧を作る。ただし、プロジェクト名は属性として作成する
example-2-2-4.xquery FLWR表現のWhere句を使い、2名以上メンバーが存在するプロジェクトのプロジェクト名とメンバーの一覧を作る
example-2-2-5.xquery FLWR表現をネストし、2名以上メンバーが存在するプロジェクトのプロジェクト名とメンバーの一覧を作る
example-2-2-6.xquery FLWR表現をネストし、2名以上メンバーが存在するプロジェクトのプロジェクト名とメンバーの一覧を作り、プロジェクト名でソートする
example-2-3-1.xquery projects.xmlをメンバー毎にどういうプロジェクトに参加しているかを表すメンバーリストを作成する
example-2-3-2.xquery メンバーリストにそれぞれのプロジェクト毎の工数計画を表示させる(フィルタを使う)
example-2-3-3.xquery メンバーリストにそれぞれのプロジェクト毎の計工数計画を表示させる(Where文を使う
example-2-3-4.xquery プロジェクト毎に工数計画の入ったメンバーリストのWhere句を取り除き、Tuple streamを確認する。これによりFor句がどのような働きをするか確認する
example-2-3-5.xquery プロジェクト計画の入ったメンバーリストのLet句のフィルタを取り除いて、let句がどのような働きをしているかを確かめてみる
example-2-4-1.xquery プロジェクトごとの工数をSum()関数で計算する
example-2-4-2.xquery sum()、max()、min()、avr()、count()関数を使い、プロジェクトの概要として最大工数、最小工数などを表示するための問い合わせ
example-2-4-3.xquery ユーザー定義関数を使って、プロジェクトごとの工数を合計する
example-2-4-4.xquery ユーザー定義関数を使い、メンバーごとに所属しているプロジェクトと工数の合計を表示する
example-2-5-1.xquery If-then-elseを使い、単位が複数あっても計算が正しくできるようにする(属性での対応)
projectx2.xml manpower要素の単位を、属性でなく、manpowerbyday、manpowerbyhourなど要素の違いで表現
example-2-5-2.xquery If-then-elseを使い、単位が複数あっても計算が正しくできるようにする(要素の選択で対応)

3/5

Index
連載:XQueryチュートリアル
  XQueryを実体験してみる
  XQueryのFLWR表現式を使いこなす
XQueryの関数を使う、定義する
  XQueryによるXML文書の結合〜1
  XQueryによるXML文書の結合〜2


XML & SOA フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

HTML5+UX 記事ランキング

本日月間