谷本 心 in せろ部屋 このページをアンテナに追加 RSSフィード

2007-04-30

[]S2DaoDerby + XML (SELECT編)

Java6についてくるJavaDB(Derby)では、XMLがサポートされてるそうなので、

これをS2Daoから呼び出す事にしました。


この辺りを参考にしながら、DBのセットアップ&テーブル作成。

JDBC SQL/XMLの新機能を使ってXMLデータ処理を効率化する

紅孔雀の日記 - Apache Derby における XML データの扱い


derbyではij.batを叩いてSQLコンソールにアクセスできるんですが、

ここでxalan.jarにクラスパスを通しておかないと、

SELECTとかINSERTでエラーが発生します。


一通り、ij.batからSELECTとかINSERTが出来るようになったので、

次はS2Daoからアクセスしてみます。

普通にDaoとEntityを作って、SQLファイルを作ります。


残念ながら、いまのDerbyはSQLXMLに対応していないので、

読み書きはStringで行なっています。


Article.java

public class Article
{
	private Integer id;
	private String data;
	// setter/getterは省略
}

ArticleDao.java

import java.util.List;
import org.seasar.dao.annotation.tiger.S2Dao;

@S2Dao(bean = Article.class)
public interface ArticleDao
{
	public void insert(Article article);
	public List<Article> getAllArticleList();
}

ArticleDao_getAllArticleList.sql

SELECT
  ID, XMLSERIALIZE(DATA AS CLOB) AS DATA
FROM
  ARTICLE

まずはselectを実行してみたところで、エラーが発生、、、

Throwable:org.seasar.framework.exception.SQLRuntimeException: [ESSR0071]SQLで例外(ErrorCode=30000, SQLState=42Y07)が発生しました。理由はorg.seasar.framework.exception.SSQLException: [ESSR0072]SQLで例外(SQL=[SELECT
  ID, XMLSERIALIZE(DATA AS CLOB) AS DATA
FROM
  ARTICLE], ErrorCode={1}, SQLState={2})が発生しました

発生したエラーを見ても、よく分かりません (^^;

こういう場合は、S2Daoを経由せず、自前でクライアントを作るに限ります。

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;

public class ArticleDaoClient
{
	private static final String PATH = "app.dicon";

	private ArticleDao dao;

	public static void main(String[] args) throws Exception
	{
		S2Container container = S2ContainerFactory.create(PATH);
		DataSource source = (DataSource) container
				.getComponent(DataSource.class);
		Connection conn = source.getConnection();

		PreparedStatement stmt = conn
				.prepareStatement("SELECT ID, XMLSERIALIZE(DATA AS CLOB) FROM ARTICLE");
		stmt.execute();
	}
}

これで実行してみると、エラーが分かりました。

Caused by: java.sql.SQLException: Failed to locate 'Xalan' API or implementation classes.  XML operations are not permitted unless these classes are in your classpath.

xalan.jarにクラスパスを通していませんでした (^^;;


xalan.jarにクラスパスを通して、再挑戦。

エラーが変わりました。

Caused by: java.sql.SQLException: Schema 'SA' does not exist

ユーザ名をsaのままにしていました (^^;;;


今回はユーザ名を空っぽにしているので、

jdbc.diconのuserを"sa"から""に修正したら、

見事に成功しました。


[]S2DaoDerby + XML (INSERT編)

次は、insertを試してみましょう。


ArticleDao_insert.sql

INSERT INTO ARTICLE (data)
VALUES
(
	XMLPARSE (DOCUMENT CAST (/*dto.data*/'<TEST/>' AS CLOB) PRESERVE WHITESPACE)
)

S2Daoから呼び出すと、やはりエラーが発生しますが、よく原因が分かりません。

ここでも同じように、自前で作ったクライアントから呼び出してみます。

	private static void insert(Connection conn) throws SQLException
	{
		PreparedStatement stmt = conn
				.prepareStatement("INSERT INTO ARTICLE (data) VALUES ( XMLPARSE (DOCUMENT CAST ('<TEST/>' AS CLOB) PRESERVE WHITESPACE))");
		stmt.execute();
	}

これを動かしてみたら、エラーが分かりました。

Caused by: java.sql.SQLException: Encountered unexpected error while processing XML: org/apache/xml/serializer/OutputPropertiesFactory

serializer.jarにクラスパスが通っていないのが問題のように見えます。

改めて、serializer.jarにもクラスパスを通した所、正常に動きました。


ぷちハマりしましたが、

ようやく、S2DaoからDerbyXMLを読み書きできるようになりました。


それにしても、

せっかくのXMLを、Stringでしか読み書きできないのは残念ですねー。

DerbyのSQLXMLへの正式対応を期待しつつ、今はStringで我慢することにします。


# 暫定対応として、S2ContainerのValueType実装として

XMLシリアライズ/デシリアライズを行なうような

# ValueTypeを作っても良いかも知れませんね。

2007-04-02

[]なぜか、ProcedureHandlerImplをリファクタ中。

ストアドプロシージャの戻り値Dtoを使えるようにするために、

自前でProcedureHandlerImplを作成しているんですが、

既存のソースが、結構、なんていうか、ざっくり作ったというか・・・


S2Daoの、テーブルへの対応は丁寧に作りこんである感じがしますが、

ストアド対応は、

ぶっちゃけ、愛が足りないなと思いました。


なので、機能追加をする前に、ちょっとリファクタをしています。

  • DatabaseMetaData#getProcedureColumnsのResultSetから、columnType、columnName、dataType、typeNameを個別の変数にせず、1クラスにまとめる。
  • 戻り値のhandle用に、ProcedureResultHandlerを作成する。(ResultSetHandlerのストアド版です)

と言った感じ。


局所的な切り出しですが、こんな感じ。

while (rs.next()) {
	int columnType = rs.getInt(5);
	int dataType = rs.getInt(6);

	ProcedureColumnDescriptor pcd = new ProcedureColumnDescriptor();
	pcd.setColumnName(rs.getString(4));
	pcd.setColumnType(columnType);
	pcd.setDataType(dataType);
	pcd.setTypeName(rs.getString(7));
	columnDescriptorList.add(pcd);

小さな変更ですが、S2Dao本体にフィードバックしたいところ。


ちなみに、ProcedureResultHandlerは、既存ものを参考に作った

  • MapProcedureResultHandler
  • ObjectProcedureResultHandler

に加えて

  • BeanProcedureResultHandler

を追加する予定です。


そして、これらのHandlerの中で、STRUCTやARRAYに対応します。

2007-03-26

[][]ストアドプロシージャの戻り値DTOで取得するには?

ストアドプロシージャの戻り値Dtoで返すためには、

の2段階が必要。


似たような処理をしているのが

BeanMetaDataとか、AbstractBeanMetaDataResultSetHandlerとなので、

この辺りを参考にして作れそう。


ただ、通常のTableと比べて、

ストアドプロシージャの戻り値はシンプルなので、

実装としては、もうちょっと簡単になるかな。


作るべきクラスは、こんな感じ。

  • ProcedureResultMetaData
  • ProcedureResultHandler
  • STRUCTを扱うクラス
  • ARRAYを扱うクラス

STRUCTを扱うクラスは、MetaDataにするか、ValueTypeが良いか悩む所。

  • StructMetaData
  • StructType implements ValueType

両方作ることになるかも知れないけど、

今の所、あまりソースを読んでないので、決定できず。


とにかく、まずはProcedureResultMetaDataを作って

ストアドプロシージャの戻り値DTOで受けられるようにしてから、

STRUCTとDTOのマッピングを行なおう。

2007-03-25

[][]引数のSTRUCTやARRAYに対応する方法

S2Daoのストアド呼び出しの引数に、STRUCTやARRAYを使えるようにする改修。


  • ストアドプロシージャの引数がSTRUCT
    • Dao側のメソッドは、引数をDtoにする
  • ストアドプロシージャの引数がARRAY
    • Dao側のメソッドは、引数をListか配列にする

、、、を実現するための方法。


いまの所、S2DaoのProcedureHandlerは変更に対して開放されてないので

org.seasar.dao.handler.AbstractBasicProcedureHandlerを直接いじるか、

org.seasar.dao.handler.ProcedureHandlerImplでオーバーライドする必要がある。


いじる対象は、bindArgs。

現在 (s2-dao 1.0.40) の実装はこんな感じ。

protected void bindArgs(CallableStatement ps, Object[] args)
		throws SQLException {
	if (args == null) {
		return;
	}
	int argPos = 0;
	for (int i = 0; i < columnTypes.length; i++) {
		if (isOutputColum(columnInOutTypes[i].intValue())) {
			ps.registerOutParameter(i + 1, columnTypes[i].intValue());
		}
		if (isInputColum(columnInOutTypes[i].intValue())) {
			ps.setObject(i + 1, args[argPos++], columnTypes[i].intValue());
		}
	}
}

これを、こんな風に変更する。

protected void bindArgs(CallableStatement ps, Object[] args)
		throws SQLException {
	if (args == null) {
		return;
	}
	int argPos = 0;
	for (int i = 0; i < columnTypes.length; i++) {
		if (isOutputColum(columnInOutTypes[i].intValue())) {
			ps.registerOutParameter(i + 1, columnTypes[i].intValue(),
					typeNames[i]);
		}
		if (isInputColum(columnInOutTypes[i].intValue())) {
			Object arg = args[argPos++];

			// ここで条件判定。STRUCTかARRAYならラッピング。
			if (columnTypes[i].intValue() == Types.STRUCT) {
				arg = new StructWrapper(arg, typeNames[i]);
			} else if (columnTypes[i].intValue() == Types.ARRAY) {
				arg = new ArrayWrapper(arg, typeNames[i]);
			}

			ps.setObject(i + 1, arg, columnTypes[i].intValue());
		}
	}
}

で、ラッパーを2クラス追加する。

import java.sql.Connection;
import java.sql.SQLException;

import oracle.jdbc.oracore.OracleTypeADT;
import oracle.sql.Datum;
import oracle.sql.ORAData;
import oracle.sql.STRUCT;
import oracle.sql.StructDescriptor;

import org.seasar.framework.beans.BeanDesc;
import org.seasar.framework.beans.factory.BeanDescFactory;

public class StructWrapper implements ORAData {
	private Object bean;

	private String typeName;

	private BeanDesc desc;

	public StructWrapper(Object bean, String typeName) {
		this.bean = bean;
		this.typeName = typeName;
		this.desc = BeanDescFactory.getBeanDesc(bean.getClass());
	}

	public Datum toDatum(Connection conn) throws SQLException {
		StructDescriptor sd = new StructDescriptor(typeName, conn);
		String[] attrNames = getAttributeNames(sd);

		Object[] args = new Object[attrNames.length];
		for (int i = 0; i < attrNames.length; i++) {
			args[i] = desc.getFieldValue(attrNames[i], bean);
		}

		return new STRUCT(sd, conn, args);
	}

	private String[] getAttributeNames(StructDescriptor sd) throws SQLException {
		OracleTypeADT adt = sd.getOracleTypeADT();
		int attrNum = adt.getNumAttrs();

		String[] fieldNames = new String[attrNum];
		for (int i = 0; i < attrNum; i++) {
			fieldNames[i] = adt.getAttributeName(i + 1).replaceAll("_", "")
					.toLowerCase();
		}

		return fieldNames;
	}
}
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collection;

import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;
import oracle.sql.Datum;
import oracle.sql.ORAData;

public class ArrayWrapper implements ORAData {
	private Object[] array;

	private String typeName;

	public ArrayWrapper(Object collection, String typeName) {
		if (collection.getClass().isArray()) {
			array = (Object[]) collection;
		} else if (collection instanceof Collection) {
			array = ((Collection) collection).toArray();
		} else {
			throw new IllegalArgumentException(
					"Argument must be array or collection");
		}

		this.typeName = typeName;
	}

	public Datum toDatum(Connection conn) throws SQLException {
		ArrayDescriptor ad = new ArrayDescriptor(typeName, conn);

		Object[] arg = null;
		if (ad.getBaseType() == Types.STRUCT) {
			arg = new Object[array.length];

			for (int i = 0; i < array.length; i++) {
				arg[i] = new StructWrapper(array[i], ad.getBaseName());
			}
		} else {
			arg = array;
		}

		return new ARRAY(ad, conn, arg);
	}
}

ホントはAbstractBasicProcedureHandler#initTypesにも

ちょっと処理を追加してるんだけど、根本の部分はこんな感じ。


これで、Dtoや配列を引数にして、STRUCTやARRAYが入った

ストアドプロシージャ呼び出しができるようになりました。


[][]次は戻り値の扱い

ラッパーを作ることで、引数のDTO(STRUCT)、配列(ARRAY)には対応ができた。

次は、戻り値の変換をどうするか。


ストアドプロシージャのreturnやOUTパラメータにSTRUCTやARRAYが入っていると、

Daoの戻り値にも、oracle.sql.STRUCT型や、oracle.sql.ARRAY型のオブジェクトが入ってくる。

これを、そのままにして、Logic側でうまく処理しろというのは、ちょっと酷。


STRUCTだったらDTOに変換してあげたいんだけど、

問題は、どのDTOに変換すべきか、(S2Daoには)分からない、ということ。


今のS2Daoのストアド呼び出しは戻り値がMapになっているせいで、

何でも入るんだけど、逆に言うと、何を入れれば良いか分からない。


その対策方法の候補は下の二つ。


  • STRUCTの型名と、Dtoの型名の命名規則を決めておいて、規則通りのコンポーネント定義をS2コンテナから取得する。
  • Daoメソッドの戻り値をMapだけではなく、Dtoも許可する。Dtoだった場合、Dtoに定義された型に変換して返す。

前者は、まぁ必要に応じてやるとして、

自分としてやりたいのは後者。


こんなDTOを作って、、、

public class XxxDto {
	private String returnValue;

	private Xxx xx;

	private List xxxArray;
}

Dao側での呼び出しを、こんな風にする。

public String getXxx_PROCEDURE = "xxx";

public XxxDto getXxx(Integer id1, String name1, Integer id2, String name2);

これで、戻り値をXxxDtoにマッピングして返してやる。

いいじゃないですか、これで。

2007-03-24

[][]ここまでのまとめ

  • 引数のSTRUCT
    • ORADataのラッパーを使って、DTOをラッピングして渡す。
    • StructDescriptorを利用する。
  • 戻り値のSTRUCT
    • StructをDtoに変換して返す。
    • DTOのクラス名の取り方は?
  • 引数のARRAY
    • ORADataのラッパーを使って、ラッピングして渡す。
    • ArrayDescriptorを利用する。
    • ArrayDescriptorの作成に必要なデータ型名は、DBMSからSQLで取得する?
  • 戻り値のARRAY
    • ArrayをListに変換して返す。

赤文字が、まだ迷っている所。

実案件では、STRUCTとDTOの命名規則を決めて対応したんですが、

公開するためには、それでは厳しそうなので、ちょっと検討中。