DAO/DTOパターン

4.2 DAO/DTOパターン

 DAOパターンのみを利用した場合、メインロジックやデータアクセスに効率の良くない部分が出てきてしまいます。そこで利用されるのがDTOパターンになります。本節ではDAOパターンとセットで利用されるDTOパターンについて学習していきます。

4.2.1 DTOとは

 DTO(Data Transfer Object)は、Java基礎(上)テキストの3.3節で軽く触れているJavaBeans(ジャバビーンズ)の概念を基に作られた、「データの受け渡し専用のクラス」です。データを管理するためのフィールド変数とフィールド変数に対応したセッター、ゲッターが定義されるのが一般的です。ArrayListや配列では管理できない、型の異なるデータを一括で管理することができるため、データの受け渡しが容易になります。

4.2.2 DTOパターンとは

 DTOパターンとは、プログラムのデザインパターンの一種で、DTOクラスを作成し必要なデータの受け渡しを行います。前節ではDAOパターンのみを利用したプログラムを作成しましたが、戻り値や引数の型の影響によって、データを取得する際にカラムごとに検索用メソッドを利用したり、登録用メソッドの引数がカラムの数分必要になったりと、あまり効率的とは言えない部分があったと思います。このようなデメリットを解消するためにDAOパターンとセットでDTOパターンを利用します。

図 4.2.1 DAO/DTOパターンとデータベース接続

4.2.3 DAO/DTOパターンの基本的な構成とメリット

 DAOクラスとDTOクラスをセットで利用する場合、DTOクラスはデータベースの1レコードを管理できるクラスとして定義し、DAOクラスの更新系メソッドの引数や検索系メソッドの戻り値に利用します。
 DAOパターンのみを利用したプログラムに比べ、DTOパターンも利用したプログラムでは、以下のようなメリットがあります。

     
  • 1レコードのデータを一括で管理できるため、データの受け渡しが容易になる。
  •  
  • アクセス処理の回数を減らしパフォーマンスを向上することができる。

図 4.2.2: DTOを利用したデータの受け渡し

DAO/DTOパターンを利用したプログラム

 このプログラムは、DAOパターンを利用したプログラムにDTOパターンを追加し更に効率的な処理を行うように改良したプログラムです。DAO/DTOパターンを使用したプログラムの動きを確認しましょう。
 なお、このプログラムではデータを格納するための「SampleDTO.java」とデータベースアクセスを行う「SampleDAO2.java」、プログラムのメイン処理を記述する「InsertProgram2.java」を作成します。

ソースコード

① ソース・フォルダー      :myjdbc_kanda/src
② パッケージ          :jp.co.f1.jdbc.ch04
③ 名前             :SampleDTO

➢ SampleDTO.java
package jp.co.f1.jdbc.ch04;

public class SampleDTO {

	private String isbn;	// isbn
	private String title;	// タイトル
	private int price;	// 価格

	// コンストラクタ
 	public SampleDTO() {
 		// 初期化処理
 		isbn  = null;
 		title = null;
 		price = 0;
 	}
 
 	// isbnのゲッターメソッド
 	// フィールド変数isbnで管理された値を返す
 	public String getIsbn() {
 		return isbn;
 	}
 
 	// isbnのセッターメソッド
 	// 引数に受け取った値をフィールド変数isbnに格納する
 	public void setIsbn(String isbn) {
 		this.isbn = isbn;
 	}
 
 	// titleのゲッターメソッド
 	// フィールド変数titleで管理された値を返す
 	public String getTitle() {
 		return title;
 	}
 
 	// titleのセッターメソッド
 	// 引数に受け取った値をフィールド変数titleに格納する
 	public void setTitle(String title) {
 		this.title = title;
 	}
 
 	// priceのゲッターメソッド
 	// フィールド変数priceで管理された値を返す
 	public int getPrice() {
 		return price;
 	}
 
 	// priceのセッターメソッド
 	// 引数に受け取った値をフィールド変数priceに格納する
 	public void setPrice(int price) {
 		this.price = price;
 	}
 
 }

① ソース・フォルダー      :myjdbc_kanda/src
② パッケージ          :jp.co.f1.jdbc.ch04
③ 名前             :SampleDAO2

➢ SampleDAO2.java
package jp.co.f1.jdbc.ch04;

import java.sql.*;
import java.util.ArrayList;

public class SampleDAO2 {

	//接続用の情報をフィールドに定数として定義
	private static final String RDB_DRIVE="org.mariadb.jdbc.Driver";
 	private static final String URL="jdbc:mariadb://localhost/mybookdb";
 	private static final String USER="bms";
 	private static final String PASSWD="bms123";
 
 	// データベース接続を行うメソッド
 	// データベース接続用定義を基にデータベースへ接続し、戻り値としてコネクション情報を返す
 	private static Connection getConnection(){
 		try{
 			Class.forName(RDB_DRIVE);
 			Connection con = DriverManager.getConnection(URL, USER, PASSWD);
 			return con;
 		}catch(Exception e){
 			throw new IllegalStateException(e);
 		}
 	}
 
 	// データベースから全ての書籍情報の検索を行うメソッド
 	// テーブルに登録された全てのデータをArrayList<SampleDTO>型オブジェクトへ格納し、戻り値として返す
 	public ArrayList<SampleDTO> selectAll(){
 
 		// 変数宣言
 		Connection con = null;	// DBコネクション
 		Statement smt = null;	// SQLステートメント
 
 		// 配列宣言
 		ArrayList<SampleDTO> list = new ArrayList<SampleDTO>();
 
 		// SQL文作成
 		String sql = "SELECT * FROM bookinfo ORDER BY isbn";
 
 		try{
 			// DBに接続
 			con = SampleDAO2.getConnection();
 			smt = con.createStatement();
 
 			// SQL文発行
 			ResultSet rs = smt.executeQuery(sql);
 
 			// 検索結果をArrayListに格納
 			while(rs.next()){
 				SampleDTO objDto = new SampleDTO();
 				objDto.setIsbn(rs.getString("isbn"));
 				objDto.setTitle(rs.getString("title"));
 				objDto.setPrice(rs.getInt("price"));
 				list.add(objDto);
 			}
 
 		}catch(SQLException e){
 			System.out.println("Errorが発生しました!\n"+e);
 		}finally{
 			// リソースの開放
 			if(smt != null){
 				try{smt.close();}catch(SQLException ignore){}
 			}
 			if(con != null){
 				try{con.close();}catch(SQLException ignore){}
 			}
 		}
 		return list;
 	}
 
 	// 書籍情報を登録するメソッド
 	// 引数に渡された書籍情報をデータベースへ登録し、戻り値として登録件数を返す
 	public int insertBook(SampleDTO book){
 		// 変数宣言
 		Connection con = null;	// DBコネクション
 		Statement smt = null;	// SQLステートメント
 
 		int rowsCount = 0;
 
 		// SQL文
 		String sql = "INSERT INTO bookinfo(isbn,title,price) " +
 				"VALUES('" + book.getIsbn() + "','" + book.getTitle() + "'," + book.getPrice() + ")";
 
 		try{
 			// DBに接続
 			con = SampleDAO2.getConnection();
 			smt = con.createStatement();
 
 			// SQL文発行
 			rowsCount = smt.executeUpdate(sql);
 
 		}catch(SQLException e){
 			System.out.println("Errorが発生しました!\n"+ e +"\n");
 		}finally{
 			// リソースの開放
 			if(smt != null){
 				try{smt.close();}catch(SQLException ignore){}
 			}
 			if(con != null){
  				try{con.close();}catch(SQLException ignore){}
  			}
  		}
  		return rowsCount;
  	}
  
  }

① ソース・フォルダー      :myjdbc_kanda/src
② パッケージ          :jp.co.f1.jdbc.ch04
③ 名前             :InsertProgram2
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

➢ InsertProgram2.java
package jp.co.f1.jdbc.ch04;

import java.util.ArrayList;

public class InsertProgram2 {

	// 配列宣言
	private static ArrayList<SampleDTO>bookList = null;	// SampleDTO型のオブジェクトを格納するArrayList

	public static void main(String[] args) {
		try{
			// DAOオブジェクト化
			SampleDAO2 objDao = new SampleDAO2();

			// 書籍情報を取得するメソッドをDAOから呼び出す
			bookList = objDao.selectAll();	// 全ての情報を取得メソッド呼び出し

			// 取得した書籍情報を表示
			System.out.println("■登録SQL発行前の書籍一覧表示■");
			display();

			SampleDTO bookDto = new SampleDTO();
			bookDto.setIsbn("00009");
			bookDto.setTitle("Ruby入門テキスト");
			bookDto.setPrice(2500);

			// 書籍情報を登録するメソッドをDAOから呼び出す
			int rowsCount = objDao.insertBook(bookDto);
			if(rowsCount > 0){
				System.out.println(rowsCount + "件のレコードを登録しました。\n");
			}

			// 書籍情報を取得するメソッドをDAOから呼び出す
			bookList = objDao.selectAll();	// 全ての情報を取得メソッド呼び出し

			// 取得した書籍情報を表示
			System.out.println("■登録SQL発行後の書籍一覧表示■");
			display();

		}catch(Exception e){
			System.out.println("エラーが発生しました。" + e);
		}
	}

	public static void display() {
		for(int i=0;i<bookList.size();i++){
			SampleDTO bookDto = bookList.get(i);
			System.out.print("ISBN→"  + bookDto.getIsbn()  + "\t");
			System.out.print("Title→" + bookDto.getTitle() + "\t");
			System.out.print("Price→" + bookDto.getPrice() + "\n");
		}
		System.out.println();
	}

}

実行結果

解説

 このプログラムは4.1節で作成したプログラムにDTOクラスを追加し、効率よくデータの受け渡しを行えるように改修したプログラムです。プログラムの大きな流れはInsertProgram1.javaと変わりませんが、検索メソッドの呼び出しと戻り値、登録メソッドの引数の記述が変わっています。

 16行目ではSampleDAO2クラスのselectAll()メソッドを呼び出し、テーブルに登録された全ての情報を検索しています。その戻り値としてArrayList<SampleDTO>オブジェクトを受け取っています。
   16: bookList = objDao.selectAll();

 検索メソッドの処理の中では検索する情報と戻り値が大きく違っています。各列のデータをそれぞれメソッドを使い取得していたInsertProgram1.javaと違い、全ての列を1つの検索メソッドで取得しています。

図 4.2.3: selectAll ()メソッドの流れ

 図の※①の部分の処理ではSELECT文の結果セットから1行分のデータを取得し、DAOクラスのオブジェクトへ格納、その後、ArrayListにDAOクラスのオブジェクトを追加します。
 while文とnext()メソッドを利用することでSELECT文の結果セットの各行のデータを格納しているDAOクラスのオブジェクトをArrayListに追加して行くことができます。※②では、全てのデータを格納したArrayListを戻り値として呼び出し元へ返しています。この※①と※②の処理の動きについては次の項4.2.4で詳しく解説していきますので、ここでは大まかな流れだけ覚えておいて下さい。

 このプログラムでは、ArrayListの中でDTOクラスのオブジェクトを管理することで、全てのデータが1つのArrayListの中で管理されることになります。そのため、3つあった検索用メソッドを1つに統一することができるようになります。

 28行目のデータの登録処理でもDTOクラスが利用されています。SampleDAO2クラスのinsertBook()メソッドを呼び出し、引数にDTOクラスを渡しています。
   28: int rowsCount = objDao.insertBook(bookDto);

図 4.2.4: DTOを利用したinsertBook()メソッドの流れ

 引数で渡されたDTOにはisbnとtitle、price情報が格納されていて、insertBook()メソッドではDTO内のデータ使ってSQL文を生成します。その際、DTOクラスの中のデータはprivate修飾子がついているため、ゲッターメソッドを利用して参照することになります。

 34行目から38行目では、16行目から20行目と同じ処理を行い、登録結果を表示しています。
   34: bookList = objDao.selectAll();
   37: System.out.println("■登録SQL発行後の書籍一覧表示■");
   38: display();

 実行結果を確認すると1件のデータが正しく登録されているのが確認できます。

 DAOパターンのみを利用した場合に発生してしまったデメリットが、DTOパターンを利用することで改善されているのが分かります。このように2つのパターンを組み合わせて利用することが一般的となっています。

ポイント
  • DTOパターンを利用することで、データの受け渡しを効率的に行うことができる。
  • DTOとArrayListを組み合わせることでテーブルのデータを一括管理することができる。
  • DTOの値にアクセスするにはゲッターメソッド、またはセッターメソッドを利用する。

4.2.4 ArrayListとDTOクラスを利用したデータの管理

 前の項で作成したプログラムの検索用メソッドではSELECT文の結果セットから取得したデータをArrayListとDTOクラスを組み合わせ1つのオブジェクトで管理していました。本項では、その仕組みについて詳しく説明していきます。

図 4.2.5: selectAll ()メソッドの流れ

 このメソッドでは次のように処理が行われています。
 35行目では全てのデータを格納するためのArrayListを宣言しています。クラス名の後ろに<SampleDTO>を付けることでArrayList内にSampleDTOクラスのオブジェクトを格納できるようにしています。
   35: ArrayList<SampleDTO> list = new ArrayList<SampleDTO>();

図 4.2.6: ArrayList配列の作成

 つぎに49行目です。ここからがデータをArrayListへ格納する処理になります。
 49行目のwhile文とnextメソッドを組み合わせた処理は2.2.節のデータの検索で学習しましたが、結果セットのデータを1行ごとに参照しデータがある間、ループ内の処理を繰り返します。    49: while(rs.next()){

1回目のループ
 50行目では、SELECT文の結果セットを1行のデータを格納するためにDTOクラスのオブジェクトを作成しています。
   50: SampleDTO objDto = new SampleDTO();

図 4.2.7:SampleDTOオブジェクトの作成

 51行目から53行目では結果セットの1行のデータをDTOオブジェクト内の変数に格納しています。
   51: objDto.setIsbn(rs.getString("isbn"));
   52: objDto.setTitle(rs.getString("title"));
   53: objDto.setPrice(rs.getInt("price"));

図 4.2.8: SampleDTOオブジェクトへの値の代入

 54行目ではaddメソッドを利用し、DTOオブジェクトのアドレスをArrayListへ格納します。
   54: list.add(objDto);

 図の①でDTOオブジェクトの場所情報をコピーしています。その結果、②のように同じオブジェクトを参照するようになります。

図 4.2.9: ArrayListへDTOオブジェクトの格納

 1回目のループが終了すると、以下のように1つにDTOオブジェクトが追加された状態になります。

図 4.2.10: 1回目のループ終了時のArrayListの構成

 1周目の処理が終了するとwhileの条件であるnext()メソッドの戻り値が判断されます。
 このプログラムでは2行目のデータが存在するため、戻り値としてtrueが返され、while文の中の処理が実行されます。

2回目のループ
 2周目の50行目では1周目とは別のDTOオブジェクトを作成します。
 同じDTOオブジェクトを使いまわすと各行のデータを格納するアドレスが同じものになってしまい1周するごとに前の周で格納したデータが上書きされてしまうのを防ぐ為です。
   50: SampleDTO objDto = new SampleDTO();

図 4.2.11:新しいSampleDTOオブジェクトの作成

 51行目から53行目では、結果セットの2行のデータを新しいDTOオブジェクト内の変数に格納しています。
   51: objDto.setIsbn(rs.getString("isbn"));
   52: objDto.setTitle(rs.getString("title"));
   53: objDto.setPrice(rs.getInt("price"));

図 4.2.12: SampleDTOオブジェクトへの値の代入

 54行目ではaddメソッドを利用し、DTOオブジェクトのアドレスをArrayListへ格納します。
   54: list.add(objDto);

 1回目のループと同じように、図の①でDTOオブジェクトの場所情報をコピーし、その結果、②のように同じオブジェクトを参照するようになります。なお、1回目のループで要素[0]にデータが入っているため、2回目のループでは要素[1]にデータが格納されます。

図 4.2.13: 2回目のArrayListへDTOオブジェクトの格納

 2回目のループが終了すると以下のように2つ目のDTOオブジェクトが追加された状態になります。

図 4.2.14: 2回目のループ終了時のArrayListの構成

3回目から7回目のループ
 next()メソッドを使った判定はデータがある限り、同じ処理が繰り返されます。そのため、3回目から7回目も2回目と同じ処理が行われます。

8回目のループ
 8回目の処理ではDTOオブジェクトには結果セットの最後の行のデータが格納されます。

図 4.2.15: 8回目のSampleDTOオブジェクトへの値の代入

 最後の行のデータが格納されたDTOオブジェクトを、add()メソッドを使いArrayListに格納すると、以下のように8つのデータが追加された状態になります。

図 4.2.16: 8回目のループ終了時のArrayListの構成

ループ終了後の処理
 ループが終了すると、68行目のreturn文を使って検索結果のデータが管理されたArrayListをselectAllメソッドの呼び出し元に返します。
   68: return list;

 この場合、selectAllメソッドの呼び出し元にある戻り値を受け取る変数にはList実体の場所情報がコピーされ、データを参照することが可能になります。この処理の流れは以下の図のようになります。

図 4.2.17: return処理の流れ

図 4.2.18: return処理の流れ(場所情報のコピー)

 以上がArrayListとDTOを利用したデータ管理の流れになります。

ポイント
  • ArrayListとDTOを組み合わせて利用することでSELECT文の結果セットを1つのオブジェクトで管理することができる。
  • ArrayListの型の指定にDTOクラスを利用することができる。
  • ArrayListに格納する際に参照先をコピーしているため、DTOは使い回さず新規に作成する。

NEXT>> 4.3 本章のまとめ