JavaのDAOパターンとは?ServletとDB操作を分離して保守性を高める方法
新人
「先輩、Servletの中にJDBCの接続処理やSQL文を全部書いているのですが、コードがどんどん長くなって読みづらくなってきました。これって普通なんですか?」
先輩
「それは『密結合』という状態だね。今は動いていても、後から修正するのがすごく大変になるよ。Javaの世界では、データベース操作を専門に担当する『DAO』という仕組みを使うのが一般的なんだ。」
新人
「ダオ……ですか?名前は聞いたことがありますが、具体的にどうやって分けるのか、なぜ分ける必要があるのか詳しく知りたいです!」
先輩
「よし、じゃあDAOパターンの基本と、それを使うメリットについて順番に解説していくよ!」
1. ServletにDB操作を直接書くとどうなる?(密結合の問題点)
Java Webアプリケーションを学び始めたばかりの頃は、Servlet(サーブレット)の中に直接、データベース(DB)への接続処理やSQLの実行コードを書いてしまいがちです。 しかし、このやり方には「密結合(みつけつごう)」という大きな罠が潜んでいます。
2つのプログラム要素が強く依存し合っている状態のことです。一箇所を直すと、関係ないはずの別の場所まで壊れてしまうリスクが高まります。
例えば、Servletの中に直接SQLを書いてしまうと、以下のような問題が発生します。
- コードの重複: 似たようなDB操作をするServletが複数ある場合、同じような接続コードを何度も書くことになります。
- 変更に弱い: データベースのテーブル名やカラム名が変わっただけで、全てのServletを修正してコンパイルし直さなければなりません。
- 可読性の低下: 画面の表示制御(Servletの本来の仕事)とデータの取得(DBの仕事)が混ざり合い、ソースコードがスパゲッティのように複雑になります。
まずは、あえて「良くない例(密結合なコード)」を見てみましょう。ServletがDBの面倒まで全て見ている状態です。
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/bad-example")
public class BadServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Servletの中で直接DB接続とSQL実行を行っている(密結合)
String url = "jdbc:mysql://localhost:3306/sample_db";
String user = "root";
String password = "password";
try (Connection con = DriverManager.getConnection(url, user, password)) {
String sql = "SELECT name FROM users WHERE id = ?";
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.setInt(1, 1);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
request.setAttribute("userName", rs.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
}
// 表示処理へ
request.getRequestDispatcher("/show.jsp").forward(request, response);
}
}
このコードは一見正しく動きますが、もし「接続先のデータベースがMySQLからPostgreSQLに変わった」としたらどうでしょう? 全てのServletファイルを一つずつ開いて、接続設定を書き換えていかなければなりません。これはプロフェッショナルの仕事としては非常に効率が悪いです。
2. DAOパターンとは?データアクセスを分離する目的と役割
そこで登場するのがDAO(Data Access Object:ダオ)パターンです。 DAOとは、その名の通り「データへのアクセスを担当するオブジェクト(部品)」のことです。
DAOパターンの役割分担
Servlet: ユーザーからのリクエストを受け取り、どの画面を表示するかを判断する(司令塔)。
DAO: データベースに接続し、データの追加、更新、削除、取得を行う(職人)。
DTO (Data Transfer Object): データを運ぶための専用ボックス。DBから取得した情報を詰め込む。
DAOパターンを導入すると、Servletは「DBの接続先がどこか」「SQLはどう書くか」といった細かいことを知る必要がなくなります。 ただDAOに対して「ユーザー情報を取ってきて!」と頼むだけで済むようになるのです。
では、DAOを使った「良い例」の構成を見てみましょう。まずはデータを格納する「User」クラス(DTO)と、DB操作を行う「UserDao」クラスを準備します。
// データを運ぶためのクラス(DTO)
public class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
public String getName() { return name; }
}
// DB操作を専門に行うクラス(DAO)
import java.sql.*;
public class UserDao {
private String url = "jdbc:mysql://localhost:3306/sample_db";
private String user = "root";
private String pass = "password";
public User findById(int id) {
String sql = "SELECT name FROM users WHERE id = ?";
try (Connection con = DriverManager.getConnection(url, user, pass);
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setInt(1, id);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return new User(id, rs.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
そして、Servlet側はこのDAOを呼び出すだけになります。
@WebServlet("/good-example")
public class GoodServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// DAOを生成してメソッドを呼ぶだけ!
UserDao dao = new UserDao();
User user = dao.findById(1);
if (user != null) {
request.setAttribute("userName", user.getName());
}
request.getRequestDispatcher("/show.jsp").forward(request, response);
}
}
いかがでしょうか。Servletから「SQL文」や「DB接続情報」が消えて、非常にスッキリしました。 これが、データアクセスを分離するということの本質です。
3. なぜDAOが必要なのか?保守性とテスト効率が劇的に変わる理由
DAOパターンを採用する最大の理由は、一言で言えば「プログラムを長持ちさせるため」です。 具体的には「保守性」と「テスト効率」という2つの大きなメリットがあります。
1. 保守性(メンテナンスのしやすさ)の向上
開発が進むと、当初使っていたデータベースを変更したり、テーブルの構造を最適化したりすることがよくあります。 もしDAOを使っていれば、変更箇所は「DAOクラスの中身」だけに限定されます。
Servlet側には一切影響が出ないため、大規模なシステムであっても修正漏れによるバグを防ぐことができます。 「一箇所を直せば全てに適用される」という状態は、開発者にとって非常に安心感があります。
2. テスト効率の劇的な改善
本格的な開発では、作成したプログラムが正しく動くかテストコードを書きます。 ServletにDB操作が混ざっていると、テストを行うたびに本物のデータベースに接続しなければならず、準備が非常に大変です。
DAOを分離しておけば、「DAOだけを単体でテストする」ことが可能になります。 また、テスト用のダミーデータを返す「偽物のDAO」に差し替えることで、データベースが完成していない状態でもServlet側の開発を進めることができるのです。
3. チーム開発のしやすさ
「画面担当(Servlet/JSP)」と「データベース担当(DAO)」で作業を分担しやすくなります。 初心者のうちは一人で全て作ることが多いですが、現場に出ると役割分担は必須です。 DAOという共通のルールがあることで、お互いのコードを邪魔することなくスムーズに連携できるようになります。
まとめのヒント:
DAOは単なる「クラスの分割」ではありません。
「どこに何が書いてあるか」を明確にする地図のようなものです。
初心者こそ、早い段階でこのパターンに慣れておくことで、Javaエンジニアとしてのスキルが飛躍的に向上します。
4. ServletとDAOの役割分担:ビジネスロジックとDB操作の境界線
Java Webアプリケーションを開発する上で、Servlet(サーブレット)とDAO(ダオ)の境界線をどこに引くべきか、迷う初心者の方は非常に多いです。 結論から言えば、Servletは「交通整理の司令塔」、DAOは「倉庫の管理者」という明確な役割分担が必要です。
Servletの仕事は「制御」に徹すること
Servletの本来の責任範囲は、Webブラウザからのリクエストを受け取り、適切なレスポンスを返すまでの「流れ」を管理することです。 具体的には以下の作業のみを担当させます。
- 画面の入力フォームから送信されたパラメータの受け取り。
- DAOなどの部品を呼び出し、必要な処理を依頼する。
- 処理結果をリクエストスコープやセッションスコープに保存する。
- JSPなどの次の画面へ遷移(フォワード)する。
DAOの仕事は「データ操作」に徹すること
一方で、DAOはデータベースという外部のリソースを専門に扱う場所です。 ここには「SQL文」や「JDBCのAPI」を閉じ込めます。DAOが知っているべき情報は、テーブルの構造や接続設定だけであり、 「このデータが画面のどこに表示されるか」といったWeb特有の知識は一切持たせません。
なぜ境界線を守る必要があるのか?
もしServletの中にビジネスロジックやDB操作が混ざってしまうと、プログラムの再利用が不可能になります。 例えば、Web画面からだけでなく、スマートフォンのアプリからも同じデータを使いたいとなった場合、 DAOとして分離されていればDAOを使い回すだけで済みますが、Servletに書いてあると、また同じコードを書き直す必要が出てくるからです。
5. 実装例で見るDAOパターンの基本構造(InterfaceとImplの活用)
本格的な開発現場では、DAOを「インターフェース」と「実装クラス」に分けて作成することが強く推奨されます。 これによって、特定のデータベース製品(MySQLやOracleなど)に依存しない、より柔軟な設計が可能になります。
ステップ1:インターフェースの定義
まずは、どのような操作ができるのかという「契約」をインターフェースで定義します。 これにより、呼び出し側(Servlet)は具体的な中身を知らなくても、どのようなメソッドがあるかを知ることができます。
package dao;
import java.util.List;
import model.User;
/**
* ユーザー情報のデータアクセスを行うインターフェース
*/
public interface UserInterface {
// IDを元にユーザーを1件取得する
User findById(int id);
// 全てのユーザー情報を取得する
List<User> findAll();
// ユーザー情報を新規登録する
boolean insert(User user);
}
ステップ2:実装クラス(Impl)の作成
次に、インターフェースを実装(implements)した具体的なクラスを作成します。 クラス名は「UserDaoImpl」のように、最後にImpl(Implementation:実装)を付けるのが一般的です。
package dao;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import model.User;
public class UserDaoImpl implements UserInterface {
private String url = "jdbc:mysql://localhost:3306/sample_db";
private String user = "root";
private String pass = "password";
@Override
public User findById(int id) {
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection con = DriverManager.getConnection(url, user, pass);
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setInt(1, id);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return new User(rs.getInt("id"), rs.getString("name"), rs.getString("email"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
@Override
public List<User> findAll() {
List<User> list = new ArrayList<>();
String sql = "SELECT * FROM users";
try (Connection con = DriverManager.getConnection(url, user, pass);
PreparedStatement pstmt = con.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
list.add(new User(rs.getInt("id"), rs.getString("name"), rs.getString("email")));
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
@Override
public boolean insert(User userObj) {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (Connection con = DriverManager.getConnection(url, user, pass);
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setString(1, userObj.getName());
pstmt.setString(2, userObj.getEmail());
int result = pstmt.executeUpdate();
return result > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
}
このようにインターフェースを使うことで、テストの際には「本物のDBに繋がないテスト用DAO」を簡単に差し替えることができるようになります。 これは、Googleなどの検索エンジンが高く評価する「拡張性の高い設計」の第一歩でもあります。
6. データの受け渡しに欠かせないDTO(Entity)との連携の仕組み
DAOがデータベースから取得したデータは、バラバラの状態で保持されているわけではありません。 そこで必要になるのがDTO(Data Transfer Object:データ転送オブジェクト)です。 文脈によっては「Entity(エンティティ)」や「JavaBeans」と呼ばれることもあります。
DTOの役割:データの器
DTOは、データベースの1行(レコード)を一つのJavaオブジェクトとして表現するためのものです。 DAOは、ResultSetから取り出した値をDTOという「箱」に詰め込み、それをServletに渡します。 DTOを使うことで、複数の項目(名前、メールアドレス、住所など)をまとめて一つの変数として持ち運べるようになります。
package model;
import java.io.Serializable;
/**
* ユーザー情報を保持するDTOクラス(JavaBeans)
*/
public class User implements Serializable {
private int id;
private String name;
private String email;
// 引数なしコンストラクタ(JavaBeansのルール)
public User() {}
// 全てのフィールドを初期化するコンストラクタ
public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// GetterとSetter
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
全体の連携フロー
ここまでの流れを整理すると、実際のアプリケーションでは以下のようなサイクルでデータが動いています。
- Servlet: ユーザーから「ID=1のユーザーが見たい」と要求を受ける。
- Servlet:
UserDao(インターフェース型で宣言)のfindById(1)を呼ぶ。 - DAO: SQLを実行し、DBから1レコード分のデータを取得する。
- DAO: 取得したデータを
User(DTO)のインスタンスにセットして返す。 - Servlet: 受け取った
Userオブジェクトをリクエストスコープに入れ、JSPへ送る。 - JSP:
Userオブジェクトからデータを取り出して画面に表示する。
この一連の流れがスムーズに行えるようになると、Javaエンジニアとしてのレベルが一段階上がったと言えるでしょう。 コードの重複が減り、どこに不具合があるのかも特定しやすくなります。
応用編:例外処理とコネクション管理のポイント
DAOパターンを実務レベルで活用するためには、エラーが発生した際の「例外処理」と、データベース接続の「リソース解放」についても理解を深めておく必要があります。 JDBCを使用する場合、データベースへの接続は有限なリソースであるため、使い終わったら必ず閉じる(クローズする)のが鉄則です。
try-with-resources文の活用
Java 7以降では、try-with-resources文を使うことで、明示的にclose()を呼び出さなくても、
ブロックを抜けた際に自動的に接続が閉じられる仕組みが導入されました。
これにより、クローズ漏れによるメモリリークやデータベースの接続数上限エラーを防ぐことができます。
独自例外の定義
DAO内で発生したSQLExceptionをそのままServletまで投げると、Servletがデータベースの内部的なエラー内容に依存してしまいます。
現場では、DAO独自の例外クラス(例:DataAccessException)を作成し、SQLExceptionをラップして投げ直す手法が取られることもあります。
これにより、呼び出し側は「データアクセスに失敗した」という事実だけを知ればよくなり、より抽象度の高いプログラムになります。
ステップアップのためのヒント
現在は「MyBatis」や「Spring Data JPA」といったフレームワークを使う現場も多いですが、それらの根本にある考え方はすべてこのDAOパターンに基づいています。 基本をしっかり押さえておくことで、新しい技術に出会った際も「これはDAOの役割を自動化しているんだな」と、すぐに本質を理解できるようになります。
7. DAO導入で得られるメリット:複数画面からの共通利用と再利用性
DAOパターンを導入する最大の技術的な利点は、「一度書いたデータ操作処理を、どこからでも使い回せる」という再利用性の高さにあります。 初心者のうちは、一つのServletから一つのテーブルを操作するだけの単純な構造を想像しがちですが、実際のシステム開発では同じデータを異なる画面や目的で利用するシーンが数多く存在します。
複数のServletからの共通利用
例えば、ユーザー情報を表示する「詳細画面」と、管理者だけがアクセスできる「ユーザー一覧管理画面」の二つがあるとします。 もしServletに直接SQLを書いていた場合、どちらのServletにも同じようなSELECT文を記述しなければなりません。 しかし、DAOとして「findById」や「findAll」といったメソッドが定義されていれば、それぞれのServletはDAOのインスタンスを生成してメソッドを呼び出すだけで済みます。
これにより、もしデータベースのテーブル定義が変更され、カラム名が「user_name」から「full_name」に変わったとしても、修正するのはDAOクラスの中にあるSQL文一箇所だけで完結します。 全てのServletを点検して回る必要がないため、修正漏れによるシステムダウンのリスクを最小限に抑えることが可能です。
Web以外のインターフェースへの対応
システムの規模が大きくなると、ブラウザからのリクエスト処理(Servlet)だけでなく、決まった時間に自動実行される「バッチ処理」や、スマートフォンのアプリ向けにデータを提供する「API(JSON出力)」などが必要になることがあります。 DAOを独立した部品として設計しておけば、Web画面用のプログラム(Servlet)からでも、バッチ処理用のプログラム(Mainメソッドを持つJavaクラス)からでも、全く同じDAOを呼び出してデータ操作が行えます。
このように、データの取得・保存という「手段」をDAOに閉じ込めることで、アプリケーション全体で一貫したデータ操作が可能になり、開発効率が飛躍的に向上します。 「どこに書いたか分からない」という迷いもなくなり、チーム全体でコードの共有がスムーズになるのです。
プロの視点:DRY原則の徹底
プログラミングにはDRY(Don't Repeat Yourself:同じことを繰り返さない)という重要な原則があります。 DAOの導入は、まさにこの原則をデータベース操作において実現するための手法です。 重複したコードは不具合の温床となります。DAOを使って、常に「正しいコードは一箇所にある」状態を保ちましょう。
具体的な例として、複数の目的で利用される「商品管理DAO」を想定したコードを見てみましょう。 一つのDAOが、在庫チェックや詳細表示など、異なる役割を持つServletから呼び出されるイメージです。
package dao;
import java.sql.*;
import model.Product;
/**
* 商品情報のデータアクセスを担当するDAO
* 複数のServletやバッチ処理から共通利用される想定
*/
public class ProductDao {
private String url = "jdbc:mysql://localhost:3306/shop_db";
private String user = "root";
private String pass = "password";
/**
* 在庫数を取得するメソッド
* 在庫確認Servletや注文処理クラスから利用される
*/
public int getStockCount(int productId) {
String sql = "SELECT stock FROM products WHERE id = ?";
try (Connection con = DriverManager.getConnection(url, user, pass);
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setInt(1, productId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return rs.getInt("stock");
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
}
/**
* 在庫を更新するメソッド
* 注文完了Servletや、キャンセル処理、入荷バッチなどで共通利用
*/
public boolean updateStock(int productId, int amount) {
String sql = "UPDATE products SET stock = stock + ? WHERE id = ?";
try (Connection con = DriverManager.getConnection(url, user, pass);
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setInt(1, amount);
pstmt.setInt(2, productId);
return pstmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
}
8. Servlet開発におけるDAO実装の注意点とトランザクション管理
DAOを導入して設計が綺麗になった後、次に直面するのが「トランザクション管理」の問題です。 実務では、一つの業務処理(ユースケース)において、複数のテーブルを更新したり、一つのテーブルに対して複数回の更新処理を行ったりすることが多々あります。 例えば「銀行振込」であれば、自分の口座からマイナスし、相手の口座にプラスするという二つの処理がセットで成功しなければなりません。
トランザクションをどこで管理するか
DAOの中に「接続・コミット・切断」の全てを書いてしまうと、困ったことが起きます。 複数のDAOメソッドを連続して呼び出す際、一つ目のメソッドが終了した時点でコネクションが閉じられ、コミットされてしまうからです。 これでは、途中でエラーが起きたときに全ての処理を「なかったこと(ロールバック)」にすることができません。
そのため、本格的な設計では以下の二つのアプローチが取られます。
- コネクションの外出し: Servlet側(または後述するService層)でConnectionオブジェクトを生成し、それをDAOのメソッドに引数として渡す方法です。これにより、複数のDAOメソッドで同じコネクションを共有し、一括でコミットやロールバックを制御できます。
- サービス層の導入: ServletとDAOの間に、業務の手順(シナリオ)を記述する「Serviceクラス」を用意し、そこでトランザクションの開始と終了を管理します。
自動コミットモードの解除
JDBCはデフォルトで「オートコミット」が有効になっています。
トランザクションを制御する場合は、必ずconnection.setAutoCommit(false)を呼び出す必要があります。
全ての処理が無事に完了したらconnection.commit()を呼び、例外が発生した場合はcatchブロック内でconnection.rollback()を実行するのが基本的な流れです。
以下に、複数の更新処理を一つのトランザクションとして管理する、より高度な実装例を示します。 この例では、複数の処理を一つの単位としてまとめ、データの整合性を守る仕組みを構築しています。
package service;
import java.sql.*;
import dao.ProductDao;
/**
* 業務ロジックを管理するクラス
* トランザクションの開始と終了(コミット・ロールバック)に責任を持つ
*/
public class OrderService {
private String url = "jdbc:mysql://localhost:3306/shop_db";
private String user = "root";
private String pass = "password";
public boolean processOrder(int productId, int quantity) {
Connection con = null;
try {
// 1. コネクションの確立
con = DriverManager.getConnection(url, user, pass);
// 2. オートコミットを無効化(トランザクション開始)
con.setAutoCommit(false);
ProductDao dao = new ProductDao();
// 在庫チェック(本来はここでdaoにconを渡して操作する設計が望ましい)
int currentStock = dao.getStockCount(productId);
if (currentStock >= quantity) {
// 在庫を減らす更新
boolean success = dao.updateStock(productId, -quantity);
if (success) {
// 全ての処理が成功したので確定
con.commit();
return true;
}
}
// 条件を満たさない場合はロールバック
con.rollback();
} catch (SQLException e) {
// 例外発生時は必ずロールバックしてデータの矛盾を防ぐ
if (con != null) {
try {
con.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// リソースの解放
if (con != null) {
try {
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return false;
}
}
接続プールの検討
DAO内で毎回DriverManager.getConnection()を呼び出すのは、実は非常に重い処理です。
アクセスが多いWebアプリケーションでは、あらかじめ数本の接続を保持しておき、使い回す「コネクションプール(Connection Pool)」という仕組みを利用します。
Tomcatなどのサーブレットコンテナにはこの機能が備わっており、JNDI(Java Naming and Directory Interface)経由で接続を取得するのが一般的です。
「DAOパターンを使っているから安心」ではなく、その裏側でどのようにデータベースとの接続が管理され、データの整合性が保たれているのかを意識することが、一人前のエンジニアへの道筋となります。
9. ServletとDAOを分離してクリーンな設計を目指そう
Java Web開発におけるDAOパターンの重要性について、深く掘り下げてきました。 最初は「クラスが増えて面倒くさい」と感じるかもしれませんが、そのわずかな手間が、将来のあなたやチームのメンバーを救うことになります。
学んだポイント:再利用性
DAOにデータアクセスを切り出すことで、異なるServletやバッチ、APIから同じロジックを共通利用できるようになります。 コードの重複を排除し、DRY原則を徹底することが可能です。
学んだポイント:安全な管理
トランザクション管理を適切に行うことで、システムエラー時にもデータの整合性を守れます。 try-with-resourcesやコネクションの扱いなど、実務で必須の作法も身につけましょう。
システムは一度作って終わりではありません。機能が追加され、環境が変わり、長い年月をかけて成長していきます。 その成長に耐えうる「柔軟で強い設計」の第一歩が、このDAOパターンです。 今日から書くコードに、ぜひこの考え方を取り入れてみてください。 一つ一つの役割を明確に分ける「クリーンな設計」ができるようになれば、Javaエンジニアとしての価値は確実に高まります。