Powered by SmartDoc

DbUtils活用術(EDbUtils, EDbHelper)

竹本 浩
http://www.pwv.co.jp
Jakarta CommonsのDbUtilsを使うためのテクニックをEDbUtils, EDbHelperを使って説明する

目次

Last modified: Mon Jun 13 12:12:15 JST 2005 since 2005/03/30

EDbUtilsは、EStoreのDB編

このレポートは、ショッピングカートの実装を通じてオープンソースツールをどのように活用するかを検証するレポートのDB編である。

ツール使いこなす

UNIX時代には「いかにしてツールを使うか」がプログラミングセンスの見せ所であった。言語もCからjavaに移って、開発環境もGUIペースのEclipseに取って替わられた感がある今日この頃だが、このような状況でどのようにしてソフトウェアを開発すればよいのか、オープンソースのツールを使いながら紹介する。

1 DbUtilsを使いこなす

JDBCプログラミングにすぐ効くDbUtils活用マニュアルに紹介されたDbUtilsは、検索後のデータマッピングに優れており、サイズも小さいので小さいシステムでのDB構築に向いていると思われる。しかしながらERマッピングツール(1)と比べるとデータの挿入や更新処理に対して個別にAPIを使ったコーディングが必要である。EDbHelperとEDBUtilsというユーティリティクラスを使って、DbUtilsをもう少し使いやすくしてみる。(2)

  1. 私は、OJBしか使ったことがない
  2. EDbhelperのjarファイルダウンロードAIPを参照されたい。もう少し使い込み必要な改良を加えてから、ソースも公開する予定である。

1.1 EDbUtilsを使ってみる

最初に単純な例でEDbUtilsがどれくらい簡単にDBにアクセスできるかを例を使って説明する。MemberというBeanクラスが以下のような属性を持っていた場合、

対応するテーブルは、

表 1.1.1 Memberに対応するテーブル
IDENTITY ID
VARCHAR ADDRESS
VARCHAR NAME

となる。Memberクラスのjavaソースは、

リスト 1.1.1 Member.java
package jp.co.pwv.estore.business;

/**
 * Member
 *
 * @author Hiroshi TAKEMOTO
 */
public class Member {
	private Integer		id;
	private String		name;
	private String		address;

	public Member() {
		this(null, null);
	}
	
	public Member(String name, String address) {
		this.name = name;
		this.address = address;	
	}
	
	public String getAddress() {
		return address;
	}
	public Integer getId() {
		return id;
	}
	public String getName() {
		return name;
	}
	public void setAddress(String string) {
		address = string;
	}
	public void setId(int i) {
		id = new Integer(i);
	}
	public void setName(String string) {
		name = string;
	}
	public void setId(Integer integer) {
		id = integer;
	}

}

のようになり、id, name, addressの属性を定義した後、Eclipseのgetter, setterの生成を行った。Sample1のインスタン生成を簡単にするために、Member(String name, String address)のコンストラクタも用意した。

サンプルプログラムSample1.javaのソースは、以下の通りである。

リスト 1.1.2 Sample1.java
package examples;

import java.util.List;

import javax.sql.DataSource;

import jp.co.pwv.estore.business.Member;
import jp.co.pwv.estore.util.EDbUtils;

/**
 * Sample1
 * 
 * @version 1.0 2004/09/03
 * @author Hiroshi TAKEMOTO
 */
public class Sample1 {
	public static void main(String[] args) {
		// EDbUtilsを使う前処理
		Object[]	objs = {new Member()};
		DataSource ds = EDbUtils.createDatasource(
						"org.hsqldb.jdbcDriver",
						"jdbc:hsqldb:hsql://localhost",
						"sa",
						"");		
		EDbUtils.createHelperMap(objs);
		// テーブルの作成
		EDbUtils.createTables();
		
		// Memberのインスタンをテーブルに挿入する例
		Member user1 = new Member("竹本", "東京都中野区");
		EDbUtils.insertObject(user1);

		// DBからIDを指定してオブジェクトをロードする例
		// user1と同じIDのオブジェクトをロードする
		Member user2 = (Member)EDbUtils.loadObject(Member.class, user1.getId());
		System.out.println(EDbUtils.toString(user2));
		
		// テーブルからすべてのオブジェクトを取りだす例
		List list = EDbUtils.selectObjects(Member.class, "");
		for (int i = 0; i < list.size(); i++) {
			Member member = (Member)list.get(i);
			System.out.println(EDbUtils.toString(member));			
		}	
			
		// 使用済みのテーブルを削除(通常は不要)
		// EDbUtils.dropTable(Member.class);
	}
}

動作試験の前に、使用するデータベースHSQLDBをサーバモードで起動する必要がある。(3)今回は、HSQLDB 1.7.1のdemoにあるrunServer.batを使ってDBサーバを起動する。

> runServer.bat

次にサンプルプログラムを実行するが、EDbUtilsは以下のライブラリを使用するため

最後のhsqldb.jarは、使用するデータベースによって変更する必要がある。Sample1を実行すると以下の結果がコンソールに出力される。

MEMBER: ADDRESS=東京都中野区 ID=0 NAME=竹本
MEMBER: ADDRESS=東京都中野区 ID=0 NAME=竹本			
		

DBの内容が正しく登録されているかは、HSQLDB 1.7.1のdemoにあるrunManager.batを使って調べることができる。

左のパネルにT_MEMBERが追加され、その内容がID, ADDRESS, NAMEとなっており、右のパネルでselect * from t_member;とSQL文を入力し、Executeボタンを押すと

ID ADDRESS NAME
0 東京都中野区 竹本

と表示される。では、Sample1.javaの処理をブロック毎に説明する。前処理のブロックでは、

  1. objsには、DBに登録するオブジェクトをセットする
  2. EDbUtils.createDatasource(ドライバ名,DBのURL,ユーザ名,パスワード)を指定してDataSourceを作成する
  3. EDbUtils.createHelperMap(テーブルに登録するオブジェクトの配列)を渡して、EDbHelperのマップを作成する
  4. EDbUtils.createTablesでDBにテーブルを作成する(4)

の処理を行う。

リスト 1.1.3 前処理
	// EDbUtilsを使う前処理
	Object[]	objs = {new Member()};
	DataSource ds = EDbUtils.createDatasource(
					"org.hsqldb.jdbcDriver",
					"jdbc:hsqldb:hsql://localhost",
					"sa",
					"");		
	EDbUtils.createHelperMap(objs);
	// テーブルの作成
	EDbUtils.createTables();			
		

次にオブジェクトをDBのテーブルに挿入する処理のブロックについて説明する

  1. 挿入するオブジェクトを生成する
  2. EDbUtils.insertObjectでDBのオブジェクトに対応するテーブルにレコードを挿入する
リスト 1.1.4 オブジェクトの挿入
	// Memberのインスタンをテーブルに挿入する例
	Member user1 = new Member("竹本", "東京都中野区");
	EDbUtils.insertObject(user1);			
		

DBからオブジェクトを取り出すには、idに対応するオブジェクト(5)を読み込むEDbUtils.loadObjectと複数のオブジェクトを取り出すEDbUtils.selectObjectsがある。EDbUtils.loadObjectの処理は、次のようになる。

  1. EDbUtils.loadObject(ロードするオブジェクトのクラス,オブジェクトの識別子)でオブジェクトをロードする
  2. オブジェクトの内容を出力する場合、EDbUtils.toString(出力するオブジェクト)を使うとオブジェクトのクラスでtoString()を定義する必要がないので、便利である
リスト 1.1.5 オブジェクトのロード
	// DBからIDを指定してオブジェクトをロードする例
	// user1と同じIDのオブジェクトをロードする		
	Member user2 = (Member)EDbUtils.loadObject(Member.class, user1.getId());
	System.out.println(EDbUtils.toString(user2));			
		

同様にEDbUtils.selectObjectsは、次のようになる。

  1. EDbUtils.selectObjects(SELECTするオブジェクトのクラス, SQL文のWHERE文)でオブジェクトを選択する。WHERE文に""を指定するとすべてのレコードが返される。
  2. EDbUtils.selectObjectsから戻されたListを順番に取り出し、出力する
リスト 1.1.6 オブジェクトのSELECT
			
	// テーブルからすべてのオブジェクトを取りだす例
	List list = EDbUtils.selectObjects(Member.class, "");
	for (int i = 0; i < list.size(); i++) {
		Member member = (Member)list.get(i);
		System.out.println(EDbUtils.toString(member));			
	}				
		
  1. スタンダードモードでも問題なく動作するが、デバッガで処理の途中のチェックをするためにサーバモードで起動している。特にトランザクションの処理のチェックには、有効である。
  2. 既にテーブルが存在する場合には、テーブルは作成しない
  3. idはテーブル毎に一意の値なので1個のみ読み込まれる

1.2 EDbHelperの処理

EDbUtilsでは、テーブルの作成、レコードの挿入、レコードの更新、レコードの削除等DbUtilsに含まれていない機能をヘルパークラスEDbHelperを内部で使って処理している。EDbHelperは、javaのリフレクションの機能を使ってBeanの属性とその型に応じたSQL文を生成する。Sample1で使ったMemberクラスを例にどのようなSQL文が生成されるか示す。Eclipseのスクラップを使って以下のプログラム断片を実行すると

Member member = new Member();
member.setId(10);
member.setName("Hiroshi TAKEMOTO");
member.setAddress("中野区本町");

EDbHelper helper = new EDbHelper(member);

System.out.println(helper.getCreateTableStmt());
System.out.println(helper.getInsertStmt(member));
System.out.println(helper.getUpdateStmt(member));
System.out.println(helper.getDeleteStmt(member));			
		

以下のような出力を得る。

CREATE TABLE T_MEMBER(ID IDENTITY NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR)

INSERT INTO T_MEMBER (ID,ADDRESS,NAME)VALUES(NULL,'中野区本町','Hiroshi TAKEMOTO')

UPDATE T_MEMBER SET ADDRESS='中野区本町',NAME='Hiroshi TAKEMOTO' WHERE ID=0

DELETE FROM T_MEMBER  WHERE ID=0			
		

テーブル名は、クラス名の前にT_が付き、クラスの属性と対応するSQL型を持ち、

のように各SQL文が組み立てられる。(6)

  1. レコードの識別子名をIDに統一することによってEDbHelperの構造も簡単になっている。

1.3 オブジェクト間の関連

オブジェクト間の関係をDBのテーブルにマッピングする場合を例に取ってみる。例として、本(Book)とその著者(People)の関係を1:1対応(7)について考えてみる。クラスの関係は、

のようにBookから著者authorを参照している。DBでは、参照するauthorオブジェクトの識別子をauhtorRefで関係付けするものとする。そのような場合、データのロードや挿入時にこれらの関係を処理する必要が出てくる。この処理をするインタフェースがIEPropertiesである。IEPropertiesインタフェースでは、

を実装する必要がある。

  1. 実際には共著があるので1:N対応だが簡単のためにここでは1:1対応で説明する

1.3.1 insertPropertiesの例

BookのinsertPropertiesは、つぎのようにものになる。

	public void insertProperties() {
		if (author != null) {
			EDbUtils.insertObject(author);
			authorRef = author.getId();
		}
	}				
			

著者への参照オブジェクトauthorがNULLで無かったら、authorをDBに挿入し、その識別子をauthorRefにセットする。

1.3.2 loadPropertiesの例

insertPropertiesとは、逆にloadPropertiesでは、Peopleテーブルへの参照IDであるauthorRefがNULLでない場合に、Peopleテーブルから著者authoreをloadObjectを使ってロードし、セットする。

	public void loadProperties() {
		if (authorRef != null)
			author = (People)EDbUtils.loadObject(People.class, authorRef);
		
	}							
			

1.3.3 printPropertiesの例

printPropertiesは、DBとは直接関係はないが、デバッグ時にオブジェクトの内容を出力するときに使用する。不要の場合には、空のメソッドを定義すればよい。Bookでは、"author->(著者オブジェクトの内容)"のような形式で出力している。

	public String printProperties() {
		if (author != null)
			return (" author->(" + EDbUtils.toString(author) + ")");
		else
			return (" author->NULL");
	}				
			

次にBook, PeopleのサンプルプログラムSample2.javaを示す。

	// Bookのインスタンをテーブルに挿入する例
	EDbUtils.insertObject(book);			
			

の部分でbookオブジェクトをDBに挿入するとその参照オブジェクトであるauthorも同時にDBに挿入されている点に注意されたい。

リスト 1.3.3.1 Sample2.java
package examples;

import javax.sql.DataSource;

import jp.co.pwv.estore.business.Book;
import jp.co.pwv.estore.business.People;
import jp.co.pwv.estore.util.EDbUtils;

/**
 * Sample1
 * 
 * @version 1.0 2004/09/03
 * @author Hiroshi TAKEMOTO
 */
public class Sample2 {
	public static void main(String[] args) {
		// テストデータを作成
		Book book = new Book();
		book.setTitle("1:1の例題");
		People author = new People();
		author.setName("Hiroshi TAKEMOTO");
		book.setAuthor(author);
		
		// EDbUtilsを使う前処理
		Object[]	objs = {book, author};
		DataSource ds = EDbUtils.createDatasource(
						"org.hsqldb.jdbcDriver",
						"jdbc:hsqldb:hsql://localhost",
						"sa",
						"");		
		EDbUtils.createHelperMap(objs);
		
		// テーブルの作成
		EDbUtils.createTables();
		
		// Bookのインスタンをテーブルに挿入する例
		EDbUtils.insertObject(book);

		// DBからIDを指定してオブジェクトをロードする例
		// user1と同じIDのオブジェクトをロードする
		Book ret = (Book)EDbUtils.loadObject(Book.class, book.getId());
		System.out.println(EDbUtils.toString(ret));

		// 使用済みのテーブルを削除(通常は不要)
		EDbUtils.dropTable(Book.class);
		EDbUtils.dropTable(People.class);
	}
}

1.3.4 線形リストの例

もう一つ線形リストにIEPropertiesを使った例を示す。線形リストLinkedListには、id, value, next, nextRefが属性として定義され、Bookと同様にIEPropertiesのメソッドを実装したのが、LinkedList.javaである。

リスト 1.3.4.1 LinkedList.java
package examples;

import jp.co.pwv.estore.util.EDbUtils;
import jp.co.pwv.estore.util.IEProperties;

/**
 * LinkedList
 *
 * @author Hiroshi TAKEMOTO
 */
public class LinkedList implements IEProperties {
	private Integer 	id;
	private String		value;
	private Integer	nextRef;
	private LinkedList	next;

	public LinkedList() {	
		this(null);	
	}
	public LinkedList(String value) {
		this.value = value;
	}
	public void add(LinkedList obj) {
		this.next = obj;
	}
	public void insertProperties() {
		if (next != null) {
			EDbUtils.insertObject(next);
			nextRef = next.getId();
		}
	}
	public void loadProperties() {
		if (nextRef != null)
			next = (LinkedList)EDbUtils.loadObject(LinkedList.class, nextRef);
		
	}
	public String printProperties() {
		if (next != null)
			return (" next->(" + EDbUtils.toString(next) + ")");
		else
			return (" next->NULL");
	}
	// Eclipseで自動生成されたgetter, setter
	public Integer getId() {
		return id;
	}
	public String getValue() {
		return value;
	}
	public LinkedList getNext() {
		return next;
	}
	public Integer getNextRef() {
		return nextRef;
	}
	public void setId(Integer integer) {
		id = integer;
	}
	public void setValue(String string) {
		value = string;
	}
	public void setNext(LinkedList list) {
		next = list;
	}
	public void setNextRef(Integer integer) {
		nextRef = integer;
	}
}

Sample2と同様に線形リストのトップの要素l1に対してのみEDbUtils.insertObject(l1)を実行するだけで、リスト上のすべての要素がDBに挿入され、トップの要素のに対するEDbUtils.loadObject(LinkedList.class, l1.getId())で線形リストの構造を完全に復元することができる。Sample3の実行結果は、次のようになる。(8)

LINKEDLIST: ID=2 NEXT=examples.LinkedList@1ac2f9c NEXTREF=1 VALUE=a 
	next->(LINKEDLIST: ID=1 NEXT=examples.LinkedList@169ca65 NEXTREF=0 VALUE=b 
		next->(LINKEDLIST: ID=0 VALUE=c next->NULL))				
			

すこし見にくいがl1->l2->l3のリンクが正常に復元されているのが分かる。Sample3のソースは、以下の通りである。

リスト 1.3.4.2 Sample3.java
package examples;

import javax.sql.DataSource;

import org.hsqldb.jdbcDriver;

import jp.co.pwv.estore.util.EDbUtils;
import jp.co.pwv.estore.util.IEProperties;

/**
 * Sample1
 * 
 * @version 1.0 2004/09/03
 * @author Hiroshi TAKEMOTO
 */
public class Sample3 {
	
	public static void main(String[] args) {
		// テストデータを作成
		LinkedList	l1 = new LinkedList("a");
		LinkedList	l2 = new LinkedList("b");
		LinkedList	l3 = new LinkedList("c");
		
		// l1->l2->l3の単方向のリストを作成
		l1.add(l2);
		l2.add(l3);		
		// EDbUtilsを使う前処理
		Object[]	objs = {l1};
		DataSource ds = EDbUtils.createDatasource(
						"org.hsqldb.jdbcDriver",
						"jdbc:hsqldb:hsql://localhost",
						"sa",
						"");		
		EDbUtils.createHelperMap(objs);		
		// テーブルの作成
		EDbUtils.createTables();		
		// Bookのインスタンをテーブルに挿入する例
		EDbUtils.insertObject(l1);
		// l1と同じIDのオブジェクトをロードする
		LinkedList ret = (LinkedList)EDbUtils.loadObject(LinkedList.class, l1.getId());
		System.out.println(EDbUtils.toString(ret));

		// 使用済みのテーブルを削除(通常は不要)
		EDbUtils.dropTable(LinkedList.class);
	}
}
  1. 出力が長いので、改行とインデントを付けて加工した

1.4 EDbHelper, EDbUtilsの制約

これまでの説明で、EDbUtilsを使うことによってオブジェクトとDBの連携がきわめてスムーズに行われているような印象を得るだろう。これにはいくつかのトリックがある。

  1. オブジェクトは、必ずidという属性を持つ必要がある。さらにDBのテーブルでは、IDを主キーとするテーブルが作成される
  2. 主キーの値は、HSQLDBのIDENTITYを使っており、使用するDBに依存する。
  3. 自動生成されるCREATE TABLE文には、フィールドの桁数が指定されていないため、運用時には、ALTER TABLE文を手で修正する必要がある。
  4. オブジェクトは、引数なしのコンストラクタを持つ必要がある。
  5. EDbHelperで扱えるオブジェクトに、内部クラスは使用できない。
  6. テーブルのフィールドにはpublicのgetter, setter、フィールドの型がINTEGER, DOUBLE, VARCHAR, BIT, DATE, TIMESTAMPが使用できる。(9)

EDbUtils, EDbHelperは、コメントを入れても1000行に満たない小さなユーティリティであるが、いくつかの規約と制限を享受することによって、DbUtilsというツールを有効に利用することが可能になる。また、説明が抜けていたが、volatile宣言された属性は、テーブルのフィールドへのマッピングは行われないので、この機能を利用すれば、不要なフィールドのDBへの投入を防ぐことができる。

  1. バイナリ型はサポートしていない

1.5 オブジェクトの更新と削除

オブジェクトの更新と削除について、EDbUtilsのjunit用単体テストコードを例に説明する。

1.5.1 オブジェクトの更新

オブジェクトの更新は、きわめて簡単で変更したオブジェクトに対して、EDbUtils.updateObject(変更したオブジェクト)を実行するだけである。testUpdateでは、Memberのaddressが"addr a"から"addr AAA"に更新され、EDbUtils.loadObjectでロードされたretと等しいことが確認できる。

リスト 1.5.1.1 testUpdate
	public void testUpdate() {
		Member user1 = new Member("a", "addr a");
		EDbUtils.insertObject(user1);
		user1.setAddress("addr AAA");
		EDbUtils.updateObject(user1);
		System.out.println(EDbUtils.toString(user1));

		Member ret = (Member)EDbUtils.loadObject(Member.class, user1.getId());
		
		assertEquals(EDbUtils.toString(user1), EDbUtils.toString(ret));
	}
			
			

1.5.2 オブジェクトの削除

オブジェクトの削除は、もっと簡単である。EDbUtils.deleteObjectに削除したいオブジェクトを渡すだけである。

	EDbUtils.deleteObject(user2);				
			

1.6 トランザクション処理

DbUtilsのドキュメントには、コネクションを接続してからの処理に対し、トランザクション処理を行うことができる旨の説明がある。これには、データベースのAUTOCOMMITをFALSEにする必要がある。EDbUtilsでは、loadObject, selectObject, updateObject, deleteObjectにコネクションとヘルパーマップを先頭に追加した引数を持つメソッドを提供し、トランザクション処理を実現できるようにしている。junitの単体テストメソッドtestTransactionを使ってトランザクション処理を説明する。

  1. Block1で、Memberクラスのオブジェクトuser1を生成、DBに挿入する
  2. Block2で、ヘルパーマップ、コネクションを取得し、setTransactionでAUTOCOMMIT FALSEにし、user2, user3を挿入する
  3. Block3で、user1と同じデータをロードし、nameを変更後、削除する
  4. Block4で、user1が削除され、テーブルの総数が2レコードであることを確認
  5. Block5で、ロールバックを実施
  6. Block6で、ロールバック後のDBにあるデータがuser1のみであることを確認
リスト 1.6.1 testTransaction
	public void testTransaction() {
		// Block 1:テーブルにuser1のデータ1個のみが存在する状態
		Member user1 = new Member("a", "addr a");
		EDbUtils.insertObject(user1);
		List list = EDbUtils.selectObjects(Member.class, "");
		assertEquals(1, list.size());
		
		try {
			// Block 2:ヘルパーマップ、コネクションを取得し、user2, user3を挿入する
			HashMap map = EDbUtils.getMap();
			Connection conn = ds.getConnection();
			// AUTOCOMMIT FALSEにする
			EDbUtils.setTransaction(conn);
			
			Member user2 = new Member("b", "addr b");
			EDbUtils.insertObject(conn, map, user2);
			Member user3 = new Member("c", "addr c");
			EDbUtils.insertObject(conn, map, user3);
			
			// Block 3:user1と同じデータをロードし、nameを変更後、削除する
			Member ret = (Member)EDbUtils.loadObject(Member.class, user1.getId());
			ret.setName("Takemoto");
			EDbUtils.updateObject(conn, map, ret);
			EDbUtils.deleteObject(conn, map, ret);
			
			// Block 4:user1が削除され、テーブルの総数が2レコードであることを確認
			list = EDbUtils.selectObjects(conn, Member.class, "");
			assertEquals(2, list.size());
			
			// Block 5:ロールバックを実施
			DbUtils.rollback(conn);
			DbUtils.closeQuietly(conn);
		
			// Block 6:ロールバック後のDBにあるデータがuser1のみであることを確認
			conn = ds.getConnection();
			list = EDbUtils.selectObjects(conn, Member.class, "");
			assertEquals(1, list.size());			
			assertEquals(EDbUtils.toString(user1), EDbUtils.toString(list.get(0)));
		}
		catch (Exception e) {
			fail();
		}
	}			
		

1.7 ツリー構造のサポート

コンポジットパターンのようなツリー構造をデータベースにマッピングするためにEDbUtilsを拡張し、以下のようなノードマップテーブルを導入し、複数のクラスのインスタンスからなるリストを扱えるユーティリティメソッドを追加した。(10)

表 1.7.1 ノードマップテーブル
カラム名 内容
ID INTEGER レコード識別子
OWNERCLSNAME VARCHAR 保有者のクラス名
OWNERREF INTEGER 保有者が割り当てたID
TARGETCLSNAME VARCHAR 実際のオブジェクトのクラス名
TARGETREF INTEGER オブジェクトのレコードの識別子

追加したメソッドは以下の通りである。(11)

表 1.7.2 追加メソッド
メソッド名 内容
insertMappedList リストをENODEMAPPERにマップし、DBに挿入する
loadMappedList 識別子ownerIdを持つオーナーオブジェクトのENODEMAPPERにマップされたリストをロードする
loadMappedObject ENODEMAPPERにマップされたオブジェクトをロードする
insertMappedObject オブジェクトをENODEMAPPERにマップし、DBに挿入する
  1. 現在のところ、正式サポートではなくあくまでも実験実装である。
  2. 詳しくは、AIPを参照されたい。

1.7.1 relaxerを使ったメニューツリー

relaxerは、RELAXで記述されたXMLをjavaのクラスにマッピングするツールであり、XMLとjavaのオブジェクトマッピングの他に、コンポジットパターン、ビジターパターンを扱う構造への変換をサポートしている。今回はrelaxerのビジターパターンを使って、relaxerによって自動生成されたexample.relaxer.menuパッケージのMenubar, Menu, Itemからそのサブクラスexmpale.relaxer.emenuに変換し、XMLファイルのツリー構造をデータベースに挿入とデータベースからの読み込みを行う例を示す。パッケージexample.relaxer.menu, example.relaxer.emenuのクラス関連図を以下に示す。

Menubarは、複数のMenuから構成され、Menuには、MenuまたはItemが含まれるツリー構造を持っている。relaxerのRELAXソースmenu.rlxは、参考文献のメニューサンプルから引用した。

リスト 1.7.1.1 Menu例題のRELAXソース
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//RELAX//DTD RELAX Core 1.0//JA"
			"relaxCore.dtd">
<module moduleVersion="1.0" relaxCoreVersion="1.0">
  <interface>
    <export label="menubar"/>
  </interface>
  <elementRule label="menubar">
    <tag>
    </tag>
    <sequence>
      <ref label="menu" occurs="*"/>
    </sequence>
  </elementRule>
  <elementRule label="menu">
    <tag>
    </tag>
    <sequence>
      <ref label="title"/>
      <choice occurs="*">
        <ref label="item"/>
        <ref label="menu"/>
      </choice>
    </sequence>
  </elementRule>
  <elementRule label="title" type="string">
    <tag/>
  </elementRule>
  <elementRule label="item" type="string">
    <tag>
    </tag>
  </elementRule>
</module>
				
			

RELAXソースからjavaのソースに変換するrelaxerの起動は、以下の通りです。

relaxer -visitor -package:examples.relaxer.menu menu.rlx				
			

1.7.2 ビジターの処理

relaxerの生成したソースを加工してDBへのアクセス処理を追加することも可能であるが、それでは仕様変更に弱い実装になる。そこでrelaxerの持つビジターパターンを使ってMenubar, Menu, Itemのツリー構造をEMenubar, EMenu, EItemのツリー構造に変換する。変換するビジターEMenuVisitorのソースを以下に示す。

リスト 1.7.2.1 EMenuVisitor.java
package examples.relaxer.emenu;

import java.util.Stack;

import  examples.relaxer.menu.*;

/**
 * @version menu.rxm 1.0 (Sun Sep 12 21:54:00 JST 2004)
 * @author  Relaxer 1.0rc2 (http://www.relaxer.org)
 */
public class EMenuVisitor extends RVisitorBase {
	EMenubar	menubar = null;
	Stack		stack = new Stack();
	
    /**
     * Visits this node for enter behavior.
     *
     * @param visitable
     * @return boolean
     */
    public boolean enter(Menubar visitable) {
    	menubar = new EMenubar(visitable);
    	menubar.clearMenu();
    	stack.push(menubar);
        return (true);
    }

    /**
     * Visits this node for leave behavior.
     *
     * @param visitable
     */
    public void leave(Menubar visitable) {
    	stack.pop();
    }

    /**
     * Visits this node for enter behavior.
     *
     * @param visitable
     * @return boolean
     */
    public boolean enter(Menu visitable) {
    	EMenu menu = new EMenu(visitable);
    	menu.clearContent();
    	IRNode parent = (IRNode)visitable.rGetParentRNode();
    	if (parent instanceof Menubar) {
    		EMenubar menubar = (EMenubar)stack.peek();
    		menubar.addMenu(menu);
    	}
    	else if (parent instanceof Menu) {
    		EMenu	menuItem = (EMenu)stack.peek();
			menuItem.addContent(menu);
    	}
    	stack.push(menu);
        return (true);
    }

    /**
     * Visits this node for leave behavior.
     *
     * @param visitable
     */
    public void leave(Menu visitable) {
    	stack.pop();
    }

    /**
     * Visits this node for enter behavior.
     *
     * @param visitable
     * @return boolean
     */
    public boolean enter(Item visitable) {
    	EItem	item = new EItem(visitable);
		IRNode parent = (IRNode)visitable.rGetParentRNode();
		if (parent instanceof Menu) {
			EMenu	menuItem = (EMenu)stack.peek();
			menuItem.addContent(item);
		}
		stack.push(item);
       return (true);
    }

    /**
     * Visits this node for leave behavior.
     *
     * @param visitable
     */
    public void leave(Item visitable) {
    	stack.pop();
    }

	/**
	 * @return
	 */
	public EMenubar getMenubar() {
		return menubar;
	}

}

ビジターは、relaxerが生成したRVisitorBase.javaをコピー&ペーストして作成した。ビジターの処理は、きわめて単純であり、

  1. enterメソッドのvisitable毎に対応するexample.relaxer.emenuパッケージのオブジェクトを生成する
  2. オブジェクトのコンポジットをクリアする
  3. スタックから取り出した親ノードに生成したオブジェクトを追加する(12)
  4. オブジェクトをスタックにプッシュする
  5. 各leaveメソッドにスタックのポップ処理を挿入する

を実装すればよい。

  1. EMenuの場合、IMenuChoiceインタフェースを持つEMenuとEitemの のいずれかの場合があるため、instaceofを使って処理を振り分ける。 IMenuChoiceに子ノードの追加機能があるといいのかも知れません。

1.7.3 独自クラスの実装

順序は逆になったが、パッケージexample.relaxer.emenuのクラスを実装する。独自クラスで行う処理は、きわめて簡単であり、

だけである。EMenu.javaを例にinsertPropertiesの処理を見る。基本的には、コンポジットがあれば、getNewOwnerIdメソッドで新しい所有者IDを取得し、それを元にinsertMappedListメソッドを呼び出すだけである。

	// DBで参照関係確認のために挿入
	if (rGetParentRNode() != null) {
		int parentId = EDbUtils.insertMappedObject(this, EDbUtils.getNewOwnerId(), rGetParentRNode());
		parentRNodeRef = new Integer(parentId);
	}
	// メニューがコンテンツを持っている場合
	if (getContent().length > 0) {
		contentOwnerId = EDbUtils.getNewOwnerId();
		EDbUtils.insertMappedList(this, contentOwnerId, Arrays.asList(getContent()));
	}				
			

同様にloadPropertiesは、contentOwnerIdがnullで無い場合、loadMappedListメソッドでコンポジットのリストを取得し、コンポジットに追加するだけである。(13)

	// メニューがコンテンツを持っていて、まだDBから読み込まれていない場合
	if (contentOwnerId != null && getContent().length == 0) {
		List list = EDbUtils.loadMappedList(this, contentOwnerId);
		if (list != null) {
			for (int i = 0; i < list.size(); i++) {
				addContent((IMenuChoice)list.get(i));
			}
		}			
	}
					
			

EMenubar, EMenu, EItemの完全なソースを以下に示す。

リスト 1.7.3.1 EMenubar.java
package examples.relaxer.emenu;

import java.util.Arrays;
import java.util.List;

import jp.co.pwv.estore.util.*;
import examples.relaxer.menu.*;

/**
 * EMenubar
 * @version 1.0	2004/09/08
 * @author Hiroshi TAKEMOTO
 */
public class EMenubar extends Menubar 
	implements IEProperties {
	private Integer	id;
	private Integer	parentRNodeRef;
	private Integer	menuOwnerId;

	public EMenubar() {
		super();
	}
	
	public EMenubar(Menubar menubar) {
		super(menubar);
	}
		
	public void insertProperties() {
		// DBで参照関係確認のために挿入
		if (rGetParentRNode() != null) {
			int parentId = EDbUtils.insertMappedObject(this, EDbUtils.getNewOwnerId(), rGetParentRNode());
			parentRNodeRef = new Integer(parentId);
		}
		// メニューを持っている場合
		if (getMenu().length > 0) {
			// List<EMenu>なので
			menuOwnerId = EDbUtils.getNewOwnerId();
			EDbUtils.insertMappedList(this, menuOwnerId, Arrays.asList(getMenu()));
		}
	}
	
	public void loadProperties() {
		// メニューバーがメニューを持っていて、まだロードされていない場合
		if (menuOwnerId != null && getMenu().length == 0) {		
			// List<EMenu>なので
			List list = EDbUtils.loadMappedList(this, menuOwnerId);
			if (list != null) {
				for (int i = 0; i < list.size(); i++) {
					addMenu((Menu)list.get(i));
				}
			}
		}
	}
	
	public String printProperties() {
		StringBuffer buf = new StringBuffer();
		if (parentRNodeRef != null) {
			buf.append(" parentRNodeRef = " + parentRNodeRef);
		}
		else {
			buf.append(" menuOwnerId = NULL");
		}
		if (menuOwnerId != null) {
			buf.append(" menuOwnerId = " + menuOwnerId);
		}
		else {
			buf.append(" menuOwnerId = NULL");
		}
		return (buf.toString());
	}

	public String toString() {
		return (super.toString());
	}
	// Eclipseで自動生成されたgetter, setter
	public Integer getId() {
		return id;
	}
	public Integer getParentRNodeRef() {
		return parentRNodeRef;
	}
	public void setId(Integer integer) {
		id = integer;
	}
	public void setParentRNodeRef(Integer integer) {
		parentRNodeRef = integer;
	}
	public Integer getMenuOwnerId() {
		return menuOwnerId;
	}
	public void setMenuOwnerId(Integer integer) {
		menuOwnerId = integer;
	}

}
リスト 1.7.3.2 EMenu.java
package examples.relaxer.emenu;

import java.util.Arrays;
import java.util.List;

import jp.co.pwv.estore.util.*;
import examples.relaxer.menu.*;

/**
 * EMenu
 * @version 1.0	2004/09/08
 * @author Hiroshi TAKEMOTO
 */
public class EMenu extends Menu 
	implements IEProperties {
	private Integer	id;
	private Integer	parentRNodeRef;
	private Integer	contentOwnerId;

	public EMenu() {
		super();
	}
	
	public EMenu(Menu menu) {
		super(menu);
	}
	
	public void insertProperties() {
		// DBで参照関係確認のために挿入
		if (rGetParentRNode() != null) {
			int parentId = EDbUtils.insertMappedObject(this, EDbUtils.getNewOwnerId(), rGetParentRNode());
			parentRNodeRef = new Integer(parentId);
		}
		// メニューがコンテンツを持っている場合
		if (getContent().length > 0) {
			contentOwnerId = EDbUtils.getNewOwnerId();
			EDbUtils.insertMappedList(this, contentOwnerId, Arrays.asList(getContent()));
		}
	}
	
	public void loadProperties() {
		// メニューがコンテンツを持っていて、まだDBから読み込まれていない場合
		if (contentOwnerId != null && getContent().length == 0) {
			List list = EDbUtils.loadMappedList(this, contentOwnerId);
			if (list != null) {
				for (int i = 0; i < list.size(); i++) {
					addContent((IMenuChoice)list.get(i));
				}
			}			
		}
	}
	
	public String printProperties() {
		StringBuffer buf = new StringBuffer();
		if (parentRNodeRef != null) {
			buf.append(" parentRNodeRef = " + parentRNodeRef);
		}
		else {
			buf.append(" menuOwnerId = NULL");
		}
		if (contentOwnerId != null) {
			buf.append(" contentOwnerId = " + contentOwnerId);
		}
		else {
			buf.append(" contentOwnerId = NULL");
		}
		return (buf.toString());
	}

	public String toString() {
		return (super.toString());
	}
	// Eclipseで自動生成されたgetter, setter
	public Integer getContentOwnerId() {
		return contentOwnerId;
	}
	public Integer getId() {
		return id;
	}
	public Integer getParentRNodeRef() {
		return parentRNodeRef;
	}
	public void setContentOwnerId(Integer integer) {
		contentOwnerId = integer;
	}
	public void setId(Integer integer) {
		id = integer;
	}
	public void setParentRNodeRef(Integer integer) {
		parentRNodeRef = integer;
	}

}
リスト 1.7.3.3 EItem.java
package examples.relaxer.emenu;

import jp.co.pwv.estore.util.*;
import examples.relaxer.menu.*;

/**
 * EItem
 * @version 1.0	2004/09/08
 * @author Hiroshi TAKEMOTO
 */
public class EItem extends Item 
	implements IEProperties {
	private Integer	id;
	private Integer	parentRNodeRef;

	public EItem() {
		super();
	}
	
	public EItem(Item item) {
		super(item);
	}
		
	public void insertProperties() {
		// DBで参照関係確認のために挿入
		if (rGetParentRNode() != null) {
			int parentId = EDbUtils.insertMappedObject(this, EDbUtils.getNewOwnerId(), rGetParentRNode());
			parentRNodeRef = new Integer(parentId);
		}
	}
	
	public void loadProperties() {
	}
	
	public String printProperties() {
		StringBuffer buf = new StringBuffer();
		if (parentRNodeRef != null) {
			buf.append(" parentRNodeRef = " + parentRNodeRef);
		}
		else {
			buf.append(" menuOwnerId = NULL");
		}
		return (buf.toString());
	}

	public String toString() {
		return (super.toString());
	}
	
	// Eclipseで自動生成されたgetter, setter
	public Integer getId() {
		return id;
	}

	public Integer getParentRNodeRef() {
		return parentRNodeRef;
	}

	public void setId(Integer integer) {
		id = integer;
	}

	public void setParentRNodeRef(Integer integer) {
		parentRNodeRef = integer;
	}
}
  1. 親ノードへの逆参照は、再帰的になるため、残念ならEDbUtilsでは処理できなかった。

1.7.4 テストプログラム

最後に、XMLからメニュー情報を読み込みDBに投入するテストプログラムを作成する。Sample4.javaがテストプログラムである。

  1. Menubarのuriに入力ファイルを指定し、Menubarを生成する。
  2. EMenuVisitorを生成し、URVisitor.traverseでMenubarの構造を変換する
  3. visitor.getMenubarメソッドでビジターから変換されたEMenubarオブジェクトを取得する
  4. insertObjectメソッドを使ってEMenubarオブジェクトの内容をDBに挿入する
  5. loadObjectは、DBから取り出す処理であり、toStringによって入力ファイルと同じ内容が出力することが確認できる
リスト 1.7.4.1 Sample4.java
package examples;

import javax.sql.DataSource;

import jp.co.pwv.estore.util.*;
import examples.relaxer.menu.*;
import examples.relaxer.emenu.*;

/**
 * Sample1
 * 
 * @version 1.0 2004/09/03
 * @author Hiroshi TAKEMOTO
 */
public class Sample4 {
	
	public static void main(String[] args) {
		// EDbUtilsを使う前処理
		Object[]	objs = {new ENodeMapper(),
									new EMenubar(),
									new EMenu(),
									new EItem()
						};
		DataSource ds = EDbUtils.createDatasource(
						"org.hsqldb.jdbcDriver",
						"jdbc:hsqldb:hsql://localhost",
						"sa",
						"");		
		EDbUtils.createHelperMap(objs);
				
		// 既存テーブルを削除(通常は不要)
		EDbUtils.dropTables();	
		// テーブルの作成
		EDbUtils.createTables();		
		
		//
		String uri = args[0];
		try {
			Menubar	orgMenubar = new Menubar(uri);
			EMenuVisitor	visitor = new EMenuVisitor();
			URVisitor.traverse(orgMenubar, visitor);
			EMenubar	menubar = visitor.getMenubar();
		
			// EMenubarのインスタンをテーブルに挿入する例
			EDbUtils.insertObject(menubar);
			//menubarと同じIDのオブジェクトをロードする
			EMenubar ret = (EMenubar)EDbUtils.loadObject(EMenubar.class, menubar.getId());
			System.out.println(EDbUtils.toString(ret));

		}
		catch (Exception e){
			System.err.println(e.toString());
		}
	}
}

テストデータを以下に示す。

<?xml version="1.0" encoding="Shift_JIS"?>

<!DOCTYPE menubar SYSTEM  "menu.dtd">

<menubar>
	<menu>
		<title>編集</title>
		<item>新規</item>
		<item>開く</item>
		<item>閉じる</item>
		<item>終了</item>
	</menu>

	<menu>
		<title>セットアップ</title>
		<menu>
			<title>ディスプレイ</title>
			<item>カラー</item>
			<item>フォント</item>
		</menu>
		<item>WWW</item>
	</menu>

	<menu>
		<title>ヘルプ</title>
		<item>Sample4</item>
	</menu>
</menubar>
					
			

参考文献

[3]加納 隆征; 羽生 彰洋. はじめてのHSQLDB : コンパクトながらタフなオープンソースRDBMS. WEB+DB p.139-149 17.技術評論社, 2003
[4]山根 健裕. JDBCプログラミングにすぐ効くDbUtils活用マニュアル : コンパクトでシンプルな超便利ライブラリ. WEB+DB p.66-75 20.技術評論社, 2004
[5]浅海 智晴. Relaxer : Java/XMLによるWeb開発. ピアソン・エデュケーション,