stuinfoの動作
stuinfoは、学生の情報を登録する機能と、登録した情報を表示する機能とを備えたWebアプリケーションです。
扱われているのは学生の情報のみなので、モデルクラスもStudentのみという、非常にシンプルな例になっています。
README.mdの手順に従ってアプリケーションサーバを起動し、機能を確認してみましょう。 README.mdの手順5、手順6では、ブラウザの画面に表示されたフォームに何かしらの情報を入力して送信すると、OK牧場!と表示されます。この間に何が起こっているのかを解説します。
自分のコンピュータ上でアプリケーションサーバを起動してから、ブラウザでにアクセスすると、HTML文書が表示されます。 このHTML文書にはフォーム要素が含まれています。
フォームに何かしらの情報を入力して送信ボタンをクリックすると、文書内で指定されたパスに文書内で指定された方法で入力内容が送信されます。
パスは<form>タグのaction属性で指定します。
今回は./RegistInfo、つまりhttp://localhost:8080/system-design-dev/RegistInfo
方法は<form>タグのmethod属性で指定します。
今回はpost、つまりHTTPのPOSTリクエスト
入力欄はinput要素によって実現します。<input>タグのname属性の値と、その要素に入力された入力値のペアが送信されます。
README.mdの通りにすると、アプリケーションサーバに次のようなHTTPリクエストが送信されます(一部を省略しています)。
参考にした資料
code:http
POST /system-design-dev/RegistInfo HTTP/1.1
Content-Type: application/x-www-form-urlencoded
stu_id%3Dl_chitanda%26stu_name%3D千反田える%26stu_birthplace%3D岐阜県高山市
最初の行はメソッド、パス、HTTPのバージョンを表しています。
2行目以降は「ヘッダー」から始まり、1行空けて「本文(ボディ)」という構成になっています。
サーブレット(HttpServletを継承したクラス)は、サーバがHTTPリクエストを受信した際の最初の処理を担当します。
サービスメソッド(doGetメソッドやdoPostメソッド)を実装することで、@WebServletアノテーションで指定したパスに対するGETリクエストやPOSTリクエストを受信した際の処理を規定することができます。
今回は/system-design-dev/RegistInfoに対するPOSTリクエストを受信したので、@WebServlet("/RegistInfo")というアノテーションが付いたHttpServlet(つまりRegistInfoクラス)のインスタンスが作成され、そのdoPostメソッドが呼び出されます。
code:java
// /src/main/java/servlet/RegistInfo.java
// *** 省略 ***
@WebServlet("/RegistInfo")
public class RegistInfo extends HttpServlet {
// *** 省略 ***
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// requestオブジェクトの文字エンコーディングの設定
request.setCharacterEncoding("UTF-8");
// requestオブジェクトから登録情報の取り出し
String stuId = request.getParameter("stu_id");
String stuName = request.getParameter("stu_name");
String stuBirthplace = request.getParameter("stu_birthplace");
// *** 省略 ***
// studentオブジェクトに情報を格納
Student student = new Student(stuId, stuName, stuBirthplace);
// StudentManagerオブジェクトの生成
StudentManager manager = new StudentManager();
// 登録
manager.registStudent(student);
// 成功画面を表示する
response.sendRedirect("/system-design-dev/RegistInfo");
}
}
doPostメソッドは次のような流れで処理を行っています。ほかのサーブレットの各メソッドも同様の流れで処理を行っています。
1. HTTPリクエストを読み込む。
2. 読み込んだ内容をビジネスロジックを担当するクラスのメソッドに渡して呼び出し、処理の結果を返させる。
3. 処理の結果を踏まえてレスポンスを返す。
具体的にみていきましょう。
1. HTTPリクエストを読み込む。
ブラウザから送られたHTTPリクエストに関する情報は、request(第1引数、HttpServletRequestのインスタンス)に含まれています。
本文がapplication/x-www-form-urlencodedにより符号化されている場合、本文は項目の名前と値のペアが複数含まれるものになります。項目ごとの値は、HttpServletRequest.getParameterに項目の名前(inputタグのname属性に設定するもの)を渡して呼び出した際の戻り値から得られます。
code:java
String stuId = request.getParameter("stu_id");
String stuName = request.getParameter("stu_name");
String stuBirthplace = request.getParameter("stu_birthplace");
2. 読み込んだ内容をビジネスロジックを担当するクラスのメソッドに渡して呼び出し、処理の結果を返させる。
実際に入力された情報を登録するための処理はStudentManager.registStudentメソッドに定義されています。registStudentメソッドはモデルオブジェクトStudentを引数に取るメソッドです。
入力された情報をStudentのインスタンスとしてまとめて、それをStudentManager.registStudentメソッドに渡して呼び出します。
code:java
Student student = new Student(stuId, stuName, stuBirthplace);
StudentManager manager = new StudentManager();
manager.registStudent(student);
3. 処理の結果を踏まえてレスポンスを返す。
レスポンスの返却はresponse(第2引数、HttpServletResponseのインスタンス)を通して行います。ここでは、/system-design-dev/RegistInfoにリダイレクトさせる、つまりhttp://localhost:8080/system-design-dev/RegistInfoにアクセスした時と同じページを見せるため、HttpServlet.sendRedirectメソッドを呼び出しています。
code:java
response.sendRedirect("/system-design-dev/RegistInfo");
レスポンスを受け取ったブラウザは、http://localhost:8080/system-design-dev/RegistInfoにGETリクエストを送ります。アプリケーションサーバがGETリクエストを受け取ると、doGetメソッドが呼び出され、上記と同じような流れで処理が行われます。
doGetメソッドでは最後にRequestDispatcher.forwardメソッドを呼び出しています。これはJSP(HTML文書を生成するサーブレットでJSPの記法で記述したもの)に処理を「回す」ために呼び出すメソッドです。結果として、JSPが生成したHTML文書を本文とするレスポンスを返します。
code:java
RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/registStudentSuccess.jsp");
dispatcher.forward(request, response);
Studentクラスはクラス図のクラスと同じ型と名前のフィールド及びメソッドと、各フィールドの値を読み書きするためのgetterメソッドとsetterメソッドからなっています。
getterメソッドやsetterメソッドをつくらない場合もあります。特にインスタンス化以降に値を変えられたくないフィールドに対するsetterはつくらないほうが賢明ですよね。
StudentはJavaBeansにおけるBeanとしてつくられています。そのため、引数なしでpublicなコンストラクタを持っていて、フィールドの値はsetterメソッドを通して割り当てることを念頭につくられています。 モデルオブジェクトは必ずしもBeanとしてつくる必要はありません(Beanとしてつくる必要がある場合は別です)。
StudentManager.registStudent
今回は、学生の情報を登録する機能を実装しているregistStudentメソッドが呼び出されています。
ビジネスロジックを担当するメソッドでは、次のような処理を行うことが多いです。
1. 入力された内容がビジネスロジックの制約を満たしているかどうかを判定する。
2. DAOを通してデータベースに永続化されたモデルオブジェクトを読み込んだり、書き込んだりする。
具体的にみていきましょう。
1. 入力された内容がビジネスロジックを実行するための条件を満たしているかどうかを判定する。
今回はこれに相当する内容はありません。
文字数制限を行っている場合や、存在しないオブジェクトに対する操作を拒否する場合など、ビジネスロジックを実行するための条件を満たしていることを判定する必要がある場合、ここで実装することになります。
2. DAOを通してデータベースに永続化されたモデルオブジェクトを読み込んだり、書き込んだりする。
registStudentメソッドは、Studentのインスタンスを第1引数として受け取り、StudentDAO.registStudentメソッドに渡して呼び出す処理を行っています。
なおStudentDAO.registStudentは、受け取ったStudentのインスタンスを実際にデータベース内のテーブルの新規レコードとして挿入する処理を行っています。
Connectionの扱い
StudentDAO.registStudentメソッドを呼び出す際には、第2引数としてConnectionを渡す必要があります。
ConnectionのインスタンスはStudentDAO.createConnectionメソッドの戻り値として得られます。戻り値を何らかの変数に割り当てて、StudentDAO.registStudentに渡すのがよいでしょう。
Connectionのインスタンスが不要になったら、Connection.closeメソッドを呼び出す(コネクションを閉じる)ようにしてください。StudentDAO.closeConnectionメソッドにConnectionのインスタンスを渡しても、同様のことを行えます。
コネクションを閉じたら、それが割り当てられていた変数にnullを割り当て、Connectionのインスタンスがメモリから消えるようにしておきましょう。
code:java
// *** 省略 ***
public class StudentManager {
private Connection connection = null;
public StudentManager() {
}
public void registStudent(Student student) {
StudentDAO studentDAO = new StudentDAO();
this.connection = studentDAO.createConnection();
studentDAO.registStudent(student, this.connection);
studentDAO.closeConnection(this.connection);
this.connection = null;
}
// *** 省略 ***
}
DAOにはモデルオブジェクトの読み書きを行うためのメソッドを実装することが一般的です。読み書きを細分化するとCRUD(作成、読み込み、更新、削除)の4つに分けられます(それ以外の分け方もあります)。StudentDAOは、モデルオブジェクトStudentに関するCRUDのうち、stuinfoの機能を実現するのに必要最低限のものを実装しています。 searchStudent: (読み込み・1件検索)Studentのインスタンスを引数として受け取り、そのインスタンスがもつstudentIDでstudent_infoテーブルを検索し、その検索結果から引数として受け取ったインスタンスのフィールドを穴埋めする。
registStudent: (作成)Studentのインスタンスを引数として受け取り、それを踏まえた新規レコードをstudent_infoテーブルに挿入する。
code:java
// *** 省略 ***
public class StudentDAO extends DriverAccessor {
// *** 省略 ***
public void registStudent(Student student, Connection connection) {
try {
// SQLコマンド
String sql = "insert into student_info(student_id, student_name, student_birthplace) values(?, ?, ?)";
// SQLコマンドの実行
PreparedStatement stmt = connection.prepareStatement(sql);
// SQLコマンドのクエッションマークに値を、1番目から代入する
stmt.setString(1, student.getStudentID());
stmt.setString(2, student.getStudentName());
stmt.setString(3, student.getStudentBirthplace());
stmt.executeUpdate();
} catch (SQLException e) {
// エラーが発生した場合、エラーの原因を出力する
e.printStackTrace();
} finally {
}
}
// *** 省略 ***
}
DAOの各メソッドの処理は次のように進めることが多いです。
1. PreparedStatementのインスタンスを作成し、実行するSQL文を準備する。 2. PreparedStatement.executeQuery(またはPreparedStatement.executeUpdate)メソッドを呼び出し、準備したSQL文を実行する。
実行するSQLがSELECT文など結果が返ってくるような文であればexecuteQueryメソッドを呼び出します。
そうでなければexecuteUpdateメソッドを呼び出します。
3. (2でexecuteQueryメソッドを呼び出した場合)その戻り値からモデルオブジェクトを作成して返す。
具体的にみていきましょう。
1. PreparedStatementのインスタンスを作成し、実行するSQL文を準備する。
PreparedStatementのインスタンスを用意する際は、Connection.prepareStatementメソッドにSQL文を渡して呼び出します。
SQL文にはstudent(第1引数・Studentのインスタンス)の各フィールドの値が入ることになります。例えば冒頭で入力した内容を新規レコードとして挿入するのであれば、次のようなSQL文になります。
insert into student_info(student_id, student_name, student_birthplace) values("l_chitanda", "千反田える", "岐阜県高山市")
studentの各フィールドの値が入る場所は?にしておいて、後でそれぞれの?に各フィールドの値を入れるようにします。
PreparedStatement.setStringメソッドに、第1引数: 何番目の?に、第2引数: 何を入れるか、を渡して呼び出します。
code:java
String sql = "insert into student_info(student_id, student_name, student_birthplace) values(?, ?, ?)";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, student.getStudentID());
stmt.setString(2, student.getStudentName());
stmt.setString(3, student.getStudentBirthplace());
2. PreparedStatement.executeUpdateメソッドを呼び出し、準備したSQL文を実行する。
code:java
stmt.executeUpdate();
補足: executeQueryメソッドの戻り値からモデルオブジェクトを作成する
StudentDAO.registStudentメソッドはレコードを作成するためのメソッドであり、結果を返すようなものではありませんでした。
StudentDAO.searchStudentメソッドのように、SELECT文を実行してその結果をモデルオブジェクトとして返す場合、PreparedStatement.executeQueryメソッドを呼び出し、その戻り値(ResultSetのインスタンス)からモデルオブジェクトを作成する必要があります。
リレーショナルデータベースに対する問い合わせ結果は表型データとして表現されます。これを踏まえてResultSetは読み込むレコードを決めるためのメソッドと、getterメソッド(そのレコードのフィールドから値を読み込むためのメソッド)を備えています。
読み込むレコードを決めるためのメソッド: first、last、next、previousなど
getterメソッド: getInt、getStringなど
ResultSetからのモデルオブジェクトの作成は、次の流れで進めることが多いです。
1. 結果が存在するか否かを確認する
結果が1つ以上存在するかどうかはResultSet.firstメソッドで確認できます。1つでも存在すればtrue、存在しなければfalseを返します。
2. レコードの各フィールドの値を読み込む
ResultSet.firstメソッドを呼び出したことで、結果の最初のレコードの各フィールドの値を読み込む準備ができました。これに続いて、getterメソッドにフィールド名を渡して呼び出すことで、最初のレコードの各フィールドの値を読み込みます。どのgetterメソッドを使うのかは、データベース側の各フィールドのデータ型に従って決めてください。
複数のレコードを読み込む場合は、ResultSet.nextメソッドを呼び出してからgetterメソッドを呼び出すことを繰り返します。ResultSet.nextメソッドは次のレコードがあればtrueを返し、なければ falseを返します。ResultSet.nextの呼び出しをwhile文の条件に使用することで、すべてのレコードについて各フィールドの値を読み込むことができます。
3. 読み込んだ値をモデルオブジェクトのフィールドに割り当てる。
モデルオブジェクトのsetterメソッド(フィールドに値を割り当てるためのメソッド)に読み込んだ値を割り当てます。
searchStudentメソッドのように、1つのレコードをモデルオブジェクトとして読み込む場合は、tryブロックの中を次のようにします。
code:java
String sql = "select * from student_info where student_id = ? ";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, student.getStudentID());
ResultSet rs = stmt.executeQuery();
if (rs.first()) { // DBに存在した場合
// rsからそれぞれの情報を取り出し、Studentオブジェクトに設定する
student.setStudentName(rs.getString("student_name"));
student.setStudentBirthplace(rs.getString("student_birthplace"));
} else { // DBに存在しなかった場合
student = null; // studentオブジェクトをnullにする
}
stmt.close();
rs.close();
return student;
またテーブルの全件を読み込むためのfindAllメソッドを完全に実装するならば、tryブロックの中は次のようになるでしょう。
code:java
String sql = "select * from student_info";
PreparedStatement stmt = connection.prepareStatement(sql);
ResultSet rs = stmt.executeQuery();
List<Student> students = new ArrayList<Student>();
while (rs.next()) {
String id = rs.getString("student_id");
String name = rs.getString("student_name");
String birthplace = rs.getString("student_birthplace");
Student student = new Student(id, name, birthplace);
students.add(student);
}
stmt.close();
rs.close();
return students;
Javaにはエラー処理のためにtry...catchを使用することができます。 戻り値を返す代わりにエラーを投げるかもしれないメソッドを呼び出す場合は、tryブロックの中で呼び出す必要があります。
tryブロックの後ろにはcatchブロックを書きます。catchブロック内には、tryブロック内で呼び出したメソッドがエラーを投げた場合に行う処理を書きます。
code:java
try {
Example example = new Example();
example.methodThrowsError(); // このメソッドはエラーや例外を投げる。
} catch (Exception e) {
e.printStackTrace(); // エラーが投げられた場合はこのブロック内の処理が実行される。
}
今回はConnection.prepareStatementメソッドがSQLExceptionを投げる可能性があるため、tryブロック内で呼び出しています。
参考にした資料
Java Servletに関する資料