PHP による SQL から XML への変換

データベース・クエリーの結果を簡単に XML 文書に変換する

SQL 結果セットを XML に変換する簡単な方法が欲しい思ったことはありませんか? その願いを叶えるのが PEAR パッケージ、XML_Query2XML です。このパッケージはデータベース・クエリーの結果をカスタマイズ可能な XML 文書に効率的に変換するための包括的フレームワークになります。XML_Query2XML を紹介するこの記事では、実用的なアプリケーションを例に、このパッケージを XSL と XPath で使用する方法、外部 Web サービスからのデータと組み合わせる方法、そしてデータベース・ダンプ・ファイルを作成する方法を説明します。

はじめに

PEAR (PHP Extension and Application Repository) についてはきっともう耳にしたことがあると思いますが、このコミュニティー主導のプロジェクトは、広範で高品質のコードを集めたオープン・ソース・ライブラリーを提供し、PHP 開発者が素早くアプリケーションを開発できるよう支援することを目的としています。Perl の CPAN リポジトリーとコンセプトの点で似ている PEAR は長いこと、私が興味深くて便利な PHP+XML ウィジェットを探す際にまず初めに訪れる場所となっています。このリポジトリーには、PHPデータ構造を XML オブジェクトとしてシリアライズするのに重宝な XML_Serializer クラス、Mozilla XUL アプリケーションを作成するための API を提供する XML_XUL クラス、そして SVG 形式のベクター・グラフィックをプログラムによって作成するメソッドを揃えた XML_SVG クラスなどがあります。

この記事では、PEAR の XML セクションに含まれるもう 1 つのメンバー、XML_Query2XML クラスを紹介します。このクラスが提供するのは、SQL 結果セットを整形式 XML に簡単かつ効率的に変換する API です。ほんの少し想像力を働かせれば、この出力を XSL 変換によって別の形式に変換したり、他の XML ベースのアプリケーションで統合するのもわけありません。


必要なソフトウェアをインストールする

Lukas Feiler によって積極的に開発され、保守されている XML_Query2XML パッケージは、LGPL ライセンスのもとで PHP コミュニティーに公開されています。このパッケージには PHP 5.0 以降が必要です。これをインストールするには、PHP ビルドにデフォルトで含まれている自動 PEAR インストーラーを使うのが最も簡単で、ただ単に以下のコマンドをシェル・プロンプトで実行するだけでインストールすることができます。

shell>pear install XML_Query2XML

このコマンドを実行すると、PEAR インストーラーが PEAR パッケージ・サーバーに接続し、パッケージをダウンロードしてシステムの該当する場所にインストールしてくれます。

パッケージを手作業でインストールするには、PEAR Web サイトのホーム・ページにアクセスしてパッケージ・アーカイブをダウンロードし、目的の場所にファイルを解凍してください。ただし手動インストール・プロセスでは、PEAR のパッケージ編成構造についての知識があることが前提となります。

この段階で、以下の依存関係も認識しておく必要があります。

  1. XML_Query2XML は、DB、MDB2、または ADOdb データベース抽象化レイヤーのうちの 1 つを使用してターゲット RDBMS と通信するため、これらの抽象化レイヤーのいずれかが必要であり、該当するデータベース・ドライバーと併せて正しくインストールされている必要があります。この記事の例で使用するのは、PEAR パッケージ・ツリーに含まれる MDB2 抽象化レイヤーとその MySQL ドライバーである MDB2_Driver_mysql です。前述したように、この両方のパッケージは PEAR 自動インストーラーを使ってインストールすることも、PEAR Web サイトからダウンロードすることもできます。
  2. この記事の例で利用する MySQL のサンプル world データベースには、都市と国のデータが含まれる各種のテーブルが事前に設定されてリンクされています。world データベースを入手してセットアップする手順については、この記事の「参考文献」セクションを参照してください。
  3. この記事の例には、PHP の DOM、XSL、および SimpleXML 関数をサポートする PHPビルドが必要です。PHP 5.x では、これらの関数がデフォルトで有効に設定されます。
  4. PHP の DOM および SimpleXML 関数、そして XML、XPath、XSL 技術についての実用的な知識があることが前提となります。

上記の各コンポーネントの情報およびダウンロード・リンクについては、「参考文献」を参照してください。

この記事に記載するすべての例は、XML_Query2XML Version 1.2.1 でテスト済みです。


SQL を XML に変換する

必要なすべてのコンポーネントを正常にインストールしたら、早速、以下の単純な PHP スクリプトで XML_Query2XML を試してみてください。

リスト 1. 単純な SQL から XML への変換
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

try {
    // initialize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
    
    // generate SQL query
    // get results as XML
    $sql = "SELECT * FROM Country";
    $xml = $q2x->getFlatXML($sql);
    
    // send output to browser
    header('Content-Type: text/xml');
    $xml->formatOutput = true;
    echo $xml->saveXML();
} catch (Exception $e) {
    echo $e->getMessage();
}
?>

上記のスクリプトは、XML_Query2XML クラスの基本的な使用方法を説明するものです。まず、このスクリプトは XML_Query2XML および MDB2 クラス・ファイルを組み込み、その factory() メソッドを使って MDB2 抽象化レイヤーのインスタンスを初期化しています。このメソッドが入力として受け入れる DSN には、RDBMS 型、RDBMS ユーザー名とパスワード、そしてターゲット・データベース名に関する情報が含まれます。MDB2 インスタンスが初期化されると、このインスタンスによって $q2x オブジェクトで表された XML_Query2XML インスタンスが初期化されます。

DSN を組み立てて XML_Query2XML オブジェクト・インスタンスを作成したら、今度は実際に RDBMS で SQL クエリーを実行し、その結果を XML に変換してみてください。そのために使用するのは、単純な SELECT 型クエリーに一般的に使用される XML_Query2XML の getFlatXML() メソッドです。このメソッドの出力は整形式 XML 文書で、この文書のなかに SQL 結果セットがエンコードされます。以下はその一例です。

リスト 2. リスト 1 によって生成された XML 出力 (要約)
<?xml version="1.0" encoding="UTF-8"?>
<root>
  <row>
    <code>AFG</code>
    <name>Afghanistan</name>
    <continent>Asia</continent>
    <region>Southern and Central Asia</region>
    <surfacearea>652090.00</surfacearea>
    <indepyear>1919</indepyear>
    <population>22720000</population>
    <lifeexpectancy>45.9</lifeexpectancy>
    <gnp>5976.00</gnp>
    <gnpold></gnpold>
    <localname>Afganistan/Afqanestan</localname>
    <governmentform>Islamic Emirate</governmentform>
    <headofstate>Mohammad Omar</headofstate>
    <capital>1</capital>
    <code2>AF</code2>
  </row>
  <row>
    <code>NLD</code>
    <name>Netherlands</name>
    <continent>Europe</continent>
    <region>Western Europe</region>
    <surfacearea>41526.00</surfacearea>
    <indepyear>1581</indepyear>
    <population>15864000</population>
    <lifeexpectancy>78.3</lifeexpectancy>
    <gnp>371362.00</gnp>
    <gnpold>360478.00</gnpold>
    <localname>Nederland</localname>
    <governmentform>Constitutional Monarchy</governmentform>
    <headofstate>Beatrix</headofstate>
    <capital>5</capital>
    <code2>NL</code2>
  </row>
  <row>
    <code>ANT</code>
    <name>Netherlands Antilles</name>
    <continent>North America</continent>
    <region>Caribbean</region>
    <surfacearea>800.00</surfacearea>
    <indepyear></indepyear>
    <population>217000</population>
    <lifeexpectancy>74.7</lifeexpectancy>
    <gnp>1941.00</gnp>
    <gnpold></gnpold>
    <localname>Nederlandse Antillen</localname>
    <governmentform>Nonmetropolitan Territory of 
    The Netherlands</governmentform>
    <headofstate>Beatrix</headofstate>
    <capital>33</capital>
    <code2>AN</code2>
  </row>
  ...
</root>

上記の出力された XML をよく見てみると明らかな構造がわかってきます。つまり、SQL 結果セットの各レコードは <row> 要素として表現され、レコードの個々のフィールドはそれぞれに対応する <row> 内にネストされます。ネストされた要素の名前はクエリーの対象のテーブルに含まれるフィールドの名前に対応し、文書要素 (XML ツリーのルート) はそのものずばり、<root> という名前です。


XSL で XML 出力を変換する

もちろん、SQL クエリーから XML を生成するのは、通常、作業の半分でしかありません。後の半分には生成した XML での操作が関わってきます。XML 文書ではさまざまなことができますが、なかでもよく行われる作業には XSL 変換が伴います。XSL 変換によって、XML 文書を HTML や RSS などの別の形式に変換するというわけです。この点を考慮して、今度は簡単な XSL スタイルシートを組み立ててリスト 2 の XML 出力を単純な HTML ページに変換してみましょう。

リスト 3. XSL スタイルシート
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:template match="/root">
        <html>
            <head>
                <style type="text/css">
                td { text-align: center; padding: 3px; }
                .head { font-style: italic; }
                </style>
            </head>
            <body>
                <table border="1">
                    <thead>
                        <tr>
                            <xsl:for-each select="row[1]/*">
                                <td class="head">
                                    <xsl:value-of select="local-name(.)"/>
                                </td>
                            </xsl:for-each>
                        </tr>
                    </thead>
                    <tbody>
                        <xsl:apply-templates/>
                    </tbody>
                </table>
            </body>
        </html>
    </xsl:template>

    <xsl:template match="row">
        <tr>
            <xsl:apply-templates/>
        </tr>
    </xsl:template>

    <xsl:template match="row/*">
        <td>
            <xsl:value-of select="."/>
        </td>
    </xsl:template>
</xsl:stylesheet>

リスト 4 は修正後の PHP スクリプトです。このスクリプトでは、PHP の XSL 関数を使って XML_Query2XMLで生成された出力を変換するようになっています。

リスト 4. XSL による SQL から XML への出力の変換
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

try {
    // initalize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
    
    // generate SQL query
    // get results as XML
    $sql = "SELECT * FROM Country";
    $xml = $q2x->getFlatXML($sql);
    
    // read XSL stylesheet data
    $xsl = new DOMDocument;
    $xsl->load('country.xsl');
    
    // initialize XSLT engine
    $xslp = new XSLTProcessor;
    
    // attach XSL stylesheet object
    $xslp->importStyleSheet($xsl); 
    
    // perform transformation
    header('Content-Type: text/html');
    echo $xslp->transformToXML($xml);
} catch (Exception $e) {
    echo $e->getMessage();
}
?>

スクリプトの最初の部分ではリスト 1 と同様に、SQL クエリーの結果が含まれる XML 文書を生成し、この文書を DOMDocument インスタンスとして $xml に保存しています。それから、XSLTProcessor クラスのインスタンスを初期化し、このクラスの importStyleSheet() メソッドを使って XSL スタイルシートをインポートします。次に使用しているのは transformToXML() メソッドです。ソース XML データを入力引数として受け入れるこのメソッドにより、XSL スタイルシートに指定されたルールを使用して XML 文書を HTML ページに変換しています。

この出力は図 1 のようになります。

図 1. リスト 4 で生成された HTML 文書
リスト 4 で生成された HTML 文書

XML 出力をカスタマイズする

これまでの例で説明した getFlatXML() メソッドは、SQL から XML に手軽に変換することだけが目的なら言うことありません。しかし例えば、特定の結果セット・フィールドを要素ではなく属性として示したり、独自の要素名を定義するなどといった複雑なことが必要な場合には、XML_Query2XML の getXML() メソッドを使ってみてください。このメソッドを使えば、構造やスタイルを含め、出力される XML を大々的にカスタマイズすることができます。

リスト 5 に一例を記載します。

リスト 5. SQL から XML への出力のカスタマイズ
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

try {
    // initalize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
    
    // generate SQL query
    // get results as XML
    $sql = "SELECT * FROM Country";
    $xml = $q2x->getXML($sql, array(
            'idColumn' => 'code',
            'rootTag' => 'countries',
            'rowTag' => 'country',
            'attributes' => array('code'),
            'elements' => array('name', 'continent', 'area' => 'surfacearea')
        )    
    );
    
    // send output to browser
    header('Content-Type: text/xml');
    $xml->formatOutput = true;
    echo $xml->saveXML();
} catch (Exception $e) {
    echo $e->getMessage();
}
?>

getXML() メソッドは 2 つの引数を受け入れます。1 つは実行する SQL クエリー、そしてもう 1 つは XML 出力の形式を定義するオプションの配列です。表 1 に、上記のリストにある各オプションの意味を記載します。

表 1. getXML() メソッドのオプション

オプション制御内容
rootTag文書要素の名前 (デフォルト: root)
rowTagそれぞれの結果列を表す要素の名前 (デフォルト: row)
idColumn結果セットの主キー・フィールド
attributesXML 属性として表示するフィールドのリスト
elementsXML 要素として表示するフィールドのリスト

リスト 6 は、スクリプトの出力例です。

リスト 6. リスト 5 によって生成された XML 出力 (要約)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
  <country code="AFG">
    <name>Afghanistan</name>
    <continent>Asia</continent>
    <area>652090.00</area>
  </country>
  <country code="NLD">
    <name>Netherlands</name>
    <continent>Europe</continent>
    <area>41526.00</area>
  </country>
  <country code="ANT">
    <name>Netherlands Antilles</name>
    <continent>North America</continent>
    <area>800.00</area>
  </country>
  ...
</countries>

注目する点は、この XML 文書には SQL 結果セットのすべてのフィールドが含まれるわけではなく、elements 配列と attributes 配列に指定されたフィールドだけが含まれること、そして attributes 配列に指定されたフィールドは子ノードではなく各 <country> 要素の属性として表示されることです。

また、出力される XML の要素名と属性名は対応するフィールド名にデフォルト設定されると前に説明しましたが、getXML() メソッドでは、attributes 配列と elements 配列にキーと値のペアとして別の値を指定することで、これらのデフォルト名を変えることができます。その適例として、SQL 結果セットで surfacearea という名前になっているフィールドは、XML 出力では単なる <area> 要素として表示されています。

XML_Query2XML マニュアルには、この他にも getXML() メソッド出力のカスタマイズ例が記載されているので調べてみてください (「参考文献」を参照)。


SQL の結合操作を行う

XML_Query2XML は、XML を使用して結果セットの内容を別の結果セットの内容にネストするためのフレームワークにもなります。この機能がよく使用されるのは、結合 (あるいは何らかの方法で関連付けられているクエリー) の操作をするときです。また、パフォーマンス上の理由で 1 つの大きなクエリーを複数の小さなクエリーに分割する際にも役立ちます。

この機能をよく理解できるよう、world データベースに戻って、そこに含まれる 2 つのテーブル、Country City を例に用いて説明します。この 2 つのテーブルは code 外部キーによって互いに関連付けられています。

例えば、<country> 要素のなかに複数の <city> 要素がネストされている XML 文書ツリーを生成するとします。さらに、国ごとに人口の多い上位 5 位までの都市だけを出力し、フィールド値を要素ではなく属性として表示するという設定にします。つまり想定される XML 文書は、以下のサンプルのようなものです。

リスト 7. SQL 結合の完了後に想定される XML 出力 (要約)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
  <country code="IND" name="India" region="Southern and Central Asia">
    <cities>
      <city name="Mumbai (Bombay)" district="Maharashtra" population="10500000"/>
      <city .../>
      <city .../>
      <city .../>
      <city .../>
    </cities>
  </country>
  <country ...>
      ...
  </country>
  ...
</countries>

このようなネスト構造の XML 文書を生成するために必要なのは、リスト 8 のコードです。

リスト 8. SQL結合によってカスタマイズした XML 出力の作成
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

try {
    // initalize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
    
    // generate SQL query
    // get results as XML
    $sql_1 = "SELECT * FROM Country";
    $sql_2 = "SELECT * FROM City WHERE CountryCode = ? ORDER BY Population DESC LIMIT 5";
    $xml = $q2x->getXML($sql_1, array(
            'idColumn' => 'code',
            'rootTag' => 'countries',
            'rowTag' => 'country',
            'attributes' => array('code', 'name', 'continent'),
            'elements' => array('cities' => array(
                'sql' => array('data' => array('code'), 'query' => $sql_2),
                'idColumn' => 'id',
                'rootTag' => 'cities',
                'rowTag' => 'city',
                'attributes' => array('name','district','population'))
            )
        )    
    );
    
    // send output to browser
    header('Content-Type: text/xml');
    $xml->formatOutput = true;
    echo $xml->saveXML();
} catch (Exception $e) {
    echo $e->getMessage();
}
?>

上記で注目すべき重要な点は、elements 配列です。リスト 5 では、この配列には要素として表示する結果セット・フィールドのリストが含まれているだけでしたが、上記ではこの配列が遥かに複雑な機能を果たします。配列が最初に定義しているのは新規の <cities> 要素で、この要素はキーと値のペアが含まれるオプション配列に結び付けられます。このオプション配列のなかで新しいキーは唯一、sql キーだけです。このキーが、<cities> 要素を設定するために実行する内部 SQL クエリーを定義します。

この sql キーについては、少し時間をかけて理解しておくことが重要です。連想配列に結び付けられるこのキー自体には以下の 2 つのキーが含まれます。

  • data 。外部 SQL クエリーからインポートするフィールドを指定します。
  • query <cities> 要素を設定するときに実行する内部 SQL クエリーを指定します。

この 2 番目の SQL クエリーには、疑問符 (?) プレースホルダーが含まれていることに注意してください。このプレースホルダーは実行時に、data 配列に指定された現行の値に置き換えられます。具体的な例で説明すると、外部クエリーによって返されたレコードの code フィールドに 'IND' という値が含まれている場合、この値 'IND' が内部クエリーに挿入されて ? プレースホルダーに置き換わります。

これで、XML_Query2XML の特質が明らかになったはずです。あらゆる elements 配列に別個の SQL クエリーを設定できるため、SQL 結果セットを無制限の深さまでネストすることができます。その上すべての elements 配列は親クエリーからフィールドを参照できるため、特定の名前付きフィールドでリンクした一連のチェーン・クエリー (SQL 結合と同様) を作成することも可能です。

出力は以下のようになります。

リスト 9. リスト 8 によって生成された XML 出力 (要約)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
  <country code="AFG" name="Afghanistan" continent="Asia">
    <cities>
      <city name="Kabul" district="Kabol" population="1780000"/>
      <city name="Qandahar" district="Qandahar" population="237500"/>
      <city name="Herat" district="Herat" population="186800"/>
      <city name="Mazar-e-Sharif" district="Balkh" population="127800"/>
    </cities>
  </country>
  <country code="NLD" name="Netherlands" continent="Europe">
    <cities>
      <city name="Amsterdam" district="Noord-Holland" population="731200"/>
      <city name="Rotterdam" district="Zuid-Holland" population="593321"/>
      <city name="Haag" district="Zuid-Holland" population="440900"/>
      <city name="Utrecht" district="Utrecht" population="234323"/>
      <city name="Eindhoven" district="Noord-Brabant" population="201843"/>
    </cities>
  </country>
  ...
</countries>

ここまで理解したら、新しい XML 構造を説明する新規 XML スタイルシートを生成するのは簡単です。

リスト 10. リスト 9 を変換するための XSL スタイルシート
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:template match="/countries">
        <html>
            <head>
                <style type="text/css">
                td { text-align: center; padding: 3px; }
                .head { font-style: italic; }
                </style>
            </head>
            <body>
                <xsl:for-each select="country">
                    <h2><xsl:value-of select="@name"/> - <xsl:value-of 
                    select="@continent"/></h2>
                    <table border="1">
                        <thead>
                            <tr>
                                <xsl:for-each select="cities/city[1]/@*">
                                <td class="head">
                                    <xsl:value-of select="name(.)"/>
                                </td>
                                </xsl:for-each>
                            </tr>
                        </thead>
                        <tbody>
                            <xsl:apply-templates/>
                        </tbody>
                    </table>
                </xsl:for-each>
            </body>
        </html>
    </xsl:template>

    <xsl:template match="cities/city">
      <tr>
        <xsl:for-each select="@*">
        <td>
            <xsl:value-of select="."/>
        </td>
        </xsl:for-each>
    </tr>
    </xsl:template>
</xsl:stylesheet>

当然、オリジナルの PHP スクリプトも修正して、生成された XML をこのスタイルシートを使って変換するようにしなければなりませんが、この変更は取るに足らないものです。

リスト 11. リスト 8 で生成された XML 出力の変換
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

try {
    // initalize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
    
    // generate SQL query
    // get results as XML
    $sql_1 = "SELECT * FROM Country";
    $sql_2 = "SELECT * FROM City WHERE CountryCode = ? ORDER BY Population DESC LIMIT 5";
    $xml = $q2x->getXML($sql_1, array(
            'idColumn' => 'code',
            'rootTag' => 'countries',
            'rowTag' => 'country',
            'attributes' => array('code', 'name', 'continent'),
            'elements' => array('cities' => array(
                'sql' => array('data' => array('code'), 'query' => $sql_2),
                'idColumn' => 'id',
                'rootTag' => 'cities',
                'rowTag' => 'city',
                'attributes' => array('name','district','population'))
            )
        )    
    );
    
    // read XSL stylesheet data
    $xsl = new DOMDocument;
    $xsl->load('countries.xsl');
    
    // initialize XSLT engine
    $xslp = new XSLTProcessor;
    
    // attach XSL stylesheet object
    $xslp->importStyleSheet($xsl); 
    
    // perform transformation
    header('Content-Type: text/html');
    echo $xslp->transformToXML($xml);
} catch (Exception $e) {
    echo $e->getMessage();
}
?>

変換後の XML は図 2 にようになります。

図 2. リスト 11 で生成された HTML 文書
リスト 11 で生成された HTML 文書

このネスト機能はさまざまな方法で利用できる他、XML_Query2XML には XML 出力をさらに調整するための各種オプションが用意されています。詳しい説明は、XML_Query2XML マニュアルに記載されているので、そちらを参照してください (「参考文献」を参照)。


XPath で SQL レコードをフィルタリングする

ご想像のとおり、getXML() メソッドの出力を特定の制約に一致するレコードだけに絞り込むのは至って簡単です。該当する WHERE 節を単に SQL クエリーに追加するだけでいいからです。あるいは XPath 構文を使って、フィルタリングした XML ノード・ツリーのサブセットを作成し、呼び出し側に返すという方法もあります。

リスト 12 はそれを実現する方法を示す単純な例です。これはリスト 11 を修正したもので、XPath 条件を使用して、出力する XML にはヨーロッパに位置する国と都市だけをリストするように制限しています。

リスト 12. XPath を使用した SQL から XML への出力の制約
<?php
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

try {
    // initialize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
    
    // generate SQL query
    // get results as XML
    $sql_1 = "SELECT * FROM Country";
    $sql_2 = "SELECT * FROM City WHERE CountryCode = ? ORDER BY Population DESC LIMIT 5";
    $xml = $q2x->getXML($sql_1, array(
            'idColumn' => 'code',
            'rootTag' => 'countries',
            'rowTag' => 'country',
            'attributes' => array('code', 'name', 'continent'),
            'elements' => array('cities' => array(
                'sql' => array('data' => array('code'), 'query' => $sql_2),
                'idColumn' => 'id',
                'rootTag' => 'cities',
                'rowTag' => 'city',
                'attributes' => array('name','district','population'))
            )
        )    
    );

    // now, further filter the XML using XPath
    // return only those <country> nodes which have the attribute 'continent=Europe'    
    // as a DOMNodeList
    $xpath = new DOMXPath($xml);
    $nodelist = $xpath->query("/countries/country[@continent='Europe']");
    
    // generate a new DOM tree using the XPath result set
    // create the root element
    // import each node from the node list and append to the new DOM tree
    $dom = new DOMDocument;
    $root = $dom->createElement('countries');
    $dom->appendChild($root);
    $x = 0;
    while ($node = $nodelist->item($x)) {
        $node = $dom->importNode($node, true);
        $root->appendChild($node);
        $x++;
    }
    
    // print XML
    header('Content-Type: text/xml');
    $dom->formatOutput = true;
    echo $dom->saveXML();
} catch (Exception $e) {
    echo $e->getMessage();
}
?>

上記のスクリプトの最初のセグメントは元のままです。つまり、2 つの SQL クエリーがネストされていて、内部クエリーが外部クエリーによるデータを使って国と都市データのリストを生成しています。ただし今回は、XML をすぐに出力したり XSLT プロセッサーに渡す代わりに、DOMXPath オブジェクトを初期化し、オリジナルの XML ツリーから新規 DOMNodeList を作成しています。この DOMNodeList は XPath クエリーを使用して、continent 属性の値が Europe となっている <country> 要素だけが含まれるようにしています。DOMNodeList が作成されると新規 DOMDocument が初期化され、そこに作成された DOMNodeList がノードごとにインポートされて新しい XML 文書が生成されます。

リスト 13 は、この出力の一部です。

リスト 13. リスト 12 によって生成された XML 出力 (要約)
<?xml version="1.0"?>
<countries>
  <country code="NLD" name="Netherlands" continent="Europe">
    <cities>
      <city name="Amsterdam" district="Noord-Holland" population="731200"/>
      <city name="Rotterdam" district="Zuid-Holland" population="593321"/>
      <city name="Haag" district="Zuid-Holland" population="440900"/>
      <city name="Utrecht" district="Utrecht" population="234323"/>
      <city name="Eindhoven" district="Noord-Brabant" population="201843"/>
    </cities>
  </country>
  <country code="ALB" name="Albania" continent="Europe">
    <cities>
      <city name="Tirana" district="Tirana" population="270000"/>
    </cities>
  </country>
  <country code="AND" name="Andorra" continent="Europe">
    <cities>
      <city name="Andorra la Vella" district="Andorra la Vella" population="21189"/>
    </cities>
  </country>
  <country code="BEL" name="Belgium" continent="Europe">
    <cities>
      <city name="Antwerpen" district="Antwerpen" population="446525"/>
      <city name="Gent" district="East Flanderi" population="224180"/>
      <city name="Charleroi" district="Hainaut" population="200827"/>
      <city name="Liege" district="Liege" population="185639"/>
      <city name="Bruxelles [Brussel]" district="Bryssel" population="133859"/>
    </cities>
  </country>
  ...
<countries>

複数ソースからのデータをマージする

実際の XML ベースのアプリケーション開発では、XML 文書に 1 つのソースからの情報だけが保持されることはまずありません。1 つ以上の SQL 結果セットに加え、ディスク・ファイルからのデータ、外部 Web サービスからのデータ、それにシステムのプロセス・テーブルからのデータも含まれる場合があります。このような状況を考慮して、XML_Query2XML には SQL 以外のソースからのデータを getXML() メソッドによって返される XML に組み込む方法が用意されています。

XML_Query2XML では、開発者が出力される XML 内の特定要素が呼び出すカスタム・コールバック関数を定義できるようになっています。これらのコールバック関数に期待される役目は、内部で必要なデータを取得してそれを XML に変換し、この XML を XML 文書ツリー内の適切な場所に挿入しやすいように (DOMNode インスタンスとして) 呼び出し側に返すことです。getXML() の呼び出し内でカスタム・コールバック関数の前にハッシュ (# ) 記号を付けると、関数が自動的に現行の SQL レコードを入力として受け取ることになります。

これが実際に役に立つかどうかという疑問に答えるには、実例で説明するのが一番の近道です。まず、国とその国のもっとも人口の多い都市をリストする XML 文書を生成することにします。これだけのことなら前にもたくさんの例を見てきたはずですが、今回は少々趣向を凝らして GeoNames Web サービスからのデータを使い、この XML を指定の都市ごとの緯度と経度を加えて拡張します。

リスト 14 にこのコードを示します。

リスト 14. SQL から XML への出力と Web サービス・データとの統合
<?php
ini_set('max_execution_time', 120);
// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

try {
    // initalize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/world'));
    
    // generate SQL query
    // get results as XML
    $sql = "SELECT Country.Code2 AS code, Country.Name AS country, City.Name AS city, 
        City.Population AS population FROM Country, City 
        WHERE Country.Code = City.CountryCode GROUP BY City.CountryCode 
        HAVING City.Population = MAX(City.Population) ORDER BY City.Population 
        DESC LIMIT 15";
    $xml = $q2x->getXML($sql, array(
            'idColumn' => 'code',
            'rootTag' => 'countries',
            'rowTag' => 'country',
            'attributes' => array('code', 'name' => 'country'),
            'elements' => array('city' => array (
                'elements' => array(
                    'name' => 'city',
                    'population',
                    'location' => '#getLocation'),
                )
            ),
        )    
    );
    
    // print XML
    header('Content-Type: text/html');
    $xml->formatOutput = true;
    print $xml->saveXML();
} catch (Exception $e) {
    echo $e->getMessage();
}

// function to get data from GeoNames Web service
// call GeoNames with country code and city name
// create XML document fragment with returned values
function getLocation($record) {
    // get data and format into SimpleXML object
    $sxml = simplexml_load_string(file_get_contents(
        "http://ws.geonames.org/search?maxRows=1&name=" . 
            urlencode(utf8_encode($record['city'])) . "&country=" . 
            urlencode(utf8_encode($record['code']))));
    
    // extract data from SimpleXML object
    // convert into DOMNode fragments
    $dom = new DOMDocument();
    // generate <lat> node
    $lat = $dom->createElement('lat');
    $lat->appendChild($dom->createTextNode($sxml->geoname{0}->lat));
    // generate <long> node
    $long = $dom->createElement('long');
    $long->appendChild($dom->createTextNode($sxml->geoname{0}->lng));
    return array($lat, $long);
}
?>

リスト 14 では、getXML() の呼び出しが SELECT クエリーを実行し、さまざまな都市を国別コードによってグループ化してから、人口が最大の都市を選択しています。このデータが、以下の XML 文書に変換されます (リスト 15)。

リスト 15. リスト 14 によって生成された初期段階の XML 出力 (要約)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
  <country code="AW" name="Aruba">
    <city>
      <name>Oranjestad</name>
      <population>29034</population>
    </city>
  </country>
  ...
</countries>

次のタスクは、各都市の緯度と経度を取得して上記の文書ツリーに挿入することです (リスト 14 を参照)。この情報元となる GeoNames Web サービスには REST でアクセスし、このサービスが公開する search() メソッドを使って、指定された地名に関する地理情報を返します。GeoNames Web サービスの詳しい説明はこの記事の範囲外ですが、「参考文献」にこのサービスについての詳細が記載された資料を紹介しています。

リスト 16 は、'Berlin, Germany' のクエリーに対する GeoNames 応答パケットの例です。

リスト 16. GeoNames 応答パケットの例
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<geonames>
 <totalResultsCount>807</totalResultsCount>
 <geoname>
  <name>Berlin</name>
  <lat>52.5166667</lat>
  <lng>13.4</lng>
  <geonameId>2950159</geonameId>
  <countryCode>DE</countryCode>
  <countryName>Germany</countryName>
  <fcl>P</fcl>
  <fcode>PPLC</fcode>
 </geoname>
</geonames>

ご覧のように、この応答パケットには指定の場所に関するさまざまな情報が含まれています。この情報のなかに、ここで対象としている緯度と経度もあります。

リスト 13 getXML() 呼び出しをよく見てみると、オプション配列の location キーはクエリー結果セットのフィールドではなく、getLocation() という名前のコールバック関数にリンクされていることがわかります。これはつまり、getXML() は SQL 結果セットのレコードを処理するたびに、それと同じレコードを getLocation() コールバックにフィールドと値のペアの連想配列として渡すということです。すると getLocation() メソッドは REST を使用して GeoNames Web サービスの search() メソッドを呼び出し、このメソッドにパラメーターとして SQL レコードの都市と国名を渡して応答を SimpleXML オブジェクトとして取得します。その上で、SimpleXML 表記を使って応答パケットの <lat> 要素と <lng> 要素までドリルダウンし、これらの要素を 2 つの別個の DOMNode インスタンスに変換してから、ツリーに挿入する配列として getXML() に渡します。

このプロセスの最後に出力される XML はリスト 17 のようになります。

リスト 17. リスト 14 によって生成された最終 XML 出力 (要約)
<?xml version="1.0" encoding="UTF-8"?>
<countries>
  <country code="IN" name="India">
    <city>
      <name>Mumbai (Bombay)</name>
      <population>10500000</population>
      <location>
        <lat>18.975</lat>
        <long>72.8258333</long>
      </location>
    </city>
  </country>
  <country code="KR" name="South Korea">
    <city>
      <name>Seoul</name>
      <population>9981619</population>
      <location>
        <lat>37.5663889</lat>
        <long>126.9997222</long>
      </location>
    </city>
  </country>
  ...
</countries>

このサンプルからわかるように、カスタム・コールバック関数を使用すれば、getXML() で生成される XML 出力に他のソースからのデータを簡単に組み込むことができます。リスト 14 では外部 Web サービスに接続しましたが、外部ファイルや XML-RPC 呼び出しの出力も同じく簡単に最終的な XML ツリーにインポートできます。


データベース・バックアップを作成する

XML_Query2XML のもう 1 つの便利な使い方としては、データベース・テーブルのコンテンツを保存やバックアップのために XML ベースの形式にダンプするというアプリケーションが挙げられます。このようなバックアップ・スクリプトの背後にあるロジックは極めて単純で、まずデータベースからテーブルのリストを取得し、このリストを繰り返し処理して DESC ? SELECT * FROM ? SQL などのコマンドでそれぞれ各テーブルのスキーマ、あるいはレコードを抽出するというものです。この記事のここまでの説明を理解していれば、この作業を行うのに必要なのは getXML() メソッド呼び出しだということは、すでにおわかりでしょう。

このタスクは簡単そうに思えますが、実行するとなると多少厄介です。それは主に、MDB2 抽象化レイヤーでは準備済みクエリーで列名またはテーブル名のプレースホルダーを扱えないという特定の制限があるためです。つまり MDB2 レイヤーは前述の DESC ? というクエリーやSELECT * FROM ? といったクエリーを検出するとエラーを生成するのがおちなので、これらのクエリーを使うのが難しくなります。

そこでどうすればいいかと言えば、少々の想像力を発揮してみることです (リスト 18 を参照)。

リスト 18. データベース構造およびコンテンツの XML リストの作成
<?php
ini_set('max_execution_time', 120);

// include required files
include 'XML/Query2XML.php';
include 'MDB2.php';

// set database name
$db = 'world';

try {
    // initialize Query2XML object
    $q2x = XML_Query2XML::factory(MDB2::factory('mysql://root:pass@localhost/' . $db));

    // SQL query to get table list
    // note: this SQL query varies from database to database
    $sql = "SHOW TABLES";
    $xml = $q2x->getXML($sql, array(
                'idColumn' => false,
                'rootTag' => 'database',
                'rowTag' => 'table',
                'attributes' => array('name' => 'tables_in_' . $db))    
    );

    // get a list of all the <table> nodes
    $nodelist = $xml->getElementsByTagName("table");    
    
    // iterate over the nodes
    $x = 0;
    while ($node = $nodelist->item($x)) {
        // extract the table name
        $table = $node->attributes->getNamedItem('name')->nodeValue;
        
        // get table description
        // as DOM document
        // note: this SQL query varies from database to database
        $sql_1 = 'DESC ' . $table;
        $schema = $q2x->getXML($sql_1, array (
            'idColumn' => 'field',
            'rowTag' => 'define',
            'rootTag' => 'schema',
            'elements' => array('*'))
        );
        
        // get table contents
        // as another DOM document
        $sql_2 = 'SELECT * FROM ' . $table;
        $data = $q2x->getXML($sql_2, array (
            'idColumn' => false,
            'rowTag' => 'record',
            'rootTag' => 'data',
            'elements' => array('*'))
        );
        
        // iterate over the $schema DOM document
        // use XPath to get the <schema> node and all its children
        // import it into the main XML tree, under the corresponding <table> element
        // credit: Igor Kraus, http://www.php.net/simplexml for this suggestion
        $xpath = new DOMXPath($schema);
        $query = $xpath->query('//schema');
        for ($i = 0; $i < $query->length; $i++) {
            $xml->documentElement->childNodes->item($x)->appendChild(
                $xml->importNode($query->item($i), true));
        }
        
        // do the same for the $data DOM document
        $xpath = new DOMXPath($data);
        $query = $xpath->query('//data');
        for ($i = 0; $i < $query->length; $i++) {
            $xml->documentElement->childNodes->item($x)->appendChild(
                $xml->importNode($query->item($i), true));
        }
        
        // increment counter for the next run
        $x++;
    }
    
    // write output to disk
    // print success/error message
    $xml->formatOutput = true;
    if ($xml->save('/tmp/dump.xml')) {
        echo 'Data successfully saved!';
    } else {    
        echo 'Data could not be saved!';
    }
} catch (Exception $e) {
        echo $e->getMessage();    
}
?>

かなり複雑そうに見えますが、実は非常に単純です。

  1. まず初めに、現行データベースに含まれるすべてのテーブルの名前を取得します。このリストを取得する SQL コマンドはデータベースによって異なります。リスト 18 のスクリプトでは MySQL の SHOW TABLES コマンドを使っていますが、これは RDBMS 間で移植できないので、別のデータベース・システムを使用する場合は変更してください。このコマンドが出力するのはリスト 19 のような XML 文書で、この文書は $xml として保存されます。

    リスト 19. リスト 18 によって生成された初期段階の XML 出力
    <?xml version="1.0" encoding="UTF-8"?>
    <database>
      <table name="City"/>
      <table name="Country"/>
      <table name="CountryLanguage"/>
    </database>
  2. 次に、getElementsByTagName() メソッドを使用して、前のステップで生成されたすべての <table> 要素の集合を取得します。この集合が、ループで処理されることになります。ループの繰り返し処理ごとに作成される新規 XML 文書は 2 つあり、1 つはテーブルのフィールド構造に関する情報が含まれる $schema (リスト 20 を参照)、そしてもう 1 つはテーブルからの実際のレコードを保持する $data (リスト 21 を参照) です。

    リスト 20. テーブルの schema が含まれる XML 文書
    <?xml version="1.0" encoding="UTF-8"?>
    <schema>
      <define>
        <field>ID</field>
        <type>int(11)</type>
        <null>NO</null>
        <key>PRI</key>
        <default/>
        <extra>auto_increment</extra>
      </define>
      <define>
        <field>Name</field>
        <type>char(35)</type>
        <null>NO</null>
        <key/>
        <default/>
        <extra/>
      </define>
      <define>
      ...
      </define>
    </schema>
    リスト 21. テーブルのレコードが含まれる XML 文書
    <?xml version="1.0" encoding="UTF-8"?>
    <data>
      <record>
        <id>1</id>
        <name>Kabul</name>
        <countrycode>AFG</countrycode>
        <district>Kabol</district>
        <population>1780000</population>
      </record>
      <record>
        <id>2</id>
        <name>Qandahar</name>
        <countrycode>AFG</countrycode>
        <district>Qandahar</district>
        <population>237500</population>
      </record>
      <record>
      ...
      </record>
    </data>
  3. 同じループの繰り返し処理内で引き続き、2 つの独立した XML 文書、$schema $data を親 XML 文書 $xml にインポートします。$schema および $xml から XML ノード・フラグメントを簡単に抽出する方法となるのは、以前の例でも説明した XPath です。あとは DOM 拡張の importNode() メソッドがこれらのフラグメントを XML ツリーの主幹部の適切な場所に挿入して残りの処理を行います。

    リスト 22 に最終出力の一部を示します。

    リスト 22. リスト 18 によって生成された最終 XML 出力
    <?xml version="1.0" encoding="UTF-8"?>
    <database>
      <table name="City">
        <schema>
          <define>
            <field>ID</field>
            <type>int(11)</type>
            <null>NO</null>
            <key>PRI</key>
            <default/>
            <extra>auto_increment</extra>
          </define>
          <define>
            <field>Name</field>
            <type>char(35)</type>
            <null>NO</null>
            <key/>
            <default/>
            <extra/>
          </define>
          ...
        </schema>
        <data>
          <record>
            <id>1</id>
            <name>Kabul</name>
            <countrycode>AFG</countrycode>
            <district>Kabol</district>
            <population>1780000</population>
          </record>
          <record>
            <id>2</id>
            <name>Qandahar</name>
            <countrycode>AFG</countrycode>
            <district>Qandahar</district>
            <population>237500</population>
          </record>
          ...
        </data>
      </table>
      <table>
      ...
      </table>
    </database>

リスト 23 に記載するのは、XML_Query2XML クラスの開発者 Lukas Feiler による、これよりも洗練された別のソリューションです。

リスト 23. データベース構造およびコンテンツの XML リストを生成する別の方法
<?php
ini_set('max_execution_time', 120);

// credit: Lukas Feiler, http://www.lukasfeiler.com
// include files
require_once 'XML/Query2XML.php';
require_once 'MDB2.php';

// initialize MDB abstraction layer
// load MDB manager
$mdb2 = MDB2::factory('mysql://root:pass@localhost/world');
$mdb2->loadModule('Manager');

// initialize Query2XML object
$q2x = XML_Query2XML::factory($mdb2);

// get table list
$tables = $mdb2->listTables();

// dynamically generate $options array
// once for each table
$elements = array();
for ($i=0; $i<count($tables); $i++) {
    $elements['table' . $i] = array(
        'rowTag' => 'table',
        'attributes' => array(
            'name' => ':' . $tables[$i]
        ),
        'elements' => array(
            'record' => array(
                'idColumn' => false,
                'sql' => 'SELECT * FROM ' . $tables[$i],
                'elements' => array(
                    '*'
                )
            )
        )
    );
}

// get data from tables as XML
$xml = $q2x->getXML(
    false,
    array(
        'idColumn' => false,
        'rowTag' => '__tables',
        'rootTag' => 'database',
        'elements' => $elements 
    )
);

// write output to disk
// print success/error message
$xml->formatOutput = true;
if ($xml->save('/tmp/dump.xml')) {
    echo 'Data successfully saved!';
} else {    
    echo 'Data could not be saved!';
}
?>

このソリューションでは最初に MDB2 Manager モジュールをロードして、このモジュールの listTables() メソッドによってデータベースに依存しない方法でデータベースに含まれるすべてのテーブルのリストを取得しています。次にこのテーブル・リストを繰り返し処理し、そのたびに動的に新しい elements 配列を生成しています。すべてのテーブルの処理が完了した時点で、動的に生成した elements 配列を使って getXML() を呼び出し、データベース全体の XML ダンプを作成してディスクに書き込むというわけです。リスト 24 に、この出力ファイルの一部を記載します。

リスト 24. リスト 23 によって生成された XML 出力
<?xml version="1.0" encoding="UTF-8"?>
<database>
  <table name="city">
    <record>
      <id>1</id>
      <name>Kabul</name>
      <countrycode>AFG</countrycode>
      <district>Kabol</district>
      <population>1780000</population>
    </record>
    <record>
      <id>2</id>
      <name>Qandahar</name>
      <countrycode>AFG</countrycode>
      <district>Qandahar</district>
      <population>237500</population>
    </record>
    ...
  </table>
  <table name="country">
    <record>
      <code>AFG</code>
      <name>Afghanistan</name>
      <continent>Asia</continent>
      <region>Southern and Central Asia</region>
      <surfacearea>652090.00</surfacearea>
      <indepyear>1919</indepyear>
      <population>22720000</population>
      <lifeexpectancy>45.9</lifeexpectancy>
      <gnp>5976.00</gnp>
      <gnpold/>
      <localname>Afganistan/Afqanestan</localname>
      <governmentform>Islamic Emirate</governmentform>
      <headofstate>Mohammad Omar</headofstate>
      <capital>1</capital>
      <code2>AF</code2>
    </record>
    ...
  </table>
  <table>
  ...
  </table>
</database>

まとめ

これまで記載したリストを見るとわかるように、XML_Query2XML パッケージでは単に SQL 結果セットを XML としてフォーマット設定できるだけではありません。このパッケージは、SQL から HTML への変換という単純なアプリケーションから、Web サービス、ディスク・ファイル、そして複数のデータベース・システムなどの多彩な入力ソースから複合 XML 文書を作成するツールというアプリケーションに至るまで、広範なアプリケーションを実現可能にする役割も果たします。このようなあらゆる理由から、PHP 開発者のツールキットとして追加する価値は大いにあります。今度、PHP/XML アプリケーションと SQL データベースとのインターフェースが必要になったら、是非このパッケージを使って自分で確かめてみてください。

参考文献

学ぶために

製品や技術を入手するために

  • IBM トライアル・ソフトウェア: トライアル・ソフトウェアで、次の開発プロジェクトを構築してください。
  • PEAR XML_Query2XML パッケージ: このパッケージをダウンロードして、早速、SQL SELECT クエリーで取得した情報を XML データに変換してください。
  • PEAR MDB2 パッケージ: サポートされるすべての RDBMS に共通の API として、移植可能な PEAR DB と Metabase php データベース抽象化レイヤーのマージをダウンロードしてください。
  • MySQL ドライバー: PEAR MDB2 パッケージのドライバーをダウンロードしてください。
  • MySQL のサンプル world データベース: サンプルの国、都市、そして言語データをダウンロードしてインストールするためのマニュアルを入手してください。
  • IBM®DB2® Enterprise 9: DB2 Express 9 データ・サーバーの無料バージョン、DB2 9 (DB2 Express-C 9) の試用版をダウンロードしてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source
ArticleID=253220
ArticleTitle=PHP による SQL から XML への変換
publish-date=07242007