株式会社エス・スリー・フォー

DBTools.h++/iPEXによるスキーマの読み出しとXML化

はじめに

様々なRDBに対する接続と操作を単一のインタフェースで表現してくれるDBTools.h++はデータベースアプリケーションの開発を強力にサポートしてくれます。

このアーティクルでは、指定したデータベースに格納された全テーブルのスキーマをDBTools.h++で読出し、XMLドキュメントとして出力するアプリケーション’read_schema’を実装します。

XMLドキュメントの解析/操作および出力にはインフォテリアのiPEX v2.0 を使用します。iPEXはDOM/SAX/XSLTをサポートしており、XMLの解析だけでなく出力を伴うアプリケーションの実装がとても簡単です。

ふるまい

特定のデータベースからスキーマを読み出すのに必要なパラメータは:

  • データソース名(DSN)
  • ユーザ名(USR)
  • パスワード(PWD)
  • データベース名(DB)

の4つです。これらを属性値にもつエレメント<schema>を記述したXMLドキュメント、たとえば:

<?xml version=’1.0’ encoding=’shift_jis’?>
<schema dsn=’FOO’ usr=’scott’ pwd=’tiger’ db=’TRIAL’ />

を用意しておきます。

’read_schema’はこのXMLドキュメントのURIをコマンドラインから受け取ってXML-parserで解析し、エレメントの属性値を基にデータベースに接続します。

そして接続したデータベースに格納された全テーブルのスキーマを読み出して、入力となったXMLのルートエレメント<schema>に子ノード<table>、さらにその子ノード<column>を追加し、XMLドキュメントを生成します。

たとえば上記の入力ファイル’schema.xml’がディレクトリ’A:\’にあるとき、コマンドラインから:

read_schema file:///a:/schema.xml

と入力すると、以下のような出力が得られます:

<?xml version="1.0" encoding="shift_jis"?>
<schema dsn="MSDE" usr="sa" pwd="" db="TRIAL">
<table name="PERSON">
<column name="NAME" type="MBString" size="100" precision="50"/>
<column name="PHONE" type="MBString" size="100" precision="50"/>
</table>
<table name="student">
<column name="id" type="Long" size="4" precision="10" scale="0"/>
<column name="l_name" type="String" size="30" precision="30"/>
<column name="f_name" type="String" size="30" precision="30"/>
<column name="classroom" type="String" size="4" precision="4"/>
</table>
<table name="teacher">
<column name="id" type="Long" size="4" precision="10" scale="0"/>
<column name="l_name" type="String" size="30" precision="30"/>
<column name="f_name" type="String" size="30" precision="30"/>
<column name="classroom" type="String" size="4" precision="4"/>
</table>
</schema>

また、コマンドラインの第2引数にXSLドキュメントのURLを与えると、上記のXMLをXSLで変換して出力します。HTMLへの変換スクリプト(XSL)とそのときの出力例を以下に示します。

schema.xsl

<?xml version=’1.0’ encoding=’shift_jis’ ?>
<xsl:stylesheet version=’1.0’ xmlns:xsl=’http://www.w3.org/1999/XSL/Transform’>

<xsl:template match=’/’>
<html>
<head><title>schema</title></head>
<body>
<xsl:apply-templates select=’schema’/>
</body>
</html>
</xsl:template>

<xsl:template match=’schema’>
<ul>
<li>dsn = <xsl:value-of select=’@dsn’/></li>
<li>usr = <xsl:value-of select=’@usr’/></li>
<li>pwd = <xsl:value-of select=’@pwd’/></li>
<li>db  = <xsl:value-of select=’@db’/></li>
</ul>
<xsl:apply-templates select=’table’/>
</xsl:template>

<xsl:template match=’table’>
<hr>
<table border=’1’>
<caption><b><xsl:value-of select=’@name’/></b></caption>
<tr><th>name</th><th>type</th><th>size</th><th>precision</th><th>scale</th></tr>
<xsl:apply-templates select=’column’/>
</table>
</hr>
</xsl:template>

<xsl:template match=’column’>
<tr>
<td><xsl:value-of select=’@name’/></td>
<td><xsl:value-of select=’@type’/></td>
<td><xsl:value-of select=’@size’/></td>
<td><xsl:value-of select=’@precision’/></td>
<td><xsl:value-of select=’@scale’/></td>
</tr>
</xsl:template>

</xsl:stylesheet>

html

<html>
<head><title>schema</title></head>
<body>
<ul>
<li>dsn = MSDE</li>
<li>usr = sa</li>
<li>pwd = </li>
<li>db  = TRIAL</li>
</ul>
<hr>
<table border="1">
<caption><b>PERSON</b></caption>
<tr><th>name</th><th>type</th><th>size</th><th>precision</th><th>scale</th></tr>
<tr><td>NAME</td><td>MBString</td><td>100</td><td>50</td><td/></tr>
<tr><td>PHONE</td><td>MBString</td><td>100</td><td>50</td><td/></tr>
</table>
</hr>
<hr>
<table border="1">
<caption><b>student</b></caption>
<tr><th>name</th><th>type</th><th>size</th><th>precision</th><th>scale</th></tr>
<tr><td>id</td><td>Long</td><td>4</td><td>10</td><td>0</td></tr>
<tr><td>l_name</td><td>String</td><td>30</td><td>30</td><td/></tr>
<tr><td>f_name</td><td>String</td><td>30</td><td>30</td><td/></tr>
<tr><td>classroom</td><td>String</td><td>4</td><td>4</td><td/></tr>
</table>
</hr>
<hr>
<table border="1">
<caption><b>teacher</b></caption>
<tr><th>name</th><th>type</th><th>size</th><th>precision</th><th>scale</th></tr>
<tr><td>id</td><td>Long</td><td>4</td><td>10</td><td>0</td></tr>
<tr><td>l_name</td><td>String</td><td>30</td><td>30</td><td/></tr>
<tr><td>f_name</td><td>String</td><td>30</td><td>30</td><td/></tr>
<tr><td>classroom</td><td>String</td><td>4</td><td>4</td><td/></tr>
</table>
</hr>
</body>
</html>

browser image

browser image

からくり

以上のような機能を持つ備えたアプリケーションをどのように実装するか、順を追って解説します。

その前に、データベースからのスキーマの読み出しに必要なDBTools.h++のクラスやメソッドについて簡単に説明しておきます。

データベースへの接続

RWDBManagerのstaticメソッド:

RWDBDatabase database(
const RWCString& accessLib,
const RWCString& serverName,
const RWCString& userName,
const RWCString& passWord,
const RWCString& databaseName);

によってデータベースに接続します。引数は順にアクセスライブラリ,サーバ名,ユーザ名,パスワード,データベース名です。

全テーブルの取得

RWDBDatabaseのメソッド:

RWDBTable dbTables(int type = rwdbAllTypes) const;

を呼び出すことで、データベースに格納されている全テーブルの情報が得られます。

引数’type’にはRWDBDatabaseに定義されたenum値:

enum tableType{ UserTable = 1, SystemTable = 2, View = 4, Synonym = 8 }

を’|’でつないで指定することで、それぞれユーザテーブル/システムテーブル/ビュー/シノニムの情報が得られます。

このメソッドから得られるテーブルは:

  • name (String)
  • owner (String)
  • type (Int)

の、3つのカラムで構成されています。したがってテーブル名が必要なら、得られたテーブルの最初のカラムに格納された値を取り出せばよいことになります。

スキーマの読み出し

各テーブルのスキーマを読み出すには、RWDBTableの2つのメソッド

RWBoolean fetchSchema(const RWDBConnection& connection);
RWDBSchema schema() const;

をこの順で呼び出します。

RWDBSchemaのメソッドentries()でカラム数(size_t)、column(n)で第nカラム(rwDBColumn)が得られます。

ここまでのまとめとして、特定のデータベースから全ユーザテーブルのスキーマ情報をプリントするコードを以下に示します。

// stdlib
#include <iostream>
#include <string>
#include <locale>

// Rogue Wave Tools.h++/DBTools.h++
#include <rw/cstring.h>
#include <rw/wstring.h>
#include <rw/db/db.h>

// MS SQLServer access lib.
#define SERVER "msq42d.dll"

int main(int argc, char* argv[]) {

std::locale::global(std::locale("japanese"));

RWDBDatabase database = RWDBManager::database(SERVER, "MSDE", "sa", "", "TRIAL");
if ( !database.isValid() ) {
std::cout << "can’t connect to a database...\n";
return 1;
}

const char* valueType [] = {
"NoType",       "Char",    "UnsignedChar", "Tiny",
"UnsignedTiny", "Short",   "UnsignedSort", "Int",
"UnsignedInt",  "Long",    "UnsignedLong", "Float",
"Double",       "Decimal", "Date",         "DateTime",
"Duration",     "String",  "Blob",         "MBString",
"WString"
};

RWDBConnection connection = database.connection();

RWDBTable  dbtables = database.dbTables(connection, RWDBDatabase::UserTable);
RWDBReader dbreader = dbtables.reader();

while ( dbreader() ) {
RWDBRow dbrow;
dbreader >> dbrow;

// テーブル名
RWCString tableName = dbrow[0].asString();
cout << "table: " << tableName << endl;

// スキーマ読み出し
RWDBTable dbtable = database.table(tableName);
dbtable.fetchSchema(connection);
RWDBSchema dbschema = dbtable.schema();

for ( size_t i = 0; i < dbschema.entries(); ++i ) {
RWDBColumn dbcolumn = dbschema.column(i);
// カラム名
cout << "column: " << dbcolumn.name() << endl;
// 型
if ( dbcolumn.type() != RWDBValue::NoType ) {
cout << "type: " << valueType[dbcolumn.type()] << endl;
}
// 長さ
if ( dbcolumn.storageLength() != RWDB_NO_TRAIT ) {
cout << "storage_length: " << dbcolumn.storageLength() << endl;
}
// 精度
if ( dbcolumn.precision() != RWDB_NO_TRAIT ) {
cout << "precision: " << dbcolumn.precision() << endl;
}
// スケール
if ( dbcolumn.scale() != RWDB_NO_TRAIT ) {
cout  << "scale: " << dbcolumn.scale() << endl;
}
}
cout << endl;
}

return 0;
}

このコードをベースに、XMLで出力するように拡張しましょう。

実装

文字コード変換

アプリケーション内では必要に応じてマルチバイト文字列(shift_jis/euc-jp)とダブルバイト文字列(unicode:UCS-4)相互の変換が必要となりますから、あらかじめ用意しておきましょう。iPEXはマルチバイト文字列としてstd::string、ダブルバイト文字列としてstd::wstringを使用しており、これら相互の変換にはiPEXのユーティリティクラスCharacterのstaticメソッドtoWCString/toMBStringを利用しました:

const std::wstring native = L"shift_jis"; // (注)

inline std::wstring wide(const std::string& str) {
return iPEX::Character::toWCString(str.c_str(), native);
}

inline std::string narrow(const std::wstring& str) {
return iPEX::Character::toMBString(str.c_str(), native);
}

(注)Windows環境下で実装したのでnative-encodingを"shift_jis"に設定しています。UNIXでは"euc-jp"としてください。

URLで指定されたXMLのパース

コマンドラインで与えられたURLに基づいてXMLをパースします。iPEXでのXMLのパースは、まず空のDocumentを用意し、XMLReaderを使ってその中にパース結果を流し込みます。

iPEX::InputStreamByUrl in_strm(wide(argv[1]);
iPEX::XMLReader reader(&in_strm);
iPEX::Document* document = iPEX::IPEXDocument::createDocumentObject();
reader.read(document);
iPEX::Element* schema = document-gt;getDocumentElement();

データベースに接続

パース結果のルートエレメント(schema)の属性値からデータソース名(dsn)/ユーザ名(usr)/パスワード(pwd)/データベース名(db)を取得し、対応するデータベースに接続します。

RWDBDatabase database = RWDBManager::database("msql12d.dll", // (注)
narrow(schema->getAttribute(L"dsn")).c_str(),
narrow(schema->getAttribute(L"usr")).c_str(),
narrow(schema->getAttribute(L"pwd")).c_str(),
narrow(schema->getAttribute(L"db")).c_str());

(注) MS SQLServerアクセスライブラリを利用しました。お使いのデータベースおよびbuild-typeに応じたserverTypeに設定してください。

テーブルの取得

つぎにデータベース内に格納されている全テーブルを取得し、各テーブルのスキーマを読み出します:

RWDBConnection connection = database.connection();
RWDBTable dbtables = database.dbTables(connection, RWDBDatabase::UserTable);
RWDBReader dbreader = btables.reader();
while ( dbreader() ) {
RWDBRow dbrow;
dbreader >> dbrow;
RWDBTable dbtable = database.table(dbrow[0].asString());
dbtable.fetchSchema(connection);
RWDBSchema dbschema = dbtable.schema();
...
}

XMLドキュメントへのノードの追加

スキーマが取得できたら、それから得られるカラム情報を基にカラム名、型、サイズなどをXMLエレメントの属性値に設定しながらツリーを構築します:

iPEX::Element* table = document->createElement(L"table");
table->setAttribute(L"name", dbrow[0].asString(),data());
schema-gt;appendChild(table);

for ( size_t i = 0; i < dbschema.entries(); ++i ) {
RWDBColumn dbcolumn = dbschema.column(i);
iPEX::Element* column = document->createElement(L"column&qot;);
column->setAttribute(L"name", wide(dbcolumn.name().data()));
...
}
table->appendChild(column);
}

XMLのプリント

これでスキーマ情報がXMLドキュメントに転写されました。あとはこれをテキストとして標準出力に書き出せば完了です

iPEX::OutputstreamByFile out_strm(iPEX::OutputstreamByFile::STDOUT, native);
iPEX::XMLWriter writer(&qmp;out_strm, L"" true);
writer.write(document);

XSLによる書式変換

コマンドラインに第2引数が与えられたとき、これをURLとするXSLを読み出し、スキーマ情報を表現したXMLを変換します:

iPEX::InputStreamByUrl in_strm(wide(argv[2]));
iPEX::XMLReader reader(&in_strm);
iPEX::Document* xsl_document = iPEX::IPEXDocument::createDocumentObject();
reader.read(xsl_document);
iPEX::Element* xsl_root = xsl_document->getDocumentElement();
iPEX::Node* xsl_result = document->evalXSLT(xsl_root, xsl_document); // (注)
writer.write(xsl_reslt);
delete xsl_result;
delete xsl_document;

(注) XSLT(XSL Transform)による変換にはNodeのメソッドevalXSLT()を使いました。第1引数にはパースされたXSLドキュメント、第2引数には変換結果を表現するツリーの各ノードの生成元となるDocumentを指定します。生成された変換結果のルートが戻り値となります。

ソース

/*
* read_schema
*
*  by the use of Rogue Wave DBTools.h++ & Infoteria iPEX
*/

// stdlib
#include <iostream>
#include <string>
#include <locale>

// Rogue Wave Tools.h++/DBTools.h++ 
#include <rw/cstring.h>
#include <rw/wstring.h>
#include <rw/db/db.h>

// Infoteria iPEX
#include <ipexdom.h>
#include <ipexslt.h>

/*
* access-lib の設定。(ここでは MS SQL Server)
*/
#ifdef RWDEBUG
#define SERVER "msq15d.dll"
#else
#define SERVER "msq12d.dll"
#endif

/*
* string conversions
*/
// UNIXの場合 L"euc-jp"
const std::wstring native = L"shift_jis";

// string(native) -> wstring(unicode)
inline std::wstring wide(const std::string& str) {
return iPEX::Character::toWCString(str.c_str(), native);
}

// wstring(unicode) -> string(native)
inline std::string narrow(const std::wstring& str) {
return iPEX::Character::toMBString(str.c_str(), native);
}

/*
* ErrorHandler : reports erross/warnings
*/
class ErrorHandler : public iPEX::DOMErrorHandler {
void report(Code code, const iPEX::Node* node, unsigned int line, const std::wstring& hint) {
std::wcerr << getURI() << L"(" << line << L"): Error code="
       << code << L" hint=\"" << hint << L"\"" << std::endl;
}
public:
void fatalError(Code code, const iPEX::Node* node, unsigned int line, const std::wstring& hint)
{ report(code, node, line, hint) ; }
void error(Code code, const iPEX::Node* node, unsigned int line, const std::wstring& hint)
{ report(code, node, line, hint) ; }
void warning(Code code, const iPEX::Node* node, unsigned int line, const std::wstring& hint)
{ report(code, node, line, hint) ; }
};

/*
* main routine
*/
int main(int argc, char* argv[]) {

// shift_jis/iso-2022-jp/euc-jp の各encodingをサポート
static iPEX::ShiftJISConverterFactory  f_SJIS;
static iPEX::ISO2022JPConverterFactory f_JIS;
static iPEX::EUCJPConverterFactory     f_EUC;

// localeを’日本語’に設定
std::locale::global(std::locale("japanese"));

// parse-error を出力するハンドラ
ErrorHandler error_handler;

// argv[1]で与えられたURLに基づいて読み込み
iPEX::InputStreamByUrl in_strm(wide(argv[1]));
iPEX::XMLReader reader(&in_strm, iPEX::Reader::NORMALIZE, &error_handler);

// XMLをパース
iPEX::Document* document = iPEX::IPEXDocument::createDocumentObject();
if ( !reader.read(document) ) {
return 1;
}

// ルート・エレメントを取得
iPEX::Element* schema = document->getDocumentElement();

// データベースに接続
RWDBDatabase database = RWDBManager::database(SERVER,
                    narrow(schema->getAttribute(L"dsn")).c_str(),
                    narrow(schema->getAttribute(L"usr")).c_str(),
                    narrow(schema->getAttribute(L"pwd")).c_str(),
                    narrow(schema->getAttribute(L"db")).c_str());
if ( !database.isValid() ) {
std::cout << "can’t connect to a database...\n";
return 1;
}

const wchar_t* valueType [] = {
L"NoType",       L"Char",    L"UnsignedChar", L"Tiny",
L"UnsignedTiny", L"Short",   L"UnsignedSort", L"Int",
L"UnsignedInt",  L"Long",    L"UnsignedLong", L"Float",
L"Double",       L"Decimal", L"Date",         L"DateTime",
L"Duration",     L"String",  L"Blob",         L"MBString",
L"WString"
};

RWDBConnection connection = database.connection();

// データベース内の全テーブルを取得
RWDBTable  dbtables = database.dbTables(connection, RWDBDatabase::UserTable);
RWDBReader dbreader = dbtables.reader();

// 各テーブルに対し...
while ( dbreader() ) {
RWDBRow dbrow;
dbreader >> dbrow;

// スキーマの読み出し
RWDBTable dbtable = database.table(dbrow[0].asString());
dbtable.fetchSchema(connection);
RWDBSchema dbschema = dbtable.schema();

// <table name="テーブル名">...</table>
iPEX::Element* table = document->createElement(L"table");
table->setAttribute(L"name", dbrow[0].asWString().data());

// <table> を <schema> に追加
schema->appendChild(table);

// スキーマの各カラムに対し...
for ( size_t i = 0; i < dbschema.entries(); ++i ) {
RWDBColumn dbcolumn = dbschema.column(i);
wchar_t number[16];

// <column name="カラム名" 属性名="属性値" ... />
iPEX::Element* column = document->createElement(L"column");
column->setAttribute(L"name", wide(dbcolumn.name().data()));

// type
if ( dbcolumn.type() != RWDBValue::NoType ) {
column->setAttribute(L"type", valueType[dbcolumn.type()]);
}

// size
if ( dbcolumn.storageLength() != RWDB_NO_TRAIT ) {
column->setAttribute(L"size", _itow(dbcolumn.storageLength(), number, 10));
}

// precision
if ( dbcolumn.precision() != RWDB_NO_TRAIT ) {
column->setAttribute(L"precision", _itow(dbcolumn.precision(), number, 10));
}

// scale
if ( dbcolumn.scale() != RWDB_NO_TRAIT ) {
column->setAttribute(L"scale", _itow(dbcolumn.scale(), number, 10));
}
// <column> を <table> に追加
table->appendChild(column);
}
}

// 標準出力を出力先に設定
iPEX::OutputStreamByFile out_strm(iPEX::OutputStreamByFile::STDOUT, native);
iPEX::XMLWriter writer(&out_strm,L"",true);
if ( argc < 3 ) {
// XMLをプリント
writer.write(document);
} else {
// XSLを読み出し
iPEX::InputStreamByUrl in_strm(wide(argv[2]));
iPEX::XMLReader reader(&in_strm, iPEX::Reader::NORMALIZE, &error_handler);
iPEX::Document* xsl_document = iPEX::IPEXDocument::createDocumentObject();
if ( !reader.read(xsl_document) ) {
return 1;
}
iPEX::Element* xsl_root   = xsl_document->getDocumentElement();
// schema(XML)をXSLで変換
iPEX::Node*    xsl_result = document->evalXSLT(xsl_root, xsl_document);
// 変換結果をプリント
writer.write(xsl_result);
delete xsl_result;
delete xsl_document;
}

delete document;

return 0;
}