第1章
はじめてのデザインパターン  

第2章
逆引きカタログ ロジック編

第3章
逆引きカタログ J2EE編

第4章
逆引きカタログ その他

第5章
デザインパターン適用の勘所

第3章 逆引きカタログ J2EE編

Authors:Hashimoto Masanori

DAO (Data Access Object)

 
イントロダクション
パターン解説
具体的な例
まとめ
 

イントロダクション

 私たちが作るアプリケーションのほとんどは、どこかで永続的なデータを扱うことになります。 そのデータの保存先は、リレーショナルデータベースやテキストファイル、他システムなどになるでしょう。
 そして保存されたデータへのアクセスで使用するAPIは、保存先によって変わっていきます。 例えば、リレーショナルデータベースだとJDBCを使用します。 ファイルだとjava.ioパッケージあたりを使用したりします。
 また、リレーショナルデータベースのみに焦点を当ててみても、ベンダやバージョンによって発行するSQL文を変えなければなりません。
 ファイルに永続的なデータを保存していて、その保存先がデータベースに変更されたときのことを想像してください。 ビジネスロジック(業務ロジック)の中にデータアクセスにまつわるコードを書いている場合、保存先の変更が容易ではありません(同様のことが、データベースの変更時にもありえます)。 ビジネスロジックがデータの保存先のAPIに依存してしまっているのです。 また、弊害として、同じようなデータアクセスのロジックがいたるところあるような状態も生じるでしょう。
 そのようなときはDAO (Data Access Object) パターンを使ってみましょう。
 DAOパターンは、永続的なデータへのアクセスを、ビジネスロジックから抜き出すことができます。 また、インタフェースを使ってデータへのアクセス方法を抽象化しますので、ビジネスロジックではシンプルに永続的なデータを扱うことができるようになります。
TOP

パターン解説

 DAOパターンは、データアクセスをビジネスロジックから排除し、データアクセスオブジェクト(DAO)としてカプセル化します。
 データアクセスの手段や実装が変わっても、DAO自体がビジネスロジックに公開しているインタフェースは変わりませんので、ビジネスロジックの変更が発生しません。 DAOはデータ保存先の、アダプタのようなものになります(図12)。
図12 DAOパターンを適用した場合
 もし、データアクセスに関わるロジックがビジネスロジックの中にあったらどうなるでしょうか?
 データアクセスの手段や実装が変わると、ビジネスロジック中からデータアクセスをしている個所を見つけだし、変更していかなければなりません(図13)。
図13 DAOパターンを適用しなかった場合
 それは退屈で困難な作業になります。 DAOパターンは「データアクセス部分をまとめて整理整頓する技術」とも言えそうです。
TOP

具体的な例

 商品の倉庫管理を具体例にあげてDAOパターンを説明します。
 管理される倉庫の情報は、カテゴリ・商品名・個数・単価です。 その在庫の情報はリレーショナルデータベースに保存します。 在庫の管理者は、商品の追加・更新・削除・カテゴリごとでの検索ができます(図14)。
図14 サンプルのクラス図
 WarehouseLogicクラス(リスト17)に、ビジネスロジックのすべてを記述しています。
 サンプルはとても簡単なビジネスロジックしかありませんのでWarehouseLogicクラスの必要性が薄くなりますが、実際の業務アプリケーションは、このサンプルほどシンプルなものではありません。 業務アプリケーションでは、ビジネスロジックを記述するクラス(メソッド)は、とても複雑なものになります。
リスト17 WarehouseLogic.java

package daoSample.service;
@
import daoSample.DaoFactory;
import daoSample.Item;
import daoSample.ItemDao;
/** * 倉庫の処理クラスです。 */ public class warehouseLogic { /** データアクセスオブジェクトです。 */ private ItemDao dao = DaoFactory.createItemDao(); …A /** * カテゴリに関連する商品を取得します。 * @param category * @return Item[] */ public Item[] selectByCategory(String category) { return dao.selectByCategory(category); } (中略) }
 リスト17-Aが本題のデータアクセスオブジェクトです。
 DaoFactoryクラス(リスト18)経由でItemDaoインタフェース(リスト19)を実装するクラスを取得しています。 そして、各メソッドでItemDaoを使用してデータアクセスをします。 リスト17-@のように、JDBCやデータアクセスのフレームワークのAPIがインポートされていないのが、Daoを使用するクラスのポイントになります。
 リスト18のDaoFactoryクラスがなければ、Doa生成ロジックがビジネスロジックに紛れ込んでしまいます。 そこでDataSourceなどのAPIがビジネスロジックに入らないように、ファクトリクラスを利用します。 ファクトリクラスにて、すぐに使えるDaoクラスを生成します。
リスト18 DaoFactory.java

/**
 * DAOのファクトリクラスです。
 */
public class DaoFactry {

	/**
	 * ItemDaoを生成します。
	 * @return ItemDao
	 */
	public static ItemDao createItemDao() {
		return new jdbcItemDao(getDataSource());
	}

	/**
	 * DataSourceを取得します。
	 * @return DataSource
	 */
	private static DataSource getDataSource() {
		InitialContext initCon = null;
		DataSource ds = null;
		try {
@
			initCon = new InitialContext();
			ds = (DataSource)initCon.lookup(
				"java:comp/env/jdbc/HSQLDB");
} catch (NamingException e) { e.printStackTrace(); if (initCon != null) { try { initCon.close(); } catch (NamingException ex) { ex.printStackTrace(); throw new DataAccessException(); } } throw new DataAccessException(); } return ds; } }
 このDaoFactoryでは、リスト18-@にてJNDIを経由してDataSourceを取得して、JdbcItemDaoオブジェクトを生成しています。
 また、複数の保存先に対応するようなアプリケーションの場合、アブストラクトファクトリパターンを適用すると柔軟に対応できるようになります。 たとえば、保存先をDB2、Oracle、ファイルに切り替え可能なアプリケーションでは、アブストラクトファクトリパターンを使うと便利でしょう(図15)。
図15 サンプルのクラス図
 ビジネスロジック(WarehouseLogic.リスト17)の中では、DAOのインタフェース(ItemDao.リスト19)を使ってデータアクセスするように心がけます。
 すると使用するDAOの実装クラス(JdbcItemDao.リスト20)を変更しても、ビジネスロジックの修正は不要になります。 ビジネスロジックとDAOを疎結合にしておくのです。
リスト19 ItemDao.java

/**
 * 商品データにアクセスするインタフェースです。
 */
public interface ItemDao {

	/**
	 * カテゴリで商品データの配列を取得します。
	 * @param category
	 * @return Item[]
	 */
	abstract Item[] selectByCategory(String category);

	(中略)
}
リスト20 JdbcItemDao.java

/**
 * JDBC経由で商品データにアクセスするクラスです。
 */
public class JdbcItemDao implements ItemDao {

	/** コネクションです。 */
	private DataSource source;

	/**
	 * 新しいjdbcItemDaoのインスタンスを生成します。
	 * @param source
	 */
	public jdbcItemDao(DataSource source) {
		this.source = source;
	}

	/**
	 * 商品の情報をカテゴリをキーに検索します。
	 * @param category
	 */
	public Item[] selectByCategory(String category) {
		Resultset rs = null;
		PreparedStatement statement = null;
		Connection conn = null;
		try {
			StringBuffer sql = new StringBuffer();
			sql.append("select ");
			sql.append("  id, ");
			sql.append("  category, ");
			sql.append("  name, ");
			sql.append("  number, ");
			sql.append("  cost, ");
			sql.append("from  ");
			sql.append("  stock ");
			sql.append("where ");
			sql.append("  category=? ");

			conn = source.getConnection();
			statement = conn.prepareStatement(sql.toString());

			int index = 1;
			statement.setString(index++, category);

			rs = statement.executeQuery();
			List list = new ArrayList();
			while(rs.next()) {
				Item item = new Item();
				item.setId(rs.getInt("id"));
				item.setCategory(rs.getString("category"));
				item.setName(rs.getString("name"));
				item.setNumber(rs.getInt("number"));
				item.setCost(rs.getInt("cost"));
				list.add(item);
			}

			return (Item[]) list.toArray(new Item[list.size()]);
@
		} catch (SQLException e) {
			rollback(conn);
			throw new DataAccessException(e.getMessage(), e);
} finally { close(rs); close(statement); close(conn); } } (中略) /** * コミットします。 * @param conn */ private void commit(Connection conn) { if (conn != null) { try { conn.commit(); } catch (SQLException e) { throw new DataAccessException(e.getMessage(), e); } } } /** * ロールバックします。 * @param conn */ private void rollback(Connection conn) { if (conn != null) { try { conn.close(); } catch (SQLException e1) { throw new DataAccessException(e1.getMessage(), e); } } } /** * コネクションをクローズします。 * @param conn */ private void close(Connection conn) { if (conn != null) { try { conn.close(); } catch (SQLException e) { throw new DataAccessException(e.getMessage(), e); } } } /** * ステートメントをクローズします。 * @param statement */ private void close(PreparedStatement statement) { if (statement != null) { try { statement.close(); } catch (SQLException e) { throw new DataAccessException(e.getMessage, e); } } } /** * 結果セットをクローズします。 * @param rs */ private void close(Resultset rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { throw new DataAccessException(e.getMessage(), e); } } } }
 リスト20のJdbcItemDaoクラスが実際のDAOになります。
 データアクセスに関するロジックはこのDAOクラス内に記述します。
 サンプルでは、JDBCを直接使ってデータアクセスしていますが、この部分をO/RマッピングツールやJakartaDbUtilsなどを使って実装することが実際の業務では主流になってきています。
 データベースアクセスでは、SQLExceptionをキャッチしなくてはならないときが多くあります。 そのキャッチしたSQLExceptionを再度throwしていると、ビジネスロジックまでSQLExceptionが投げられてしまい、ビジネスロジックにJDBCのAPIが混入してしまうことになります。 この場合、SQLExceptionをキャッチしたら、別の例外に置き換えて、投げ直さなければなりません。
 JdbcItemDaoでは、リスト20-@のようにRuntimeExceptionを継承した実行時例外である「DataAccessException」で投げ直しています。 他にも、アプリケーション例外(検査例外)を作成し、それで投げ直す方法もあります。
 とにかく、SQLExceptionをビジネスロジックにまで投げてはいけません。 もし永続データの保存先がファイルの場合は、同様にIOExceptionやFileNotFoundExceptionをビジネスロジックまで投げてはいけません。

◎MockDaoを使う

 分散開発で、データアクセス部分ができないとビジネスロジックが書けない「実装の順番待ち」という状態がよくあります。 そうなると、時間的にもコスト的にもあまり良いことではありません。
 そのようなときにDAOパターンを適用していれば、仮のDAOクラス(ファサードパターンで解説したモックオブジェクト)を使ってビジネスロジックを実装していくことが可能です。
 サンプルで説明すると、JdbcItemDaoクラスができるまで、WarehouseLogicクラスのテストはできません。 しかし、JdbcItemDaoクラスは他社が実装することになっており、自社で作るわけにはいかないということが実際の業務では起こりがちです。
 そういうときにMockItemDaoクラス(リスト21)を作成するのです。
 作り方は簡単、ItemDaoクラスをimplementsして、データアクセスせずに格メソッドで仮のデータを返してあげればよいのです。
リスト21 MockItemDao.java

/**
 * 擬似の商品データにアクセスするクラスです。
 */
public class MockItemDao implements ItemDao {

	/**
	 * 商品の情報をIDをキーに検索します。
	 * @param id
	 */
	public Item selectById(int id) {
		Item item = new Item();
		item.setId(0);
		item.setCategory("食品");
		item.setName("テスト缶詰");
		item.setNumber(5);
		item.setCost(1000);
	}

	(中略)
}
TOP

まとめ

 DAOパターンを使うと、データアクセスをとてもシンプルに行うことができます。
 実際には「将来にわたって永続データの保存先が変わること」ということはなかなかありえません。 しかし、DAOパターンはビジネスロジックをシンプルにするためだけにでも有効ですし、現在スタンダードな手法となっています。
 DAOパターンは、基礎に「ファサードによる層の分割」があります(ファサードパターン参照)。 その分割された層のうちの「データアクセス層」というのが、DAOパターンなのです。
MEMO
JTA(Java Transaction API)でトランザクション管理
 実際にDAOパターンを使って一番悩むのがトランザクションではないでしょうか?
 トランザクションの管理には、JTAを使うと楽になります。 JTAには、トランザクションを制御するAPIが定義されています。 詳しくは、下記URLを参照してください。
TOP