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」を作成します。
ソースコード
➢ SampleDTO.java① ソース・フォルダー :myjdbc_kanda/src
② パッケージ :jp.co.f1.jdbc.ch04
③ 名前 :SampleDTO
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; } }
➢ SampleDAO2.java① ソース・フォルダー :myjdbc_kanda/src
② パッケージ :jp.co.f1.jdbc.ch04
③ 名前 :SampleDAO2
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; } }
➢ InsertProgram2.java① ソース・フォルダー :myjdbc_kanda/src
② パッケージ :jp.co.f1.jdbc.ch04
③ 名前 :InsertProgram2
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる
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>オブジェクトを受け取っています。
検索メソッドの処理の中では検索する情報と戻り値が大きく違っています。各列のデータをそれぞれメソッドを使い取得していたInsertProgram1.javaと違い、全ての列を1つの検索メソッドで取得しています。
図 4.2.3: selectAll ()メソッドの流れ
図の※①の部分の処理ではSELECT文の結果セットから1行分のデータを取得し、DTOクラスのオブジェクトへ格納、その後、ArrayListにDTOクラスのオブジェクトを追加します。
while文とnext()メソッドを利用することでSELECT文の結果セットの各行のデータを格納しているDAOクラスのオブジェクトをArrayListに追加して行くことができます。※②では、全てのデータを格納したArrayListを戻り値として呼び出し元へ返しています。この※①と※②の処理の動きについては次の項4.2.4で詳しく解説していきますので、ここでは大まかな流れだけ覚えておいて下さい。
このプログラムでは、ArrayListの中でDTOクラスのオブジェクトを管理することで、全てのデータが1つのArrayListの中で管理されることになります。そのため、3つあった検索用メソッドを1つに統一することができるようになります。
28行目のデータの登録処理でもDTOクラスが利用されています。SampleDAO2クラスのinsertBook()メソッドを呼び出し、引数にDTOクラスを渡しています。
図 4.2.4: DTOを利用したinsertBook()メソッドの流れ
引数で渡されたDTOにはisbnとtitle、price情報が格納されていて、insertBook()メソッドではDTO内のデータ使ってSQL文を生成します。その際、DTOクラスの中のデータはprivate修飾子がついているため、ゲッターメソッドを利用して参照することになります。
34行目から38行目では、16行目から20行目と同じ処理を行い、登録結果を表示しています。
実行結果を確認すると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クラスのオブジェクトを格納できるようにしています。
図 4.2.6: ArrayList配列の作成
つぎに49行目です。ここからがデータをArrayListへ格納する処理になります。
49行目のwhile文とnextメソッドを組み合わせた処理は2.2.節のデータの検索で学習しましたが、結果セットのデータを1行ごとに参照しデータがある間、ループ内の処理を繰り返します。
1回目のループ
50行目では、SELECT文の結果セットを1行のデータを格納するためにDTOクラスのオブジェクトを作成しています。
図 4.2.7:SampleDTOオブジェクトの作成
51行目から53行目では結果セットの1行のデータをDTOオブジェクト内の変数に格納しています。
図 4.2.8: SampleDTOオブジェクトへの値の代入
54行目ではaddメソッドを利用し、DTOオブジェクトのアドレスをArrayListへ格納します。
図の①で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周するごとに前の周で格納したデータが上書きされてしまうのを防ぐ為です。
図 4.2.11:新しいSampleDTOオブジェクトの作成
51行目から53行目では、結果セットの2行のデータを新しいDTOオブジェクト内の変数に格納しています。
図 4.2.12: SampleDTOオブジェクトへの値の代入
54行目ではaddメソッドを利用し、DTOオブジェクトのアドレスをArrayListへ格納します。
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メソッドの呼び出し元に返します。
この場合、selectAllメソッドの呼び出し元にある戻り値を受け取る変数にはList実体の場所情報がコピーされ、データを参照することが可能になります。この処理の流れは以下の図のようになります。
図 4.2.17: return処理の流れ
図 4.2.18: return処理の流れ(場所情報のコピー)
以上がArrayListとDTOを利用したデータ管理の流れになります。
ポイント
- ArrayListとDTOを組み合わせて利用することでSELECT文の結果セットを1つのオブジェクトで管理することができる。
- ArrayListの型の指定にDTOクラスを利用することができる。
- ArrayListに格納する際に参照先をコピーしているため、DTOは使い回さず新規に作成する。