XMLマスターポイントレッスン ~ プロフェッショナル(データベース)編 ~
第7回 ユーザー定義関数の作成と宣言
データディレクトテクノロジーズ株式会社(現:日本プログレス株式会社)
山田敏彦 YAMADA, Toshihiko
今回はユーザー定義関数を作成し、クエリをモジュール分割して簡単なデバッグ用の関数を実装する過程を通じて、ユーザー定義関数や変数の宣言と、これまであまり詳細に解説されてこなかった「Prolog」について説明します。内容もだんだんと高度になってきましたね。しっかりと内容を理解するためには時間がかかってしまうかもしれませんが、焦らずじっくりと取り組むようにしてください。
ユーザー定義関数の宣言
ユーザー定義関数は「Prolog」で宣言します。本連載の第2回でXQueryの完全なクエリの例を挙げ、Prologについて簡単に説明しましたが、覚えているでしょうか。Prologとは、各種の宣言を行なう宣言部のことでしたね(後述「Prologの構成」を参照)。第2回で示したクエリの例をLIST1に再掲します(コメントを付け替えています)。今回はこのLIST1のクエリ例を使用して説明を進めていきます。
LIST1:サンプルクエリ
(: xquery宣言 :)
xquery version "1.0" encoding "utf-8";
(: 名前空間宣言 :)
declare namespace my_ns = "http://fugahoge/sample";
(: 関数宣言 :)
declare function my_ns:who($name)
{
$name/text()
};
(: クエリ本体 :)
<result> {
for $p in doc( "namelist.xml")//name
return
<greeting>
{ fn:concat( "Hello, ", my_ns:who($p) ) }
</greeting>
}
</result>
xquery宣言から関数宣言までの部分がPrologで、次の部分がユーザー定義関数です。declare function my_ns:who($name)
{
$name/text()
};
中カッコで囲まれた部分に記述した式の結果が関数の返却値になります。この例では、「$name」という引数を介して渡された要素のテキストノードを抽出して返却しています。関数宣言の構文図を図1に示します。
図1には、この例にはない構文要素が2つあります。「as」と「external」です。asは関数が返す結果の型やパラメータの型を明示したい場合に使用します。パラメータの型を省略した場合は、パラメータは任意のシーケンス型とバインドできます。また、関数の型が省略された場合はシーケンス型として扱われます。厳密なエラーチェックを行ないたい場合はasを省略しないほうが良いでしょう。
externalは、関数がXQuery実行環境の外部で実装されていることを表わします。このような関数を「外部関数」と呼びます。例えば、Javaで関数を実装し、結果をXQueryに返す場合などがこれにあたります。外部関数宣言と外部で実装された関数の関連付けは実装固有の方法で行なわれます。
モジュールで関数を再利用する
一度作成した関数は、できれば再利用したいですね。XQueryには関数を再利用するための仕組みとして「モジュール」があります。クエリ本体が記述されているモジュールを「メインモジュール」、関数などの再利用するパーツが記述されているモジュールを「ライブラリモジュール」と呼びます。
先ほどの例で使用したクエリをモジュールを使用して2つのファイルに分割してみます。まず、ライブラリモジュールを作成しましょう(LIST2)。
LIST2:ライブラリモジュール
(: xquery宣言 :)
xquery version "1.0" encoding "utf-8";
(: モジュール宣言 :)
module namespace my_ns="http://fugahoge/sample";
(: 関数宣言 :)
declare function my_ns:who($name)
{
$name/text()
};
ライブラリモジュールは、モジュール宣言があること以外は元のクエリのPrologと変わりありません。また、ライブラリモジュールにはクエリ本体がありません。モジュール宣言は、このモジュールがメインモジュールではなくライブラリモジュールであることを示し、モジュールの名前空間を宣言します。モジュールに含まれる関数、変数は必ずモジュール宣言で定義された名前空間接頭辞で修飾されなければなりません。これを「モジュールのターゲット名前空間」と呼びます。さて、上記のライブラリモジュールをファイルに保存しておくことで、who()関数を複数のクエリで共有できるようになりました。ここではモジュールを保存したファイルのファイル名を「my_lib.xq」とします。
my_lib.xqに保存されたライブラリモジュールをクエリから利用するには、メインモジュールのPrologにIMPORT文を記述します。メインモジュールを作成してみましょう(LIST3)。
LIST3:メインモジュール
(: xquery宣言 :)
xquery version "1.0" encoding "utf-8";
(: 名前空間宣言 :)
declare namespace my_ns2 = "http://fugahoge/sample";
(: インポート :)
import module "http://fugahoge/sample" at "file:///c:/my_lib.xq";
(: クエリ本体 :)
<result> {
for $p in doc( "namelist.xml" )//name
return
<greeting>
{ fn:concat( "Hello, ", my_ns2:who($p) ) }
</greeting>
}
</result>
IMPORT文では、モジュールのターゲット名前空間とモジュールの所在を示すURIを指定します。これらは文字列リテラルでなければなりません。また、クエリ本体で関数や変数を参照する際には、名前空間宣言でモジュールのターゲット名前空間に関連付けた名前空間接頭辞で修飾しなければなりません。ライブラリモジュールを使用する際には、次のような点に注意してください。
- ライブラリモジュールは、さらにほかのライブラリモジュールをインポートできます。ただし、メインモジュールもライブラリモジュールも、自分が直接インポートしているモジュールで定義されている関数や変数しか参照できません。
- 同じターゲット名前空間に属していない限り、ライブラリモジュールが直接/間接に相互にインポートし合うことはできません。
(: xquery宣言 :)
xquery version "1.0" encoding "utf-8";
(: インポート :)
import module namespace my_ns2 = "http://fugahoge/sample" at "file:///c:/my_lib.xq";
モジュール宣言とIMPORT文の構文図を図2に示します。
IMPORT文では、at以下のモジュールのURIを省略できることに気付いたでしょうか。実装が名前空間のURIとモジュールの所在の関連について情報を持っている場合(例えば、名前空間のURIをキーにリポジトリやディレクトリサーバーを検索してモジュールに関する情報を取得できるような場合)は省略しても構わないことになっています。確かに、ソースコード中に文字列リテラルとしてURIをハードコードするとURIの変更が発生するたびにソースの書き換えが発生するので、できれば避けたいところです。
変数の宣言
モジュールの仕組みをうまく利用すれば、ちょっとしたプロジェクトで使用する共通ライブラリを作成できそうですね。そのような場合に欲しくなる機能がグローバル変数です。Prologで変数宣言すれば、そのモジュールをインポートしているすべてのモジュールから参照できますが、第2回でも説明したようにXQueryの変数は値を変更できないことを忘れないでください。
それでは、LIST1を変数宣言を使うように書き換えてみましょう。変数を利用して、簡単なデバッグメッセージを出力にコメントとして埋め込んでみようと思います。今度は、メインモジュールから紹介します(LIST4)。
LIST4:メインモジュール(変数宣言を追加)
(: 名前空間宣言:)
declare namespace my_ns2 = "http://fugahoge/sample";
(: インポート :)
import module "http://fugahoge/sample" at "file:///c:/my_lib.xq";
(: 変数宣言 :)
declare variable $mode as xs:string := "debug";
declare variable $nop as xs:string := "";
(: クエリ本体 :)
<result> {
if ( $mode = "debug") then comment { "Entering Main module" }
else $nop
,
for $p in doc( "namelist.xml" )//name
return
<greeting>
{ fn:concat( "Hello, ", my_ns2:who($p) ) }
</greeting>
}
</result>
メインモジュールでは、xs:string型の変数の宣言を2つ追加しています。$modeが"debug"の場合、クエリの結果にデバッグメッセージをコメントとして埋め込むことができます。XQueryのif式ではelse節が省略できないので空文字列を$nopとして宣言しておき、else節が評価される場合にはif式の結果として空文字列を返すようにしています。変数宣言の構文図を図3に示します。
「external」は、その変数が外部変数であり、実際にはXQueryの実行環境外で管理されていることを示します。例えば、Javaの変数をXQueryのクエリから参照する場合などがこれに相当します。外部変数とJavaなどの変数の関連付けは実装依存になります。
変数の有効範囲
Prologで宣言した変数は、そのモジュール内部とそのモジュールをインポートしているモジュールから参照できます。ただし、変数が宣言される前に、その変数を参照することはできません。例えば、次のような宣言は$aを宣言した時点で$bは宣言されていないのでエラーになります。
declare variable $a as xs:integer := $b + 1;
declare variable $b as xs:integer := 3;
余談になりますが、関数の宣言ではこのような制約はありません。次のクエリを考えてみましょう。
declare function local:f1( $p as xs:integer) as xs:integer
{
local:f2( $p )
};
declare function local:f2($p as xs:integer ) as xs:integer
{
$p + 1
};
local:f1( 3 )
f1を宣言した時点ではf2は宣言されていません。先ほどの変数宣言の例と一緒ですね。ところが、このクエリは正常に実行でき、結果として4が返されます。関数の場合は変数と違って、まだ宣言されていない関数を参照した宣言が可能なのです。
変数とモジュール
変数の有効範囲から分かるように、メインモジュールで宣言した変数はライブラリモジュールから参照できませんので、このままではmy_lib.xqやほかのライブラリモジュールにはデバッグメッセージを埋め込めません。そこで、メインモジュールとmy_lib.xqの両方からインポートされるライブラリモジュールを作成し、$mode変数の宣言をそこに移しましょう。ついでにコメントの埋め込み処理も関数化してしまいます。新しいデバッグ用のモジュールをLIST5に示します。
LIST5:デバッグライブラリモジュール
(: xquery宣言 :)
xquery version "1.0" encoding "utf-8";
(: モジュール宣言 :)
module namespace dbg="http://fugahoge/sample/debug";
(: 変数宣言 :)
declare variable $dbg:mode := "debug";
declare variable $dbg:nop := "";
(: debug用関数宣言 :)
declare function dbg:debug($msg)
{
if ( $dbg:mode = "debug" ) then comment {$msg}
else $dbg:nop
};
このモジュールは「my_dbg.xq」というファイルに保存しましょう。my_lib.xqファイルに保存されているライブラリモジュールをデバッグライブラリに対応させます(LIST6)。LIST6:ライブラリモジュール(デバッグライブラリ対応版)
(: xquery宣言 :)
xquery version "1.0" encoding "utf-8";
(: モジュール宣言 :)
module namespace my_ns="http://fugahoge/sample";
(: インポート :)
import module namespace mDBG = "http://fugahoge/sample/debug" at "file:///c:/my_dbg.xq";
(: 関数宣言 :)
declare function my_ns:who($name)
{
if ($mDBG:mode="debug") then concat( $name/text(), "-----by Func who" )
else $name/text()
};
LIST6のwho関数では、debug関数を使用していません。debug関数をwho関数の冒頭で呼ぶと、メインモジュールのconcat関数でエラーになるので、my_lib.xqではdebug関数は使用せず、返却する文字列にトレース情報を埋め込みました。少しわき道にそれることになりますが、練習問題のつもりでエラーになる理由を考えてみてください。who関数の冒頭に次のようにdebug関数を埋め込むと、who関数はdebug関数の実行結果と$name/text()の実行結果とのシーケンスを返します。
declare function my_ns:who($name)
{
mDBG:debug("Entering Func who" )
,
$name/text()
};
debug関数が実際に返してくるのは計算コンストラクタが生成したコメントノードです。カンマ演算子でコメントノードとtext()関数が返したテキストノードが連結されてシーケンスが生成されます。who関数の型は宣言していませんから、ノード型のシーケンスを呼び出し元に返すことは問題ありません。
呼び出し元はfn:concat関数です。fn:concat関数はxs:anyAtomicType型のパラメータを2つ以上受け取り、それらをxs:string型に変換して連結した1つの文字列を返却します。第1パラメータは文字列リテラル“Hello,”なので問題ありません。2番目の引数は複数の項目のあるシーケンスになります。項目が1つしかないシーケンスはxs:anyAtomicType型との互換性がありますが、2つ以上の項目を持つシーケンスはxs:anyAtomicType型とは互換性がありません。そのため2番目のパラメータで型の不整合が発生してしまいエラーになるわけです。
debug関数を埋め込んだメインモジュールをLIST7に示します。
LIST7:メインモジュール(デバッグライブラリ対応版)
(: xquery宣言 :)
xquery version "1.0" encoding "utf-8";
(: 名前空間宣言 :)
declare namespace my_ns2 = "http://fugahoge/sample";
(: インポート :)
import module "http://fugahoge/sample" at "file:///c:/my_lib.xq";
import module namespace my_dbg = ⇒ (掲載の都合上、折り返しております。)
"http://fugahoge/sample/debug" at "file:///c:/my_dbg.xq";
(: クエリ本体 :)
<result> {
my_dbg:debug( "Entering Main module" )
,
for $p in doc( "namelist.xml")//name
return
<greeting>
{ fn:concat( "Hello, ", my_ns2:who($p) ) }
</greeting>
}
</result>
それでは、サンプルデータを作成して、今回作成したクエリを実行してみましょう。連載第2回で使用したサンプルデータ(LIST8)を入力してメインモジュールを実行してみます。LIST8:連載第2回で使用したサンプル
<namelist>
<name>Hiro</name>
<name>Toshi</name>
<name>Mayumi</name>
</namelist>
出力は次のとおりです。<result>
<!--Entering Main module-->
<greeting>Hello, Hiro-----by Func who</greeting>
<greeting>Hello, Toshi-----by Func who</greeting>
<greeting>Hello, Mayumi-----by Func who</greeting>
</result>
関数呼び出しが出力からトレースできますね。今までに学んだ項目の復習を兼ねて、皆さん自身のアイデアでデバッグ用ライブラリモジュールに機能追加や機能改善をしてみてはどうでしょう。良い力試しになると思いますよ。
Prologの構成
Prologでは、クエリ本体の処理に直接、間接に影響するさまざまな宣言を行なうことができます。Prologで行なうことのできる宣言には大きく次の7種類があります。
(1)デフォルト名前空間宣言
(2)セッター
(3)名前空間宣言
(4)インポート宣言
(5)変数宣言
(6)関数宣言
(7)オプション宣言
宣言は、次のような順序に従って行なわなければなりません。(1)デフォルト名前空間宣言から(4)インポート宣言までが1つのグループ(グループ1)、(5)変数宣言から(7)オプション宣言までがもう1つのグループ(グループ2)になり、グループ内では順不同で宣言できますが、グループを混在させることはできません。また、グループ2は必ずグループ1より後に宣言しなければなりません。
(1)デフォルト名前空間宣言、(3)名前空間宣言、(4)インポート宣言、(5)変数宣言、(6)関数宣言については本文やコラムで触れていますが、(2)セッターと(7)オプション宣言には触れていないので簡単に説明しておきましょう。セッターはクエリの細かい動作を調整するために使用します。主なセッターの一覧を表に示します。
表:主なセッター
名前 | 説明 | 構文 |
バウンダリスペース宣言 | ダイレクトコンストラクタが空白文字をどのように扱うかを指定する | declare boundary-space preserve または strip |
デフォルト照合順序宣言 | テキストの比較の際にどのような照合順序を用いるかを指定する | declare default collation <URI> |
ベースURI宣言 | ベースURI(相対URIの基点となるURI)を指定する | declare base-uri <URI> |
エンプティオーダー宣言 | order by句でソートキーに指定した値に非数や空シーケンスがあった場合、最も大きな値として扱うか、最も小さな値として扱うかを指定する | declare default order empty greatest または least |
declare boundary-space preserve;
<a>
<b> { "abcde" } </b>
</a>
このクエリの実行結果は、
<a>
<b> abcde </b>
</a>
になりますが、バウンダリスペース宣言を「declare boundary-space strip;」に変更すると、結果は次のようになります。
<a><b>abcde</b></a>
オプション宣言は特定の実装の動作を調整するために使用します。構文は決まっていますが、具体的な設定値や効果は実装依存になります。オプション宣言の構文を次に示します。
declare option <オプション名> <設定値>
今月の確認問題
ここまで、ユーザー定義関数と変数の宣言とクエリのモジュール分割、Prologについて解説してきました。これらの内容がしっかり理解できているかどうか、確認問題でチェックしてみましょう。
問題
次の[books.xml]に対して[XQuery]による問い合わせを実行した結果として、正しいものを1つ選択してください。
ただし、XQueryプロセッサはfn:doc関数で[books.xml]を正常に読み込むことができるものとします。
また、選択肢Dに関しては実行結果のXML宣言やインデントの有無や属性の順序を考慮しません。
[ books.xml ]
<books>
<book id="B001" type="Basic">
<title>XML Basic</title>
</book>
<book id="B002" type="Basic">
<title>XQuery</title>
</book>
<book id="B004" type="Practical">
<title>XMLDB</title>
</book>
</books>
[ XQuery ]
declare function local:myFunc($elements) {
for $element in $elements
return
<book>{
$element/@id,
attribute title { $element/title }
}</book>
};
<output>{
let $books := fn:doc("books.xml")//book
for $type in fn:distinct-values($books/@type)
return
<data type="{$type}">{
local:myFunc($books[@type eq $type])
}</data>
}</output>
- local接頭辞を使用するための宣言がないためエラーとなる
- myFunc関数の定義内でfor節を使用しているためエラーとなる
- $type変数に値「Practical」がバインドされたときのmyFunc関数の実行でエラーになる
- 正しく実行でき、結果は次のようになる
<output>
<data type="Basic">
<book id="B001" title="XML Basic"/>
<book id="B002" title="XQuery"/>
</data>
<data type="Practical">
<book id="B004" title="XMLDB"/>
</data>
</output>
解説
ユーザー定義関数を定義する際の関数名は必ず名前空間とともに定義します。このとき「local」接頭辞はローカルで使用するユーザー定義関数のための名前空間接頭辞としてあらかじめ定義されていますので、これについてはわざわざ接頭辞の使用を宣言することなくいつでも使用できます。したがってAは誤りです。また、ユーザー定義関数を定義する場合、関数本体にFLWOR表現を含む一般的な式を記述できるのでBも誤りです。
Cについては、実行時の処理を考えてみる必要があります。$type変数に値「Practical」がバインドされた場合、myFunc関数を実行する際の実際の引数($books[@type eq $type]の評価結果)は1つのノード(id属性の値が"B004"である1つのbook要素)となりますが、この場合でもmyFunc関数の実行に不都合はなく、ここでエラーになることはありません。したがってDが正解です。このとき、XMLでは属性に順序がありませんので、実行結果でのid属性とtitle属性については実装によっては逆の順序になってもかまいません。
* * *
今回で、筆者の担当は最後となります。長いようで短い半年間でした。多少なりとも皆さんの学習の役に立っていれば良いのですが、いかがだったでしょうか。
これからXMLマスタープロフェッショナル(データベース)受験用の参考書も出てくると思いますので、それらの参考書の知識と本連載で学んだ知識をブラッシュアップして、試験に万全の体制で臨んでいただきたいと思います。皆さんの合格を願ってやみません。
最後に今回のポイントをまとめておきます。
次回からは、XMLデータの入出力について解説する予定です。
山田敏彦 (やまだとしひこ)
慶應義塾大学理工学部卒、日立ソフト、サイベースなどを経て2003年より現職。今年の夏はすさまじい暑さでした。最高気温が35度を超えたくらいでは驚かなくなってしまった自分に改めてビックリします。この号がお手許に渡る頃には涼しくなっていると思いますが…
<掲載> P.195-201 DB Magazine 2007 November