サーブレット&JSPの基本|DAOを使ったデータベースアクセス

JDBCの複雑な処理をDAOに任せる。データベースアクセスを分離して、読みやすく保守しやすいJavaプログラムにしよう

前回は、JDBCを使ってJavaプログラムからH2 Databaseに接続し、EMPLOYEESテーブルのデータを取得しました。

Javaプログラムからデータベースを利用できるようになると、Webアプリケーションで扱えるデータの幅が一気に広がります。

たとえば、従業員一覧を表示したり、投稿内容を保存したり、ログインユーザーを検索したり、商品情報を登録したりできます。

ただし、JDBCプログラムには少し大きな問題があります。

それは、データベースを利用するためのコードが多く、プログラム全体が読みにくくなりやすいことです。

JDBCドライバの読み込み、データベース接続、SQL文の準備、SQLの実行、ResultSetからの値取得、SQLExceptionの処理など、JDBC特有の処理を毎回書く必要があります。

本来やりたいことが「従業員一覧を取得して表示する」だけでも、その周りにJDBC特有の処理がたくさん入り込んでしまいます。

プログラムが小さいうちは、まだ何とか読めるかもしれません。

しかし、アプリケーションが大きくなり、あちこちのクラスでデータベースに接続するようになると、修正がとても大変になります。

そこで登場するのがDAOパターンです。

DAOとは、Data Access Objectの略です。

DAOパターンでは、データベースアクセスを担当する専用のクラスを作ります。

データベースを利用したいクラスは、直接JDBCの処理を書かず、DAOクラスのメソッドを呼び出します。

この記事では、JDBCプログラムが複雑になりやすい理由、DAOパターンの考え方、EMPLOYEESテーブルを担当するEmployeesDAOの作り方、DAOを使ったSelectEmployees.javaの書き方、そしてWebアプリケーションとDAOの関係を学習します。

JDBCプログラムの問題

JDBCを使うと、Javaプログラムからデータベースを操作できます。

しかし、JDBCプログラムには多くの手続きが必要です。

たとえば、EMPLOYEESテーブルから全従業員情報を取得するだけでも、次のような処理が必要でした。

JDBCプログラムで必要になる処理

処理内容
JDBCドライバの読み込みH2 Database用のドライバを読み込む
データベース接続DriverManagerからConnectionを取得する
SQL文の準備SELECT文を文字列として用意する
PreparedStatementの作成SQLを実行するためのPreparedStatementを取得する
SQLの実行executeQueryでSELECT文を実行する
ResultSetの処理検索結果を1行ずつ取り出す
列の値取得getStringやgetIntで値を取得する
例外処理SQLExceptionなどを処理する
接続の終了データベース接続を閉じる

これらは、データベースを使うためには必要な処理です。

ただし、プログラム本来の目的とは少し違います。

従業員一覧を表示するプログラムで本当に注目したいのは、従業員データを取得して表示する部分です。

しかし、JDBC特有の処理が多く入り込むと、本来の目的が見えにくくなります。

JDBC特有のコードが増えると読みにくくなる

JDBCプログラムでは、Connection、PreparedStatement、ResultSet、SQLExceptionなど、データベース操作に関係する部品をたくさん使います。

これらを、データベースを利用するたびに毎回書いていると、ソースコードが長くなります。

コードが読みにくくなる理由

理由内容
JDBCの処理が多い接続、SQL実行、結果取得、例外処理が必要になる
本来の処理が埋もれる従業員を表示する処理が見つけにくくなる
修正箇所が増えるSQLや接続情報が複数のクラスに散らばる
バグの原因を探しにくい処理が混ざり、どこに問題があるか追いにくい

ソースコードの見通しが悪くなると、バグが入りやすくなります。

また、不具合が起きたときに原因を探すのにも時間がかかります。

プログラムが小さいうちは気合で読めるかもしれませんが、業務アプリケーションのようにクラス数が増えると、気合だけでは対応できなくなります。

データベース変更への対応が大変になる

JDBC特有のコードが複数のクラスに散らばっていると、データベースの仕様が変わったときに大きな問題になります。

たとえば、EMPLOYEESテーブルの列名が変わったり、接続先データベースが変わったり、SQL文を修正する必要が出たりしたとします。

もし、たくさんのクラスがそれぞれ直接データベースにアクセスしていたら、それらすべてのクラスを修正しなければなりません。

直接JDBCを書く場合の問題

変更内容影響
JDBC URLが変わる接続処理を書いているすべてのクラスを修正する必要がある
テーブル名が変わるSQLを書いているすべてのクラスを修正する必要がある
列名が変わるResultSetから値を取得している箇所を修正する必要がある
SQLの条件が変わる同じようなSQLが複数あれば修正漏れが起こりやすい

クラスが数個ならまだ対応できます。

しかし、データベースを利用するクラスが20個、50個、100個と増えたら、修正漏れを防ぐのはかなり大変です。

このような状態は、保守性が低い状態といえます。

図1:JDBC特有のコードが複数クラスに散らばる問題

この図から分かること

データベースを利用するクラスが、それぞれ直接JDBCの処理を書いていると、Connection、PreparedStatement、ResultSet、SQL文、例外処理があちこちに散らばります。

その状態でデータベースの接続先やテーブル構成が変わると、多くのクラスを修正しなければなりません。

修正箇所が多いほど、修正漏れや不具合が起こりやすくなります。

この問題を解決するために、データベースアクセスを専門に担当するDAOクラスを用意します。

DAOパターンとは

DAOパターンは、データベースアクセスを専用のクラスにまとめる設計方法です。

DAOは、Data Access Objectの略です。

簡単に言えば、データベース操作の専門担当者を作る考え方です。

データベースを利用したいクラスは、直接ConnectionやPreparedStatementを使いません。

代わりに、DAOクラスのメソッドを呼び出します。

DAOクラスの中で、JDBCドライバの読み込み、データベース接続、SQL実行、ResultSet処理などを行います。

DAOが担当すること

DAOクラスは、データベース操作を担当します。

具体的には、テーブルに対する検索、追加、変更、削除などを行います。

DAOクラスの主な役割

役割内容
検索SELECT文を実行してデータを取得する
追加INSERT文を実行してレコードを追加する
変更UPDATE文を実行してレコードを更新する
削除DELETE文を実行してレコードを削除する
変換ResultSetの値をJavaのインスタンスに詰め替える

今回の記事では、EMPLOYEESテーブルの全レコードを検索するfindAllメソッドを持つEmployeesDAOを作成します。

findAllメソッドは、EMPLOYEESテーブルから全従業員データを取得し、Employeeインスタンスのリストとして呼び出し元に返します。

DAOパターンのメリット

DAOパターンを使うと、データベースを利用するクラスからJDBC特有のコードを取り除けます。

その結果、データベースを利用する側のクラスは、何をしたいのかが分かりやすくなります。

DAOパターンのメリット

メリット内容
JDBCの知識が少なくても利用できるDAOのメソッドを呼び出せばデータを取得できる
コードの見通しがよくなるデータベース処理と本来の処理を分けられる
仕様変更に対応しやすいDB接続やSQLの修正をDAOにまとめやすい
修正箇所を減らせるデータベースアクセス処理を1か所に集められる
役割分担しやすいDB担当の処理と画面・業務処理を分離できる

DAOパターンでは、データベース操作の詳細をDAOに隠します。

呼び出す側は、findAllなどの分かりやすいメソッドを使うだけで、データベースの情報を取得できます。

DAOクラスの名前の付け方

DAOクラスは、テーブルごとに作成することがよくあります。

クラス名は、テーブル名にDAOを付けた名前にするのが一般的です。

DAOクラス名の例

テーブル名DAOクラス名
EMPLOYEESEmployeesDAO
USERSUsersDAO
PRODUCTSProductsDAO
MESSAGESMessagesDAO

今回使うテーブルはEMPLOYEESテーブルなので、DAOクラス名はEmployeesDAOにします。

EMPLOYEESテーブルの1件分のデータを表すクラスとして、Employeeクラスも作成します。

図2:DAOパターンでデータベースアクセスを1か所にまとめる

この図から分かること

DAOパターンでは、データベースを利用するクラスが直接データベースにアクセスしません。

代わりに、EmployeesDAOのようなDAOクラスを呼び出します。

DAOクラスの中に、Connection、PreparedStatement、ResultSet、SQL文、例外処理などのJDBC特有の処理をまとめます。

これにより、データベースの接続情報やSQLを修正する場合でも、修正箇所をDAOに集めやすくなります。

DAOパターンで作成するクラス

今回は、EMPLOYEESテーブルの全従業員情報を取得するために、次の3つのクラスを作成します。

作成するクラス

クラスパッケージ役割
EmployeemodelEMPLOYEESテーブルの1件分のデータを表す
EmployeesDAOdaoEMPLOYEESテーブルへのデータベースアクセスを担当する
SelectEmployeesデフォルトパッケージEmployeesDAOを利用して全従業員情報を表示する

Employeeクラスは、EMPLOYEESテーブルの1行分のデータを入れるクラスです。

EmployeesDAOクラスは、EMPLOYEESテーブルにアクセスし、全レコードを取得します。

SelectEmployeesクラスは、EmployeesDAOのfindAllメソッドを呼び出し、取得した従業員情報をコンソールに表示します。

今回使うEMPLOYEESテーブルのデータ

この記事では、EMPLOYEESテーブルのレコードを次の内容に差し替えて説明します。

EMPLOYEESテーブル

IDNAMEAGE
EMP001高橋 健一34
EMP002中村 優子28

この2件のレコードを、EmployeesDAOのfindAllメソッドで取得します。

取得した各レコードは、Employeeインスタンスに変換されます。

そして、EmployeeインスタンスをListに追加して、呼び出し元に返します。

Employeeクラスとは

Employeeクラスは、EMPLOYEESテーブルの1件分のデータを表すクラスです。

EMPLOYEESテーブルには、ID、NAME、AGEという列があります。

そのため、Employeeクラスにも、id、name、ageというフィールドを用意します。

Employeeクラスの役割

フィールド対応する列内容
idID従業員ID
nameNAME従業員名
ageAGE年齢

DAOは、ResultSetから取り出した値をEmployeeインスタンスに詰め替えます。

Javaプログラム側では、Employeeインスタンスとして従業員データを扱えます。

EMPLOYEESテーブルのレコードを表すクラスの例

ファイル名: Employee.java

package model;

public class Employee {
    private String id;
    private String name;
    private int age;

    public Employee(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

Employee.javaは、modelパッケージに作成します。

このクラスは、EMPLOYEESテーブルの1行分のデータをJavaで扱うための入れ物です。

コンストラクタでは、id、name、ageを受け取り、それぞれのフィールドに設定しています。

getterメソッドを用意しているため、ほかのクラスから従業員ID、名前、年齢を取得できます。

EmployeesDAOのfindAllメソッド

EmployeesDAOには、findAllメソッドを用意します。

findAllメソッドは、EMPLOYEESテーブルからすべてのレコードを取得し、Listとして返します。

findAllメソッドの処理

順番処理内容
1EMPLOYEESテーブルからレコードを取得するSELECT文を実行する
2ResultSetから列の値を取り出すID、NAME、AGEを取得する
3Employeeインスタンスを作成する取得した値をコンストラクタに渡す
4Listに追加する作成したEmployeeをempListに追加する
5レコードの数だけ繰り返すwhile文でResultSetを処理する
6Listを返す呼び出し元へ従業員一覧を返す

DAOの中ではJDBCの処理を行います。

ただし、DAOを呼び出す側は、JDBCの詳細を意識しなくて済みます。

図3:EmployeesDAOのfindAllメソッドがListを返す流れ

この図から分かること

SelectEmployeesクラスは、EmployeesDAOのfindAllメソッドを呼び出します。

EmployeesDAOは、EMPLOYEESテーブルに対してSELECT文を実行し、ResultSetを取得します。

ResultSetから1行ずつID、NAME、AGEを取り出し、それぞれEmployeeインスタンスに変換します。

作成したEmployeeインスタンスはArrayListに追加されます。

最後に、Employeeインスタンスが入ったListが呼び出し元へ返されます。

EMPLOYEESテーブルを担当するDAOの例

ファイル名: EmployeesDAO.java

package dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import model.Employee;

public class EmployeesDAO {
    // データベース接続に使用する情報
    private final String JDBC_URL =
            "jdbc:h2:tcp://localhost/~/study";
    private final String DB_USER = "sa";
    private final String DB_PASS = "";

    public List<Employee> findAll() {
        List<Employee> empList = new ArrayList<>();

        // JDBCドライバを読み込む
        try {
            Class.forName("org.h2.Driver");
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(
                    "JDBCドライバを読み込めませんでした");
        }

        // データベースに接続
        try (Connection conn = DriverManager.getConnection(
                JDBC_URL, DB_USER, DB_PASS)) {

            // SELECT文を準備
            String sql = "SELECT ID, NAME, AGE FROM EMPLOYEES";
            PreparedStatement pStmt = conn.prepareStatement(sql);

            // SELECT文を実行し、結果表を取得
            ResultSet rs = pStmt.executeQuery();

            // 結果表に格納されたレコードの内容を
            // Employeeインスタンスに設定し、ArrayListインスタンスに追加
            while (rs.next()) {
                String id = rs.getString("ID");
                String name = rs.getString("NAME");
                int age = rs.getInt("AGE");

                Employee employee = new Employee(id, name, age);
                empList.add(employee);
            }
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }

        return empList;
    }
}

EmployeesDAO.javaは、daoパッケージに作成します。

このクラスは、EMPLOYEESテーブルへのデータベースアクセスを担当します。

データベースの接続先、ユーザー名、パスワードは、フィールドとしてまとめています。

JDBC_URLには、studyデータベースへサーバーモードで接続するためのJDBC URLを指定しています。

findAllメソッドでは、EMPLOYEESテーブルから全レコードを取得し、EmployeeインスタンスのListとして返します。

EmployeesDAOで注目したいポイント

EmployeesDAOには、JDBCプログラム特有の処理がまとまっています。

EmployeesDAOに含まれるJDBC処理

処理内容
Class.forNameH2のJDBCドライバを読み込む
DriverManager.getConnectionstudyデータベースへ接続する
ConnectionDBMSとの接続を表す
PreparedStatementSELECT文を実行する
ResultSet検索結果を受け取る
SQLExceptionデータベース処理の例外を扱う

これらの処理は、データベースアクセスに必要です。

ただし、これらをすべてのクラスに書くのではなく、DAOにまとめることが大切です。

EmployeesDAOにまとめておけば、データベースの接続先やSQLを変更する場合も、EmployeesDAOを中心に修正できます。

findAllメソッドの戻り値

findAllメソッドの戻り値は、Listです。

これは、Employeeインスタンスを複数格納できるリストです。

EMPLOYEESテーブルに2件のレコードがあれば、Listの中には2つのEmployeeインスタンスが入ります。

Listのイメージ

インデックスidnameage
0EMP001高橋 健一34
1EMP002中村 優子28

呼び出し元のクラスは、このListを受け取り、拡張for文などで1件ずつ処理できます。

DAOの外側では、ResultSetを直接扱う必要がありません。

DAOを利用して全従業員情報を表示するクラスの例

EmployeesDAOを利用すると、前回のSelectEmployees.javaはかなりすっきりします。

ファイル名: SelectEmployees.java

import java.util.List;

import dao.EmployeesDAO;
import model.Employee;

public class SelectEmployees {
    public static void main(String[] args) {
        // EMPLOYEESテーブルの全レコードを取得
        EmployeesDAO empDAO = new EmployeesDAO();
        List<Employee> empList = empDAO.findAll();

        // 取得したレコードの内容を出力
        for (Employee emp : empList) {
            System.out.println("ID:" + emp.getId());
            System.out.println("名前:" + emp.getName());
            System.out.println("年齢:" + emp.getAge() + "\n");
        }
    }
}

SelectEmployees.javaは、デフォルトパッケージに作成する想定です。

このクラスでは、java.sqlパッケージをimportしていません。

Connection、PreparedStatement、ResultSet、SQLExceptionも登場しません。

データベースから全従業員情報を取得する処理は、EmployeesDAOのfindAllメソッドに任せています。

SelectEmployees.javaは、取得したListを受け取り、1件ずつ表示することに集中しています。

DAOを使ったSelectEmployees.javaの見方

DAOを使ったSelectEmployees.javaでは、本来の処理が分かりやすくなっています。

SelectEmployees.javaで行っていること

順番処理内容
1EmployeesDAOを作成するEMPLOYEESテーブル担当のDAOを用意する
2findAllを呼び出す全従業員情報を取得する
3Listを受け取るEmployeeインスタンスの一覧を受け取る
4拡張for文で繰り返す従業員データを1件ずつ取り出す
5getterで値を取得するID、名前、年齢を取り出す
6コンソールに表示する取得した情報を出力する

このクラスを見ると、従業員一覧を取得して表示していることがすぐに分かります。

JDBCの接続処理やSQL実行処理はEmployeesDAOの中にあるため、SelectEmployees.javaはとても読みやすくなっています。

実行結果

EMPLOYEESテーブルに次のデータが入っている場合を考えます。

EMPLOYEESテーブル

IDNAMEAGE
EMP001山田 太郎30
EMP002佐藤 花子27

SelectEmployees.javaを実行すると、Eclipseのコンソールビューには次のように表示されます。

ID:EMP001
名前:山田 太郎
年齢:30

ID:EMP002
名前:佐藤 花子
年齢:27

実行結果は、前回のJDBCプログラムと同じように従業員情報を表示しています。

しかし、SelectEmployees.javaの中身は大きく変わっています。

JDBC特有の処理はEmployeesDAOに移動し、SelectEmployees.javaには本来の表示処理だけが残っています。

DAOを使う前と使った後の違い

DAOを使うと、データベースを利用する側のクラスが大きく簡潔になります。

比較ポイント

項目DAOを使わない場合DAOを使う場合
JDBCドライバ読み込み利用クラスに書くDAOに書く
データベース接続利用クラスに書くDAOに書く
SQL文利用クラスに書くDAOに書く
ResultSet処理利用クラスに書くDAOに書く
SQLException処理利用クラスに書くDAOに書く
利用クラスの役割DB処理と本来の処理が混ざる本来の処理に集中できる
修正しやすさ複数箇所の修正になりやすいDAOを中心に修正しやすい

DAOを使うことで、データベースを利用する側のクラスからJDBC特有のコードを取り除けます。

その結果、プログラムの見通しがよくなります。

また、データベース接続情報やSQL文の変更があった場合も、DAOを修正すれば済む場面が多くなります。

DAOパターンとデザインパターン

DAOパターンは、デザインパターンの1つです。

デザインパターンとは、よくある設計上の問題に対して、先人たちが考えた定番の解決方法です。

簡単に言えば、こういう場面ではこのようにクラスを設計すると、開発しやすく、後から修正しやすい、という設計の型です。

デザインパターンの考え方

項目内容
目的開発しやすく、保守しやすい設計にする
内容クラスの役割分担や関係の作り方を整理する
効果コードの見通しや再利用性を高める
DAOパターンデータベースアクセスをDAOに任せる設計

DAOパターンを使うと、データベースアクセスという役割をDAOに分けられます。

そのため、サーブレットや業務ロジックのクラスが、JDBCの細かい処理を直接持たなくて済みます。

WebアプリケーションとDAOパターン

ServletとJSPで作るWebアプリケーションでも、DAOパターンはよく使われます。

DAOは、サーブレット、JSP、一般的なJavaクラスのどこからでも呼び出せます。

ただし、MVCモデルで考える場合、DAOは通常、モデル側のクラスから利用します。

サーブレットはコントローラとしてリクエストを受け取り、モデルに処理を依頼します。

モデルは必要に応じてDAOを呼び出し、データベースからデータを取得します。

取得した結果をサーブレットがスコープに保存し、JSPが表示します。

MVCモデルとDAOの役割

役割担当内容
コントローラサーブレットリクエストを受け取り、処理の流れを制御する
モデルLogicクラスなど業務処理を行い、必要に応じてDAOを呼び出す
DAOEmployeesDAOなどデータベースアクセスを担当する
ビューJSPスコープに保存された結果を表示する

JSPから直接DAOを呼び出すことも技術的には可能です。

しかし、JSPは画面表示を担当するビューです。

そのため、JSPにデータベースアクセスの処理を書かないほうが、役割分担がきれいになります。

MVCモデルでDAOを使う流れ

WebアプリケーションでDAOを使う場合、次のような流れになります。

DAOを使った画面表示の流れ

順番処理担当
1ブラウザから検索や一覧表示のリクエストを送るブラウザ
2サーブレットがリクエストを受け取るコントローラ
3サーブレットがモデルの処理を呼び出すコントローラ
4モデルがDAOを呼び出すモデル
5DAOがデータベースへアクセスするDAO
6DAOが取得結果をモデルへ返すDAO
7モデルが結果をサーブレットへ返すモデル
8サーブレットが結果をスコープに保存するコントローラ
9JSPへフォワードするコントローラ
10JSPが結果を表示するビュー

この流れにすると、サーブレットやJSPが直接データベース処理を持たなくなります。

データベースアクセスはDAOに任せ、業務処理はモデルに任せ、画面表示はJSPに任せることができます。

図4:MVCモデルとDAOパターンの関係

この図から分かること

MVCモデルでは、サーブレットがコントローラ、モデルが業務処理、JSPがビューを担当します。

DAOは、モデルから呼び出され、データベースアクセスを担当します。

ブラウザからリクエストが送られると、サーブレットが受け取り、モデルに処理を依頼します。

モデルはEmployeesDAOを呼び出し、EMPLOYEESテーブルからデータを取得します。

取得した結果は、最終的にJSPで表示されます。

このように役割を分けることで、サーブレットやJSPにデータベース処理を書かずに済みます。

サーブレットやJSPから直接データベースを利用しない理由

サーブレットやJSPからDAOを直接呼び出すことは可能です。

しかし、役割分担を考えると、なるべく避けたほうがよいです。

特にJSPは、画面表示を担当するファイルです。

JSPの中にデータベース接続やSQL実行の処理が入ると、画面表示とデータ取得の責任が混ざってしまいます。

直接データベースを利用しないほうがよい理由

場所理由
JSP画面表示に集中させたい。DB処理を書くと読みづらくなる
サーブレットリクエスト制御に集中させたい。業務処理やDB処理を詰め込みすぎない
モデル業務処理を担当し、必要に応じてDAOを呼び出すのに向いている
DAOデータベースアクセスを担当する場所として向いている

Webアプリケーションでは、それぞれのクラスやファイルに役割を持たせることが大切です。

サーブレットは交通整理、モデルは業務処理、DAOはデータベースアクセス、JSPは画面表示というように分けると、後から修正しやすくなります。

DAOで押さえたいこと

DAOパターンは、JDBC特有のコードをDAOクラスにまとめる設計方法です。

EMPLOYEESテーブルを扱う場合は、EmployeesDAOのようなDAOクラスを作成します。

DAOは、データベース接続、SQL文の実行、ResultSetの処理、例外処理などを担当します。

データベースを利用する側のクラスは、DAOのメソッドを呼び出すだけでデータを取得できます。

今回の例では、EmployeesDAOのfindAllメソッドを呼び出すことで、EMPLOYEESテーブルの全レコードをListとして取得できました。

DAOを使うと、SelectEmployees.javaからConnection、PreparedStatement、ResultSet、SQLExceptionなどのJDBC特有のコードを取り除けます。

その結果、プログラム本来の目的である従業員一覧の表示が分かりやすくなります。

Webアプリケーションでは、サーブレットやJSPから直接データベースを利用するのではなく、モデルのクラスからDAOを利用する構成にすると、MVCモデルの役割分担がきれいになります。

データベースアクセスはDAOに任せる。この考え方を身に付けると、ServletとJSPを使ったWebアプリケーションを、より保守しやすい構成で作れるようになります。