HTTPによるリモートDBとの同期(その4「そう甘くはない」)

JSONの実装は気になるものの、後でXMLをJSONにしても大して手間は掛かるまいと思い、まずはWebRowSetの実装を試してみることにした。

試してみようと思ったのは、私のWebRowSetの使い方は少々特殊かもしれないからだ。

そもそもRowSetファミリは非接続且つシリアライズ可能なデータベース結果セットを表現するインタフェースだ。CachedRowSetインタフェースはその代表格であり、それ自体がJDBC接続をサポートしており自らがデータベースに接続してデータをロードした後に接続を一時的に切ることができる。(その間、データは保持していることができる)
今回対象としているWebRowSetはCachedRowSetを継承し、内部のメタデータとデータそのものをXMLシリアライズ、又はXMLから自身をデシリアライズする機能が追加されたクラスである。

私が対象としているアプリケーションはWebサーバに接続するHTTPクライアントであり、Swingを使った本格的なGUIアプリケーションではあるが、データベース接続層は持たない構成である。従ってRowSetを使うといっても必要最小限のパラメタしか扱うことができない。

  • クライアント側で扱えるパラメタ
データソース名(セキュリティ設定上、必要ならばユーザとパスワードの情報)
SQL文(DB上のデータ形式を推測できるメタデータとして扱える)

一般的にデータソースはサーバ側で全ての諸元を管理しているはずなので、クライアント側ではデータソースの名前しか知る必要は無いはずだ。あとはデータを引っ張るためのSQLが必要な位か。
この最小限のパラメタをセットしたWebRowSetをシリアライズしてサーバに送信し、サーバ側ではJDBCデータソースを取得、SQLを実行してその結果が格納されたWebRowSetをシリアライズして返して欲しいのである。

クライアント --> WebRowSet生成 --> データソース名 + SQL文 --> WebRowSetのXMLシリアライズ --> HTTP --> サーバ --> WebRowSetデシリアライズ --> JDBCデータソース取得 --> SQL実行 --> 結果格納 --> WebRowSetのXMLシリアライズ --> クライアント

このようなサイクルを期待する訳だ

ところが、以下のように

WebRowSet rowSet = new WebRowSetImpl();
rowSet.setDataSourceName("Hoge");
rowSet.setCommand("SELECT * FROM Hoge.FooTable");
rowset.writeXml(URLConnection等から取得した出力ストリーム);

必要最小限のパラメタをセットしたWebRowSetをストリームに対して出力しようとしたのだが、例外を吐いてしまう。

java.lang.NullPointerException
	at com.sun.rowset.internal.WebRowSetXmlWriter.writeMetaData(WebRowSetXmlWriter.java:260)
	at com.sun.rowset.internal.WebRowSetXmlWriter.writeRowSet(WebRowSetXmlWriter.java:129)
	at com.sun.rowset.internal.WebRowSetXmlWriter.writeXML(WebRowSetXmlWriter.java:81)
	at com.sun.rowset.WebRowSetImpl.writeXml(WebRowSetImpl.java:140)
         :

WebRowSetImplはsunのWebRowSetのRI(参照)実装でありソースコードが無いので推測となるが、WebRowSetImplは内部でXMLへのシリアライズにWebRowSetXmlWriterというクラスを使っているようだが、この実装はデータベース接続ができるレイヤでの使用を前提にしているらしく、メタデータが取得できなくともそのままXMLに出力しようとして例外になっているようだ。

リクエスト側はWebRowSetを使わずに必要なパラメタだけをHTTP POSTで送る方法もあるが、そうすると同一のオブジェクト(WebRowSet)をやり取りして差分の同期を行う用途には使えない。
やはり自分で用意しないと駄目みたいだ。

早速追記。
以下のように、空のメタデータを予めWebRowSetにセットしておくことで、シリアライズは可能となった。

WebRowSet rowSet = new WebRowSetImpl();
if (rowset.getMetaData() == null ) {
    RowSetMetaData md = new RowSetMetaDataImpl();
    rowset.setMetaData(md);
}
rowSet.setDataSourceName("Hoge");
rowSet.setCommand("SELECT * FROM Hoge.FooTable");
rowset.writeXml(URLConnection等から取得した出力ストリーム);

この内容でサーバに送られる、WebRowSetのシリアライズされたXMLは以下のようになる。(このメタデータ以外は何もセットしなくともシリアライズできる訳で、バグとも仕様ともとれる微妙な振る舞いだと思う)

<?xml version="1.0"?>
<webRowSet xmlns="http://java.sun.com/xml/ns/jdbc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/jdbc http://java.sun.com/xml/ns/jdbc/webrowset.xsd">
  <properties>
    <command>SELECT * FROM Hoge.FooTable</command>
    <concurrency>1008</concurrency>
    <datasource>Hoge</datasource>
    <escape-processing>true</escape-processing>
    <fetch-direction>1000</fetch-direction>
    <fetch-size>0</fetch-size>
    <isolation-level>2</isolation-level>
    <key-columns>
    </key-columns>
    <map>
    </map>
    <max-field-size>0</max-field-size>
    <max-rows>0</max-rows>
    <query-timeout>0</query-timeout>
    <read-only>true</read-only>
    <rowset-type>ResultSet.TYPE_SCROLL_INSENSITIVE</rowset-type>
    <show-deleted>false</show-deleted>
    <table-name>Hoge.FooTable</table-name>
    <url><null/></url>
    <sync-provider>
      <sync-provider-name>com.sun.rowset.providers.RIOptimisticProvider</sync-provider-name>
      <sync-provider-vendor>Sun Microsystems Inc.</sync-provider-vendor>
      <sync-provider-version>1.0</sync-provider-version>
      <sync-provider-grade>2</sync-provider-grade>
      <data-source-lock>1</data-source-lock>
    </sync-provider>
  </properties>
  <metadata>
    <column-count>0</column-count>
  </metadata>
  <data>
  </data>
</webRowSet>

見て解る通りだが、必要なプロパティは全てシリアライズされている。これならばいけそうだ。

更に追記
サーバに送ることは成功したが、サーバ側でWebRowSetを復元(デシリアライズ)しようとすると、やはりメタデータで問題がでる。
サーバサイドはサーブレットから呼ばれたアクションクラスで以下のようにリクエストのストリームからWebRowSetを復元しようとしているのだが、

//WebRowSetをデシリアライズ
WebRowSet rowSet = new WebRowSetImpl();
rowSet.readXml(new InputStreamReader(req.getInputStream(), encoding)); //reqはHttpServletRequest

残念ながら以下の例外が出る。

org.xml.sax.SAXException: メタデータ設定エラー : Invalid column count. Cannot be less or equal to zero
	at com.sun.rowset.internal.XmlReaderContentHandler.endElement(XmlReaderContentHandler.java:691)
	at org.apache.xerces.parsers.AbstractSAXParser.endElement(Unknown Source)
	at org.apache.xerces.impl.xs.XMLSchemaValidator.endElement(Unknown Source)
	at org.apache.xerces.impl.XMLNSDocumentScannerImpl.scanEndElement(Unknown Source)
	at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl$FragmentContentDispatcher.dispatch(Unknown Source)
	at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source)
	at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source)
	at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source)
	at org.apache.xerces.parsers.XMLParser.parse(Unknown Source)
	at org.apache.xerces.parsers.AbstractSAXParser.parse(Unknown Source)
	at org.apache.xerces.jaxp.SAXParserImpl$JAXPSAXParser.parse(Unknown Source)
	at com.sun.rowset.internal.WebRowSetXmlReader.readXML(WebRowSetXmlReader.java:91)
	at com.sun.rowset.WebRowSetImpl.readXml(WebRowSetImpl.java:158)
:
:

やはりメタデータの要素に空(と判断される)の値をセットしては駄目な仕様らしい。とほほだな。