前回のデータベースを使ったユーザ管理:ログイン処理(書き下し版)のプログラム構造を改良し、保守性、拡張性を向上させましょう。
まず、前回のプログラム構造を振り返ってみましょう。
ログイン処理(書き下し版)は四つのユースケース:ユーザ情報の一覧、ログインチェック、ユーザの追加、ユーザの削除をJSPとServletが個々に担当し、それぞれがデーターベースにアクセスして機能を実現していました。従って、
<ログイン処理(書き下し版)のプログラム構造的な問題>
- JSPは全体のUIを担当しているユーザ情報の一覧機能だけであるため、他のユースケースを担当するServletは処理の結果をユーザ情報の一覧機能に返して表示するしかなく、独自に自由な画面設計が出来ません。
- Javaプロググラム処理(ビジネスロジック・レイヤ)とデータベース処理(データベース・レイヤ)の切り分けが行われておらず、レイヤ間が密結合の状態で、データーベースの種類が変わったり、データの保存先がデータベースではなくファイルシステムに変わるような場合、全てのプログラムに変更が必要になり修正に手間がかかるだけでなく、修正時のミス防止やテストケースの増加などが予想されます。
1.はUIはJSP、コントロールはServletと役割を明確にしてプログラムを構成すれば改善できますが、2.の改善にはビジネスロジック・レイヤとデータベース・レイヤのレイヤごとの役割を明確化し、レイヤ間を疎結合にする必要があります。
そもそもオブジェクト指向プログラミング(Javaで作るビジネスロジック)とリレーショナルデータベース(データの永続化機構)との間には概念や構造の違いから発生するギャップ:インピーダンスミスマッチがあるといわれています。リレーショナルデータベースにアクセスするためのJDBCはJavaから直接呼び出すことが出来るのに「ギャップがある」というのは分かりにくいかもしれません。
これは例えばショッピングカートを考えた場合、ビジネスロジック側はショッピングカートをユーザごとにモデル化したオブジェクトとして扱うでしょうが、データベース側は正規化した複数のテーブルとテーブル間のリレーションで扱うため、両者の異なるデータ形式の変換が必ず必要になるという事です。
そこで、インピーダンスミスマッチの対応策の一つとしてJavaの開発元であったサン・マイクロシステムズがベスト・プラクティス・ガイドラインで提案したのがDAO:Data Access Objectです。
これはデータの永続化機構(例えばデータベース)とビジネスロジック間にDAOを位置付け、データベースに対するアクセスインターフェースを用意して、全てのデータアクセスをこのインターフェースを通して行わせ、データベースから取得した結果はDTO:Data Transfer Objectに格納する事により、永続化機構をビジネスロジックから隠蔽します。
この構成ではUIをJSP、コントロールをServletが担当するように事前に決めたので、いろいろな条件による画面の切り替えが容易になっていますし、Daoによりコントロールからデータベースアクセスが切り離されたのでコントロールの中が見やすくなっています。
画像をクリックすると拡大画像が表示されます。
特定のソースコードのみ確認したい場合は以下のボタンをクリックしてください。
登録済みユーザ一覧処理
ユーザログイン処理
ユーザ登録処理
ユーザ削除処理
DAOとDTO
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ユーザ管理UI</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>登録済みユーザ一覧</h1> <form action="./UserListServlet" method="post"> <button type="submit"><pre><big>登録済みユーザ一覧</big></pre></button> </form> <hr> <h1>ユーザログイン</h1> <form action="./user_login.jsp" method="post"> <button type="submit"><pre><big>ログイン</big></pre></button> </form> <hr> <h1>ユーザ登録</h1> <form action="./user_regist.jsp" method="post"> <button type="submit"><pre><big>ユーザ登録</big></pre></button> </form> <hr> <h1>ユーザ削除</h1> <form action="./user_delete.jsp" method="post"> <button type="submit"><pre><big>ユーザ削除</big></pre></button> </form> <hr> </body> </html>
各機能ごとに専用のUIを読んでいるので、構造はシンプルです。
登録済みユーザ一覧については専用のUIによる入力が不要なので、直接コントロールのサーブレット:UserListServletを呼んでいます。
package com.example; import java.io.IOException; import java.util.ArrayList; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class UserListServlet */ @WebServlet("/UserListServlet") public class UserListServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public UserListServlet() { super(); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { UserDao dao = new UserDao(); ArrayList<UserDto> list = dao.getUserList(); request.setAttribute("list", list); request.getRequestDispatcher("user_list.jsp").forward(request, response); } }
実際の処理はマーキングした部分でDAOのgetUserListメソッドで得たユーザ一覧情報をrequestオブジェクトの属性に乗せて、結果を表示するUI:user_list.jspにフォーワードしているだけで、処理は簡潔です。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="java.util.ArrayList" %> <%@ page import="com.example.UserDto" %> <% ArrayList<UserDto> list = (ArrayList<UserDto>)request.getAttribute("list"); %> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>登録済みユーザ一覧</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>登録済みユーザ一覧</h1> <table> <tr><th>登録番号</th><th>ユーザID</th><th>パスワード</th></tr> <% for (UserDto ud: list) { %> <tr> <td><%= ud.getNo() %></td> <td><%= ud.getUserid() %></td> <td><%= ud.getPassword() %></td> </tr> <% } %> </table> <br> <hr> <form action="./login_main.jsp" method="post"> <button type="submit"><pre><big>戻る</big></pre></button> </form> </body> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ログインフォーム</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>ユーザログイン</h1> <form action="./UserLoginServlet" method="post"> <table> <tr> <th>ID</th> <th>パスワード</th> <th></th> </tr> <tr> <td><input type="text" name="id" /></td> <td><input type="password" name="password" /></td> <td><input type="submit" value="実行"></td> </tr> </table> </form> <% String error = (String)request.getAttribute("error"); if (error != null) { %> <p style="color:red; font-size: larger;"><%= error %></p> <% } %> <br> <hr> <form action="./login_main.jsp" method="post"> <button type="submit"><pre><big>戻る</big></pre></button> </form> </body> </html>
ユーザIDとパスワードを受取ったら、ログイン処理のコントロールを行っているサーブレット:UserLoginServletを呼びます。
一方、UserLoginServletで入力情報のエラーを発見するとエラーメッセージをrequestオブジェクトの属性に乗せてuser_login.jspを呼ぶので、本JSPの後半ではエラーメッセージがあれば表示しています。
また、戻るボタンで初期画面に戻るようになっています。
package com.example; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Servlet implementation class LoginServlet */ @WebServlet("/UserLoginServlet") public class UserLoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public UserLoginServlet() { super(); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String id = request.getParameter("id"); String password = request.getParameter("password"); UserDao dao = new UserDao(); UserDto user = dao.findUser(id); boolean isLogin = (user != null && id.equals(user.getUserid()) && password.equals(user.getPassword())); HttpSession session = request.getSession(); session.setAttribute("isLogin", isLogin); if (isLogin) { request.setAttribute("username", user.getUserid()); request.getRequestDispatcher("/user_login_success.jsp").forward(request, response); } else { request.setAttribute("error", "IDかパスワードが間違っています。\n再入力してください。"); request.getRequestDispatcher("/user_login.jsp").forward(request, response); } } }
<%@ page language="HTML" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ユーザログイン成功</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>ユーザログイン成功</h1> <hr> <% String username = (String)request.getAttribute("username"); if (username != null) { %> <p style="color:blue; font-size: larger;">ようこそ<%= username %>さん!</p> <% } %> <br> <hr> <form action="./login_main.jsp" method="post"> <button type="submit"><pre><big>戻る</big></pre></button> </form> </body> </html>
まず登録のためのユーザ情報を受取るUI:user_regist.jspは、
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ログインフォーム</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>ユーザ登録</h1> <form action="./UserRegServlet" method="post"> <table> <tr> <th>ID</th> <th>パスワード</th> <th></th> </tr> <tr> <td><input type="text" name="id" /></td> <td><input type="password" name="password" /></td> <td><input type="submit" value="登録"></td> </tr> </table> </form> <% String error = (String)request.getAttribute("error"); if (error != null) { %> <p style="color:red; font-size: larger;"><%= error %></p> <% } %> <br> <hr> <form action="./login_main.jsp" method="post"> <button type="submit"><pre><big>戻る</big></pre></button> </form> </body> </html>
ユーザIDとパスワードを受取ったら、ユーザ登録処理のコントロールを行っているサーブレット:UserRegServletを呼びます。
一方、UserRegServletで入力情報のエラーを発見するとエラーメッセージをrequestオブジェクトの属性に乗せて本jspにフォーワードするので、後半でエラーメッセージがあればそれを表示しています。
また、戻るボタンで初期画面に戻るようになっています。
package com.example; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * Servlet implementation class UserRegServlet */ @WebServlet("/UserRegServlet") public class UserRegServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public UserRegServlet() { super(); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String id = request.getParameter("id"); String password = request.getParameter("password"); UserDao dao = new UserDao(); UserDto user = dao.findUser(id); boolean isLogin = (user != null && id.equals(user.getUserid()) && password.equals(user.getPassword())); HttpSession session = request.getSession(); session.setAttribute("isLogin", isLogin); if (!isLogin) { int result = dao.regUser(id, password); request.setAttribute("username", id); request.getRequestDispatcher("/user_reg_success.jsp").forward(request, response); } else { request.setAttribute("error", "同じIDとパスワードのユーザが既に登録されています。\n再入力してください。"); request.getRequestDispatcher("/user_regist.jsp").forward(request, response); } } }
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ユーザ登録成功</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>ユーザ登録成功</h1> <hr> <% String username = (String)request.getAttribute("username"); if (username != null) { %> <p style="color:blue; font-size: larger;"><%= username %>さんの登録が完了しました。</p> <% } %> <br> <hr> <form action="./login_main.jsp" method="post"> <button type="submit"><pre><big>戻る</big></pre></button> </form> </body> </html>
まず削除するユーザの情報を入力するUI:user_delete.jspです。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ログインフォーム</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>ユーザ削除</h1> <form action="./UserDelServlet" method="post"> <table> <tr> <th>ID</th> <th>パスワード</th> <th></th> </tr> <tr> <td><input type="text" name="id" /></td> <td><input type="password" name="password" /></td> <td><input type="submit" value="削除"></td> </tr> </table> </form> <% String error = (String)request.getAttribute("error"); if (error != null) { %> <p style="color:red; font-size: larger;"><%= error %></p> <% } %> <br> <hr> <form action="./login_main.jsp" method="post"> <button type="submit"><pre><big>戻る</big></pre></button> </form> </body> </html>
ユーザIDとパスワードを受取ったら、ユーザ削除処理のコントロールを行っているサーブレット:UserDelServletを呼びます。
一方、UserDelServletで入力情報のエラーを発見するとエラーメッセージをrequestオブジェクトの属性に乗せて本jspにフォーワードするので、後半でエラーメッセージがあればそれを表示しています。
また、戻るボタンで初期画面に戻るようになっています。
package com.example; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class UserDelServlet */ @WebServlet("/UserDelServlet") public class UserDelServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public UserDelServlet() { super(); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String id = request.getParameter("id"); String password = request.getParameter("password"); UserDao dao = new UserDao(); UserDto user = dao.findUser(id); boolean isLogin = (user != null && id.equals(user.getUserid()) && password.equals(user.getPassword())); if (isLogin) { int result = dao.delUser(id, password); request.setAttribute("username", id); request.getRequestDispatcher("/user_del_success.jsp").forward(request, response); } else { request.setAttribute("error", "該当するIDとパスワードのユーザは登録されていません。\n再入力してください。"); request.getRequestDispatcher("/user_delete.jsp").forward(request, response); } } }
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ユーザ削除成功</title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/login.css"> </head> <body> <h1>ユーザ削除成功</h1> <hr> <% String username = (String)request.getAttribute("username"); if (username != null) { %> <p style="color:blue; font-size: larger;"><%= username %>さんの登録を削除しました。</p> <% } %> <br> <hr> <form action="./login_main.jsp" method="post"> <button type="submit"><pre><big>戻る</big></pre></button> </form> </body> </html>
DAOはJDBCを使ってデータベースに対するConnection及びclose処理を行い、必要なデータベース操作のSQL文を発行し、結果をDTOに格納する各種のメソッドを提供しています。
package com.example; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; public class UserDao { private static Connection getConnection() { try { Class.forName("org.postgresql.Driver"); return DriverManager.getConnection("jdbc:postgresql:login","postgres", "root"); } catch (Exception e) { throw new IllegalArgumentException(e); } } private static void allClose(PreparedStatement statement, Connection connection) { if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } static Connection connection = null; static PreparedStatement statement = null; public UserDto findUser(String id) { UserDto user = new UserDto(); try { connection = getConnection(); statement = connection.prepareStatement("SELECT * FROM userinf WHERE userid = ?"); statement.setString(1, id); ResultSet resultSet = statement.executeQuery(); if (!resultSet.next()) { return null; } user.setUserid(resultSet.getString("userid")); user.setPassword(resultSet.getString("password")); } catch (SQLException e) { e.printStackTrace(); } finally { allClose(statement, connection); } return user; } public ArrayList<UserDto> getUserList() { ArrayList<UserDto> list = new ArrayList<UserDto>(); try { connection = getConnection(); statement = connection.prepareStatement("SELECT * FROM userinf"); ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { UserDto ud = new UserDto(); ud.setNo(resultSet.getInt(1)); ud.setUserid(resultSet.getString(2)); ud.setPassword(resultSet.getString(3)); list.add(ud); } } catch (SQLException e) { e.printStackTrace(); } finally { allClose(statement, connection); } return list; } public int regUser(String id, String password) { int result = 0; try { connection = getConnection(); statement = connection.prepareStatement("INSERT INTO userinf (userid, password) VALUES (?, ?)"); statement.setString(1, id); statement.setString(2, password); result = statement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { allClose(statement, connection); } return result; } public int delUser(String id, String password) { int result = 0; try { connection = getConnection(); statement = connection.prepareStatement("DELETE FROM userinf WHERE userid = ? AND password = ?"); statement.setString(1, id); statement.setString(2, password); result = statement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { allClose(statement, connection); } return result; } }
package com.example; public class UserDto { private int no; private String userid; private String password; public int getNo() { return no; } public String getUserid() { return userid; } public String getPassword() { return password; } public void setNo(int no) { this.no = no; } public void setUserid(String userid) { this.userid = userid; } public void setPassword(String password) { this.password = password; } }
以上のようにDAOを使うことでJSPやServletの数は増えますが、全体構造は簡潔になり保守性、拡張性が向上しログイン処理(書き下し版)とは異なる形になった事が分かるでしょう。
次のステップ:ログイン処理(DAOを使った汎用的で現実的な構造)
ではDAOを使ったより汎用的で、現実的な構造を考えます。