レベル: 中級 Matthias Nicola (mnicola@us.ibm.com), Senior Software Engineer, IBM Silicon Valley Laboratory Uttam Jain (uttam@us.ibm.com), Advisory Software Engineer, IBM
2007年 10月 11日 IBM® DB2® 9.5 for Linux, Unix, and Windows の新しい機能のなかで特に重要なのは、XML 更新機能です。前のバージョンの DB2 9 で導入された pureXML™ サポートでは、XML データを保管して索引を付けた上で、SQL/XML および XQuery 言語で XML データにクエリーを実行するようになっています。そのため、XML 文書の変更はデータベース・サーバーの外で行われ、それから文書全体が DB2 で更新されていました。今回、DB2 9.5 では XML 文書に含まれる個々の要素と属性の変更、挿入、削除を可能にする XQuery の標準拡張、XQuery Update Facility を導入しています。これにより、XML データはますます更新しやすくなり、パフォーマンスの向上も実現します。この記事では、この新しい XML 更新機能を取り上げ、代表的な XML 更新操作の例を紹介するとともに、よくある落とし穴に陥らないための方法を説明します。
概要と背景
さまざまな点で、DB2 9 の pureXML は XML 対応のデータベース管理サポートに関して業界を先駆けていました。実際、IBM が pureXML 技術の開発を進めていたとき、XML 標準と XQuery 標準を定義および管理する団体である W3C (World Wide Web Consortium) では XML 対応の標準更新メカニズムの定義に取り掛かったばかりでした。つまり、XML 更新の標準は存在しなかったということです。そのため、DB2 9 では SQL の UPDATE 文で文書全体の置換をサポートしています。以下はその一例です。
create table xmlcustomer (cid bigint, info XML);
update xmlcustomer
set info = '<customerinfo><name>John Doe</name>...</customerinfo>'
where cid = 1783;
|
UPDATE 文の set 節は、指定の行に含まれる既存の文書を新しい文書に置換します。上記の例では文書がリテラル値として指定されていますが、パラメーター・マーカーやホスト変数によってデータベースに渡される場合もあります。DB2 9 では通常、アプリケーションで XML 文書内の個別の要素または属性を変更する必要がある場合、変更はアプリケーション自体のなかで行われます。したがって、一般的な処理シーケンスは以下のようになります (図 1 を参照)。
- XML 文書を読み取るための SQL または XQuery 文をアプリケーションが実行依頼します。
- データベースから文書が取得され、テキスト・フォーマットにシリアライズされます。
- 文書がクライアントに送信されます。
- アプリケーションが XML 文書を構文解析します。解析には一般に、文書オブジェクト・モデル (DOM) が使用されます。
- アプリケーションが DOM API を使用して文書を変更します。
- クライアント・アプリケーション内で文書が再度シリアライズされます。
- アプリケーションが SQL の UPDATE 文を実行依頼し、更新した文書をデータベース・サーバーに送信します。
- データベース・サーバーが更新された XML 文書を構文解析し、古い文書を置き換えます。
図 1. DB2 9 での XML 文書の更新
XML 文書を取得、構文解析、そして変更するこのプロセスは、データベース・サーバーで実行されるストアード・プロシージャーにカプセル化されることもあります。その場合の処理手順は、クライアントとの文書の受け渡しがない点を除けば図 1 の手順と同じです。
DB2 9.5 の新しい更新機能は、このプロセスを単一のステップにまで減らします。アプリケーションはただ単に、XQuery 変換式を組み込んだ SQL の UPDATE 文を DB2 サーバーに送信するだけに過ぎません (図 2 )。この式は、XML データの更新用に新しく登場した XQuery 標準の一環です。
図 2. DB2 9.5 での XML 文書の更新
DB2 9.5 は UPDATE 文を実行する際に、該当する文書 (複数可) を見つけて指定された要素または属性を変更します。これは DB2 ストレージ層内部で行われます。つまり、XML 文書は構文解析されることも、シリアライズされることもなく、終始 DB2 の内部階層型 XML フォーマットのままであるということです。並行性制御とロギングは完全な文書のレベルで行われます。この新しい更新プロセスでは全体的な速度が図 1 のプロセスより 2 倍から 4 倍上がります。
新しい更新機能では、XML 文書をノード・レベルで変更することができます。変更対象のノードはほとんどの場合、要素または属性です。ノードでは以下の操作を実行できます。
- ノードの値の置換
- ノード自体の置換
- 新規ノードの挿入 (指定したノードの前または後ろなど、特定の位置でも挿入可能)
- ノードの削除
- ノードの名前変更
- 単一の UPDATE 文による文書内の複数のノードの変更
- 単一の UPDATE 文による複数の文書の更新
この記事の残りの部分では、上記の XML 更新を XQuery 変換式を使ってどのように実行するかを説明していきます。この説明から、UPDATE 文に変換式を組み込んで、ディスク上のデータを永続的に変更する方法や、クエリー内の XML データを永続的に変更することなく、読み出し中にその場で変更する方法を学んでください。後者の方法は、アプリケーションが受け取らなければならない XML フォーマットがデータベース内の XML フォーマットとは異なる場合に役立ちます。
サンプル・データ
新しい DB2 9.5 XML 更新機能を説明するために、以下の CREATE TABLE 文および INSERT 文で定義したサンプル・テーブルとデータを使用します。この xmlcustomer テーブルにはそれぞれ bigint 型と XML型を持つ 2 つの列、そして 2 つのデータ行があります。行を識別するのはそれぞれ cid 値、1000 および 1001 です。サンプル・データとこの記事に記載するすべての更新は、「ダウンロード」セクションからまとめて 1 つのテキスト・ファイルとしてダウンロードできます。
create table xmlcustomer (cid bigint, info XML); |
|
insert into xmlcustomer values (1000, '
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo>'); |
|
insert into xmlcustomer values (1001, '
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo>'); |
|
図 3 にサンプル文書の階層構造を示します。XML クエリーも更新も最終的には常にこのツリーをナビゲートすることになるため、XML クエリーまたはXML の更新を設計する際には、このツリー構造を念頭に置いておくと作業しやすくなります。
図 3. サンプル文書のツリー構造
XML データの更新 ‐ 単純な例
最初に紹介する更新 1 は単純な XML 更新の例です。この例では、特定カスタマーの電話番号を変更しています。全体的に見ると、これが馴染み深い SQL UPDATE 文の「update… set…. where…」構文であることがわかるはずです。また、「set」節が XML 列「info」に新しい値を割り当て、この新しい値が XMLQUERY 関数の結果となっていることも見て取れます。
SQL/XML で XML データに対してクエリーを実行する場合の XMLQUERY 関数を思い出してください。XMLQUERY は、結果セットの特定の行に位置する XML 文書に応じて XML 値を抽出または構成するために SQL クエリーの SELECT 節で使用されることがよくあります。このような XML 値の抽出あるいは構成を可能にするため、XMLQUERY 関数では SQL 文に XQuery 式を組み込めるようになっています。この例では特定の XML 文書を変更するために XMLQUERY 関数を使っています。更新 1 の XQuery 式は変換式に該当します。変換式は、XML 文書を変更できるように XQuery を拡張する新しい標準、XQuery Update Facility の中核です。
変換式はオプションの「transform」キーワードで始まり、その後に COPY、MODIFY、RETURN 節が続きます。この変換式からすぐにわかるのは、COPY 節が XML 列 (info) の入力文書を変数 (この例では $new) に割り当て、MODIFY 節がこの変数に 1 つ以上の更新操作を適用し、最後に RETURN 節が変換式の結果を生成することです。
更新 1
update xmlcustomer
set info = xmlquery( 'transform
copy $new := $INFO
modify do replace value of $new/customerinfo/phone with "905-477-9011"
return $new')
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country=“Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country=“Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>90111</zipcode>
</addr>
<phone type="work">905-477-9011</phone>
</customerinfo> |
|
特に興味深いのは、実際に行われる更新操作を定義する MODIFY 節です。この例では「phone」要素の値を単に新しい値 (905-477-9011) に置き換えているだけですが、この記事ではさまざまな MODIFY の例を記載するので注意して見てください。更新 1 やその他の一般的な使用例では、RETURN 節は変更された文書を保持する変数を返すだけにとどめていますが、要素構造や FLWOR 式を含めた、はるかに複雑な式にすることもできます。同様に、COPY 節の右側はたいてい、入力文書 (この例では $INFO) を保持する変数に過ぎませんが、それより複雑にすることも可能です。COPY 節の右側は単一のノードにならなければなりません。つまり、空のシーケンスにも、複数の項目のシーケンスにもできないということです。この単一のノードは子孫ノードを持つことができるので、XML 文書のルートにすることができます (たいていの場合はそうなります)。
「transform」キーワードはオプションのため、以降は省略します。
SQL クエリーでの文書の更新
最初は矛盾しているように聞こえますが、変換式をクエリーで使用することは、実際には正しいことであり、非常に簡単なことです。こうすることで、XML 文書をデータベースから読み取ってアプリケーションに渡すときに変更することができます。更新 2 は、更新 1 と同じ XMLQUERY 関数、変換式、そして WHERE 節を使用した SQL の SELECT 文です。更新 1 ではディスク上の文書に永続的に記録される変更を加えている一方、更新 2 ではテーブル内の元の文書はそのまま変更せずに、クエリー処理の一環としてだけその場で文書を変更しています。この場合の更新は、電話番号の市外局番以外の部分を隠すためのものです。
更新 2
select xmlquery('copy $new := $INFO
modify do replace value of $new/customerinfo/phone with "905-xxx-xxxx"
return $new ')
from xmlcustomer
where cid = 1000;
|
この記事に記載するほとんどの例では、変換式は SQL の UPDATE 文内に含まれますが、更新 2 のようにクエリー内に変換式が現れる場合もあります。後で紹介する例からわかるように、クエリー内の TRANSFROM 式が特定の要素または属性を文書から削除したり、要素を別の名前で公開する際にも役立ちます。さらに、新しい更新機能を最初に調べてテストするときには、UPDATE 文ではなくSELECT 文に変換式と一緒に XMLQUERY 関数を組み込んだほうが便利な場合もあります。このようにすると、更新の効果をすぐに見て確かめられるため、更新が予想外の結果をもたらしてテーブルのデータを台無しにすることがありません。
パラメーター・マーカーを使用した更新
上記の更新 1 と更新 2 では、「phone」要素の新しい値を UPDATE 文のテキストにハード・コーディングしています。一方、UPDATE 文を作成してコンパイルするのは一度だけにして、更新を実行するたびに新しい値を渡したいという場合も珍しくありません。そうすれば、毎回実行するごとにデータベース・サーバーで UPDATE 文を再コンパイルする必要がなくなるからです。更新 3 では、XMLQUERY 関数で SQL スタイルのパラメーター・マーカー ("?") を使用することによって再コンパイルの必要をなくしています。パラメーター・マーカーは変数 ($z) として XQuery 式に渡されます。更新 3 では更新された行を選択するために WHERE 節でもパラメーター・マーカーを使っています。
更新 3
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do replace value of $new/customerinfo/phone with $z
return $new ' passing cast(? as varchar(15)) as "z")
where cid = ?
|
既存の値に代わる新規の値を計算することも可能です。例えば既存の電話番号に内線番号を追加するには、modify 節を modify do replace value of $new/customerinfo/phone with concat($new/customerinfo/phone, "x301") のようにします。あるいはカスタマーに balance 要素が設定されている場合には、modify do replace value of $new/customerinfo/balance with $new/customerinfo/balance + 1000 のようにして値を増やすこともできます。
スキーマ検証を使用した更新
XML 文書を変更する際に、文書が特定の XML スキーマに変わらず準拠しているかどうかを検証したいという場合があります。更新 4 は XMLVALIDATE 関数を使用して、更新された文書が「cust.custschema」という ID を持つ XML スキーマに従った有効なものであるかどうかをチェックする例です。変更後の文書が有効でない場合、UPDATE 文を実行しても失敗します。
更新 4
update xmlcustomer
set info = xmlvalidate(xmlquery('
copy $new := $INFO
modify do replace value of $new/customerinfo/phone
with "905-477-9011"
return $new ')
according to xmlschema id "cust.custschema")
where cid = 1000; |
XML の更新例
今まで見てきた例では、「replace value of」操作を使用して要素ノードの値を変更しました。以降の例では XML 文書の要素および属性ノードの削除、挿入、名前変更などの操作を実行する方法を説明します。
ノードの削除
更新 5 は、MODIFY 節で「delete」操作を使用して XML 文書から「phone」要素を削除する例です。この例では、ただ単に削除するノード (複数可) のパスを指定して文書から該当するノードを削除しています。MODIFY 節以外はすべて、前に説明した更新とまったく変わらないことに注目してください。実際、さまざまな種類の更新を説明する、以降の例では、MODIFY 節しか変更していません。
更新 5
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do delete $new/customerinfo/phone
return $new')
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
</customerinfo> |
|
ノード・レベルの更新はすべて、属性にも適用することができます。更新 6 は属性を削除する例です。ここでは、「phone」要素の属性「type」を削除しています。
更新 6
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do delete $new/customerinfo/phone/@type
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone>963-289-4136</phone>
</customerinfo> |
|
ノードの名前変更
要素、属性、または処理命令ノードに別の名前を指定するには、「rename」操作を使用します。これ以外のノード (テキスト・ノードや XML コメントなど) にはノード名がないため、名前の変更はできません。更新 7 に示す名前変更の例では、要素の名前を「addr」から「address」に変更しています。この更新では特定の行を選択する SQL の WHERE 節を使っていないので、テーブル内のすべての行 (文書) が変更されることになります。そのため、この操作には単一の文書を更新するよりも時間がかかるはずです。
更新 7
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do rename $new/customerinfo/addr as "address"
return $new' ) ; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<address country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</address>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<address country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</address>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
ノードの置換
更新 8 は、「replace」操作を使って既存の「phone」要素を新しい要素に置き換える例です。「replace」操作と「replace value of」操作の動作は異なります。前者はノード全体を置き換える (古いノードは削除されます) 一方、後者はノードのコンテンツのみを置き換えます。更新 8で更新の MODIFY 節に新しい「phone」要素全体が指定されているのは、このためです。
更新 8
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do replace $new/customerinfo/phone with
<phone type="home">416-123-4567</phone>
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="home">416-123-4567</phone>
</customerinfo> |
|
複数ノードの値の置換
当然、単一の UPDATE 文で同じ文書に複数の変更を加えることも可能です。更新 9 を見ると、MODIFY 節には複数の更新操作が含まれています。このように複数の更新操作をコンマで区切って括弧で囲むと、更新操作の種類が同じであるか異なるかによらず、複数の更新操作を組み込むことができます。更新 9 の更新操作はどちらも「replace value of」型となっています。この 2 つの更新はそれぞれ、「phone」要素の値と @type 属性の値を置き変えます。面白いことに、これらの操作による最終的な結果は更新 8 と同じです。
更新 9
update xmlcustomer
set info = xmlquery('
copy $new := $INFO
modify (do replace value of $new/customerinfo/phone with "416-123-4567",
do replace value of $new/customerinfo/phone/@type with "home")
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="home">416-123-4567</phone>
</customerinfo> |
|
ノードの挿入
「insert」操作では、XML 文書に要素や属性などの新規ノードを追加することができます。新規ノードを挿入する文書内の位置を指定するには 5 通りの方法があります。例えば新しい要素を文書に挿入する場合の挿入書式は、以下のとおりです。
-
insert
要素1
into
要素2
要素1 は要素2 の子として挿入されます。要素2 が持つ既存の子のなかでの要素1 の位置は不確定です。
-
insert
要素1
as last into
要素2
要素1 は要素2 の最後の子として挿入されます。
-
insert
要素1
as first into
要素2
要素1 は要素2 の最初の子として挿入されます。
-
insert
要素1
after
要素2
要素1 は要素2 の兄弟として、要素2 の直後に挿入されます。
-
insert
要素1
before
要素2
要素1 は要素2 の兄弟として、要素2 の直前に挿入されます。
要素1 の代わりに新しい属性を挿入するとしても、「into
要素2」、「as last into
要素2」、および「as first into
要素2」の各操作による結果はいずれも同じで、属性は要素2 に追加されます。ただし、XML データ・モデルでは要素での属性の配置順は定義していないことに注意してください。したがって、last、first、before、および after によって属性の順序が左右されることはありません。「属性
before または after
要素2」として属性を挿入すると、属性は要素2 の親に追加されます。
上記にリストした書式では、要素1 をノードのシーケンスとなる式にすることもできます。その場合、シーケンスに含まれるすべてのノードが指定された位置に挿入されます。また、要素がネストされた XML フラグメント全体を挿入する場合には、要素1 をその XML フラグメントのルートにすることもできます。
それではここで、いくつかの例を見てみましょう。まず更新 10 は、2 つ目の電話番号をサンプル文書に挿入する例です。「into $new/customerinfo」とすることで、「customerinfo」要素は明示的に新規「phone」要素の親として指定されます。
更新 10
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do insert <phone type="cell">777-555-3333</phone>
into $new/customerinfo
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">416-123-4567</phone>
<phone type="cell">777-555-3333</phone>
</customerinfo> |
|
更新 10 の結果では、新しい「phone」要素は「customerinfo」の最後の子になっています。しかしこの位置が保証されるのは、更新 11 のように「as last into」を明示的に指定した場合に限られます。
更新 11
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do insert<phone type="cell">777-555-3333</phone>
as last into $new/customerinfo
return $new' )
where cid = 1000; |
|
この新規の携帯電話番号を文書内の最初の「phone」要素にしたいのであれば、既存の「phone」要素の最初のオカレンスの直前に携帯電話番号が挿入されるよう明示的に要求します。このように要求しているのが、更新 12 です。
更新 12
update xmlcustomer
set info = xmlquery( 'copy $new := $INFO
modify do insert <phone type="cell">777-555-3333</phone>
before $new/customerinfo/phone[1]
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="cell">777-555-3333</phone>
<phone type="work">416-123-4567</phone>
</customerinfo> |
|
同様に、更新 13 に示すとおり、新規「phone」要素をカスタマー名の直後に挿入することもできます。
更新 13
update xmlcustomer
set info = xmlquery( 'copy $new := $INFO
modify do insert <phone type="cell">777-555-3333</phone>
after $new/customerinfo/name
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<phone type="cell">777-555-3333</phone>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">416-123-4567</phone>
</customerinfo> |
|
更新 14 では変換式の中でリレーショナル列の値「CID」を使用して、新規要素「customerid」をルート要素「customerinfo」の最初の子要素にしています。
更新 14
update xmlcustomer
set info = xmlquery( 'copy $new := $INFO
modify do insert <customerid>{ $CID } </customerid>
as first into $new/customerinfo
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<customerid>1000</customerid>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">416-123-4567</phone>
</customerinfo> |
|
「customerid」を「customerinfo」要素の属性にするには、挿入式にいわゆる計算による属性コンストラクターを含める必要があります。このコンストラクターは attribute キーワードと、それに続く目的の属性名、そしてその属性値の式で構成されます。更新 15 はその一例です。
更新 15
update xmlcustomer
set info = xmlquery('
copy $new := $INFO
modify do insert
attribute customerid { $CID }
into $new/customerinfo
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo customerid="1000">
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">416-123-4567</phone>
</customerinfo> |
|
よくある落とし穴とその解決策
ここまでで説明した更新はいずれも単純明快でわかりやすい例でしたが、これよりも複雑な更新に手を付け始めると、間違いを犯して DB2 に更新式が受け付けられないような状況を招くことも考えられます。そこで、以降のセクションでは陥りがちな落とし穴を取り上げ、単純な解決策を提案します。
要素を文書ノードに挿入する
XML データ・モデルでは、構文解析済みの整形式 XML 文書が持つ文書ノードは 1 つだけで、そのノードは文書のルート要素 (customerinfo) の親であることが条件となっています。この文書ノードは XML 文書のテキスト (シリアライズされた) 表現には現れません。
新しい要素をルート要素ではなく文書ノードに挿入してしまうという間違いはよくあります。更新 16 は更新 10 とほとんど同じですが、この場合、挿入は「into $new/customerinfo」ではなく「into $new」として指定されています。これはつまり、更新 16 では新規「phone」要素を「customerinfo」要素の子としてではなく、兄弟として挿入するということです。その結果、ノードは 2 つの要素 (customerinfo と phone) からなる XML シーケンスになってしまいます。そうなると、整形式 XML 文書にはなりません。DB2 の XML 列には整形式 XML 文書しか含められないため、この更新は失敗します。
更新 16
update xmlcustomer
set info = xmlquery( '
copy $new := $INFO
modify do insert <phone type="cell">777-555-3333</phone> into $new
return $new' )
where cid = 1000; |
|
Error Message:
SQL20345N The XML value is not a well-formed document with a single root element. SQLSTATE=2200L
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
|
| | 元の XML 文書: | 受け付けられなかった XML 値: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo>
<phone type="cell">777-555-3333</phone> |
|
繰り返しノードを変更する
単一の XPath 式が 1 つの文書内で複数のノードを識別する場合、これらのノードは繰り返しノードと呼ばれます。例えば、サンプル・テーブルの 2 番目の文書には 2 つの「phone」要素が含まれるため、/customerinfo/phone という式は 2 つの要素のシーケンスを表すことになります。ノードのシーケンスに作用する操作は「delete」だけです。この操作は、更新 17 に示すようにシーケンスに含まれるすべてのノードを削除します。
更新 17
update xmlcustomer
set info = xmlquery( 'copy $new := $INFO
modify do delete $new/customerinfo/phone
return $new' )
where cid = 1001; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
</customerinfo> |
|
「phone」要素のいずれか 1 つだけを削除するには、MODIFY 節で
modify do delete $new/customerinfo/phone[@type = "work"] といった述部を使用します。
更新 18 では「phone」要素の値を削除するのではなく置換しようとしていますが、DB2 が実行時に複数の「phone」要素があることを検出すると、SQL16085N エラーを返します。DB2 コマンド・プロンプトで ? SQL16085N と入力すると、このエラーの考えられる理由のリストが表示されます。エラー・メッセージに示される理由コード「XUTY0008」によってすぐに、「置換式のターゲット・ノードが単一のノードではない」ことが原因だとわかります。
更新 18
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do replace value of $new/customerinfo/phone with "444-444-4444"
return $new')
where cid = 1001; |
|
Error message:
SQL16085N The target node of an XQuery "replace value of" expression is not valid.
Error QName=err:XUTY0008. SQLSTATE=10703 |
|
このエラーが原因で、すべての「phone」要素を同じ番号に更新できないわけですが、そもそも「phone」要素をすべて同じ番号にするのは意味がありません。複数のノードをどうしても同じように更新しなければならない場合には、XQuery の「for」式を使ってノードを繰り返し処理するという手段があります (更新 19 を参照)。
更新 19
update xmlcustomer
set info = xmlquery( 'copy $new := $INFO
modify for $j in $new/customerinfo/phone return
do replace value of $j with "444-444-4444"
return $new' )
where cid = 1001; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">444-444-4444</telephone>
<phone type="cell">444-444-4444</telephone>
</customerinfo> |
|
ただし、たいていの場合、変更しなければならないのは複数のノードのうちの 1 つだけです。更新 20 では、MODIFY 節の中で述部を使って携帯電話番号だけを変更しています。
更新 20
update xmlcustomer
set info = xmlquery( 'copy $new := $INFO
modify do replace value of $new/customerinfo/phone[@type = "cell"]
with "444-444-4444"
return $new' )
where cid = 1001; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">444-444-4444</phone>
</customerinfo> |
|
存在しないノードを変更しようとする
前のセクションでも説明したように、繰り返しの要素などからなるノードのシーケンスには「delete」操作しか適用できません。その他の「rename」や「replace」などの操作には、関係するノードを繰り返し処理する「for」式が必要になります (更新 19 を参照)。これは、更新操作のターゲットが空の場合にも当てはまります。例えば、更新 21 では @type = "home" となっている「phone」要素の値を置換しようとしていますが、サンプル文書には自宅の電話番号は含まれていません。そのため $new/customerinfo/phone[@type = "home"] 式の結果は空となり、更新 21 は失敗します。存在しない自宅の電話番号を削除しようとする「delete」操作は成功しますが、文書には何の効果も現れません。
更新 21
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify do replace value of $new/customerinfo/phone[@type = "home"]
with "777-777-7777"
return $new')
where cid = 1001; |
|
SQL16085N The target node of an XQuery "replace value of" expression is not
valid. Error QName=err:XUTY0008. SQLSTATE=10703 |
|
この場合も、理由コード「XUTY0008」が「置換式のターゲット・ノードが単一のノードではない」ことを示しています。更新 18 での「単一のノードではない」とは複数のノードがあることを意味していましたが、更新 21 で「単一のノードではない」という場合、これはノードが 1 つ未満、つまり存在していないことを意味します。この問題を解決できるのも同じく「for」による繰り返しです。この方法を用いた更新 22 は成功するとは言え、更新後の文書には何の変わりもありません。
更新 22
update xmlcustomer
set info = xmlquery( '
copy $new := $INFO
modify for $j in $new/customerinfo/phone[@type = "home"] return
do replace value of $j with "777-777-7777"
return $new' )
where cid = 1001; |
| | 更新前の XML 文書: | 更新後の XML 文書 (更新前と同じ) |
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
代わりの手段となるのは、XQuery if-then-else 式を使って条件付きで自宅の電話番号を更新し、あるいは自宅の電話番号がない場合には新しく挿入することです。この場合の方法を、更新 23 に示します。
更新 23
update xmlcustomer
set info = xmlquery('
copy $new := $INFO
modify if ($new/customerinfo/phone[@type="home"]) then
do replace value of $new/customerinfo/phone[@type="home"]
with "777-777-7777"
else do insert <phone type="home">777-777-7777</phone>
as last into $new/customerinfo
return $new' )
where cid = 1001; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
<phone type="home">777-777-7777</phone>
</customerinfo> |
|
同じノードを何度も変更する
更新 9 からわかるように、1 つの UPDATE 文の MODIFY 節には同じ文書に対する複数の更新操作を含められます。ただし、同じノードの名前変更や置換、あるいは値の変更を実行できるのは 1 度限りです。一般に、このような操作を何度も行うのは意味がありません。更新 24 はその一例です。この更新は、実行時に DB2 が両方の「rename」操作が同じ「phone」要素を対象としていることを検出した時点で失敗します。
更新 24
update xmlcustomer
set info = xmlquery('copy $new := $INFO
modify(
do rename $new/customerinfo/phone[@type="work"] as "telephone",
do rename $new/customerinfo/phone[. ="222-222-2222"] as "telephone" )
return $new')
where cid = 1001; |
|
Error Message:
SQL16083N Incompatible "rename" expressions exist in the modify clause of a
Transform expression. Error QName=err:XUDY0015. SQLSTATE=10704 |
|
ここで必要となるのは、更新操作の述部が確実にそれぞれ別のノードを選択するようにすることです。これを説明している更新 25 には、$new/customerinfo/phone を対象とした 2 つの「replace value of」式があります。式の一方は勤務先の電話番号に適用され、もう一方は自宅の電話番号に適用されるため、SQL16083N エラーにはなりません。また、更新 22 と同様に「for」による繰り返しを使用して、勤務先の電話番号または自宅の電話番号が存在しなくても UPDATE 文が失敗しないようにしています。このパターンは一般に、オプション要素とスキーマの多様性を扱うのに役立ちます。
更新 25
update xmlcustomer
set info = xmlquery('
copy $new := $INFO
modify (for $x in $new/customerinfo/phone[@type="work"]
return do replace value of $x with "444-444-4444" ,
for $y in $new/customerinfo/phone[@type="home"]
return do replace value of $y with "555-555-5555")
return $new')
where cid = 1001; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">222-222-2222</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
<customerinfo>
<name>David Patterson</name>
<addr country="Canada">
<street>Fifth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">444-444-4444</phone>
<phone type="cell">333-333-3333</phone>
</customerinfo> |
|
copy 節でノードを選択する
これまでに紹介した更新例では、いずれも copy $new := $INFO という同じ「copy」節を使用しました。常に同じなのに、どうしてこの構文を毎回使用しているのか疑問に思われるかもしれませんが、その理由は、copy 節の柔軟性が極めて重宝する場合があるからです。例えば、アプリケーションで特定のカスタマーの情報を取得する必要がある一方、プライバシー上の理由からカスタマーの電話番号は除外しなければならないとします。まさにこの条件を満たすのが、更新 26 と更新 27 です。更新 26 では SQL 述部で文書を選択している一方、更新 27 では XQuery 述部を使って SQL をまったく使用しないようにしています。これらのクエリーは、文書をクエリー処理中にその場で変換するため、テーブル内の文書は変更されません。
更新 26
xquery
copy $new := db2-fn:sqlquery("select info from xmlcustomer where cid = 1000")
modify do delete $new/customerinfo/phone
return $new; |
| | クエリーの実行結果: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
</customerinfo> |
|
更新 27
xquery
copy $new := db2-fn:xmlcolumn("XMLCUSTOMER.INFO")[/customerinfo/name="John Smith"]
modify do delete $new/customerinfo/phone
return $new; |
| | クエリーの実行結果: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
</customerinfo> |
|
COPY 節の右側は、1 つの項目 (1 つの文書など) だけに評価されなければなりません。「John Smith」の名前で複数のカスタマーがいたり、あるいはカスタマーがいない場合には、DB2 は以下のエラーを返します。
SQL16084N An assigned value in the copy clause of a transform expression is not a
sequence with exactly one item that is a node. Error QName=err:XUTY0013. SQLSTATE=10705 |
更新 28 では、カスタマーを繰り返し処理することによってこのエラーを防いでいます。
更新 28
xquery
for $i in db2-fn:xmlcolumn("XMLCUSTOMER.INFO")[/customerinfo/name="John Smith"]
return
copy $new := $i
modify do delete $new/customerinfo/phone
return $new; |
| | クエリー結果: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
</customerinfo> |
|
もう 1 つの例として、更新 29 では COPY 節を使ってカスタマーの住所のみを取得し、RETURN 節を使って変更済みの住所を新しい「sendto」要素に組み込んでいます。
更新 29
xquery
for $i in db2-fn:xmlcolumn("XMLCUSTOMER.INFO")/customerinfo[name="John Smith"]
return
copy $new := $i/addr
modify do rename $new/zipcode as "postalcode"
return <sendto>{$new}</sendto>; |
| | 元の XML 文書: | クエリーの実行結果: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<sendto>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<postalcode>M1T 2A9</postalcode>
</addr>
</sendto> |
|
SQL の UPDATE 文で変換式を使用するときには、フィルタリング述部を COPY 節で使うことはめったになく、通常は SQL の WHERE 節で使用します。
複数の更新操作を組み合わせる
新たに登場した XQuery 更新の標準では、MODIFY 節に含まれるすべての更新操作はそれぞれ独立して元の文書に適用されると指定しています。操作が互いの結果を知ることはありません。これは「スナップショット動作」と呼ばれます。つまり、それぞれの更新操作は論理的に元の文書の個別のスナップショットに適用されます。この動作を説明しているのが、2 つの更新操作が含まれる更新 30 です。最初の更新操作は追加の「phone」要素を挿入し、2 番目の更新操作はすべての「phone」要素を「telephone」に名前変更します。ただし、名前変更は元の文書の「phone」要素にだけ適用され、同じ UPDATE 文で追加された新しい「phone」要素には適用されないようになっています。MODIFY 節内での更新操作の順序は関係しません。
更新 30
update xmlcustomer
set info = xmlquery( 'copy $new := $INFO
modify ( do insert <phone type="cell">777-555-3333</phone>
after $new/customerinfo/addr ,
for $j in $new/customerinfo/phone
return do rename $j as "telephone" )
return $new' )
where cid = 1000; |
| | 更新前の XML 文書: | 更新後の XML 文書: |
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="work">963-289-4136</phone>
</customerinfo> |
|
<customerinfo>
<name>John Smith</name>
<addr country="Canada">
<street>Fourth</street>
<city>Calgary</city>
<state>Alberta</state>
<zipcode>M1T 2A9</zipcode>
</addr>
<phone type="cell">777-555-3333</phone>
<telephone type="work">963-289-4136</telephone>
</customerinfo> |
|
別の例として、単一の UPDATE 文で「addr」要素を削除するとともに新しい要素「POBox」を /customerinfo/addr に挿入する場合を考えてみてください。「POBox」要素の親要素である「addr」は削除されるため、この新規要素は更新された文書に現れません。
最終的な XML 構造が XML の基本原則に違反する場合には、更新は受け付けられません。例えば、XML 要素は同じ名前の属性を 2 つ持つことはできません。そのため、要素に追加しようとしている属性と同じ名前の要素がすでにある場合、DB2 は更新を受け付けません。
まとめ
DB2 9.5 が導入する XQuery Update Facility によって、XML 文書内に含まれる個別の要素と属性の名前変更、挿入、削除、置換、それに値の変更が可能になり、XML データを簡単かつ効率的に更新できるようになります。この更新機能を利用すれば、XML を構文解析したり、アプリケーションに文書を読み込ませることなく XML 列の文書を変更することができます。この記事に記載した例は、更新機能の概要を説明するものです。これらの例を手本にして、独自の XML 更新を作成してください。
謝辞
レビューと有益なコメントでこの記事の改善に協力してくれた Don Chamberlin、Cindy Saracco、Henrik Loeser、Susanne Englert、そして Mel Kiyama の各氏に感謝します。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Script for queries in this article | sample.sql | 15KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
- DB2 を無料で使用するには、コミュニティー向け DB2 Express Edition の無料バージョン、DB2 Express-C をダウンロードしてください。DB2 Express Edtion と同じコア・データ機能を備えた DB2 Express-C は、アプリケーションをビルドしてデプロイするための安定した基盤になります。
- 次期開発プロジェクトの構築に、developerWorks から直接ダウンロードできるIBM の体験版ソフトウェアをご利用ください。
-
DB2 Enterprise 9 の無料の試用版をダウンロードしてください。
-
IBM 製品の評価版をダウンロードして、DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® のアプリケーション開発ツールとミドルウェア製品を使ってみてください。
議論するために
著者について  | 
|  | Nicola 博士は、IBM Silicon Valley Lab の XML データベース・パフォーマンスの技術リーダーです。彼の業務は、XQuery、SQL/XML、また DB2 ネイティブの XML 機能を含め、DB2 での XML パフォーマンスのあらゆる側面を対象としています。Nicola 博士は DB2 XML 開発チームや XML を使用する顧客やビジネス・パートナーと緊密に業務を進めながら、XML ソリューションの設計や実装、最適化に協力しています。IBM に入社する前には、Informix Software でデータ・ウェアハウスのパフォーマンスに関する業務を行っていました。また、分散データベースや複製データベースに関する研究と業界プロジェクトにも 4 年間携わっていました。彼は、1999年にドイツの Technical University of Aachen でコンピューター・サイエンスの博士号を取得しています。 |
 | 
|  | Uttam Jain は Advisory Software Engineer であり、DB2 pureXML ストレージの技術リーダーを務めています。IBM Silicon Valley Laboratory に入社したのは 2001年で、DB2 pureXML チームに加わる前は、DB2 でのオブジェクト・リレーショナル構造の実装に従事した経験を持ちます。彼は 2000年に University of Florida で電気工学およびコンピューター・エンジニアリングの修士号を取得しました。現在は、XQuery、SQL/XML、そして分散 XML データベースなどを専門としています。 |
記事の評価
|