ActiveObjectsを使ってみた(3) - 一覧に検索機能を追加する
ActiveObjectsを使ってみた(2) - PostgreSQLまでに作成した、「単品商品一覧画面」に、検索機能を追加しました。
まずは、画面イメージ
今回変更・追加したプログラム
SingleProductListServlet.java - 単品商品一覧画面(サーブレット) - 変更
POSTリクエスト用のメソッドを追加しました。このメソッドでは以下のような処理を行っています。
- 入力値を取得する
- 入力値からAND条件を作成する
- AND条件からクエリを設定する。
- クエリを使用して単品商品を検索する。
以下、ソースファイルです。
package sample.servlet; import java.sql.SQLException; import net.java.ao.EntityManager; import net.java.ao.Query; import sample.SingletonEntityManager; import sample.entity.SingleProduct; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; import java.io.IOException; import sample.aoutil.AndCondition; /** * 単品商品一覧画面。 */ public class SingleProductListServlet extends HttpServlet { /** * POSTリクエスト。 * 入力された条件で検索する。 */ public void doPost( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException { // クエリを作成する。 Query query = Query.select(); // 入力値を取得する。 SingleProductListInput input = new SingleProductListInput( req ); if ( input.isError() ) { // 入力値の解析でエラーが発生する: // エラーメッセージを表示する。 req.setAttribute( "Message", "入力条件に誤りがあります。" ); // 後のfind()時に無条件で検索を行うため、クエリに条件を設定しない。 // TODO 前回の検索条件をセッションから取得するよう変更する?? } else { ///// 入力値からAND条件を作成する。 ///// AndCondition andCondition = new AndCondition(); // 商品番号(FROM)が入力されていることを確認する。 if ( input.getProductNoFrom() != null ) { // 商品番号(FROM)をAND条件に追加する。 andCondition.add( "productNo >= ?", input.getProductNoFrom() ); } // 商品番号(TO)が入力されていることを確認する。 if ( input.getProductNoTo() != null ) { // 商品番号(TO)をAND条件に追加する。 andCondition.add( "productNo <= ?", input.getProductNoTo() ); } // 販売期間(FROM)が入力されていることを確認する。 if ( input.getSaleTermFrom() != null ) { // 販売期間(FROM)をAND条件に追加する。 andCondition.add( "saleTo >= ?", input.getSaleTermFrom() ); } // 販売期間(TO)が入力されていることを確認する。 if ( input.getSaleTermTo() != null ) { // 販売期間n(TO)をAND条件に追加する。 andCondition.add( "saleFrom <= ?", input.getSaleTermTo() ); } // 単価(FROM)が入力されていることを確認する。 if ( input.getPriceFrom() != null ) { // 単価(FROM)をAND条件に追加する。 andCondition.add( "price >= ?", input.getPriceFrom() ); } // 単価(TO)が入力されていることを確認する。 if ( input.getPriceTo() != null ) { // 単価(TO)をAND条件に追加する。 andCondition.add( "price <= ?", input.getPriceTo() ); } ////// 作成したAND条件を、クエリに設定する。 ////// // AND条件が空でないことを確認する。 if ( andCondition.getWhereClause().length() > 0 ) { query.where( andCondition.getWhereClause(), andCondition.getWhereParams() ); } } // 入力条件 req.setAttribute( "Input", input ); // デバッグ用!!!JSPにクエリを表示できるようにする。 req.setAttribute("Query", query ); try { EntityManager manager = SingletonEntityManager.getInstance(); ///// クエリを使用して、単品商品を検索する。 ///// SingleProduct[] products = manager.find( SingleProduct.class, query ); req.setAttribute( "SingleProducts", products ); } catch ( SQLException e ) { // 検索に失敗する: // エラーメッセージを表示する。 req.setAttribute( "Message", "検索に失敗しました。" ); } // JSPへ転送する。 req.getRequestDispatcher( "../jsp/SingleProductList.jsp" ).forward( req, resp ); } /** * GETリクエスト。 * 無条件で検索する。 */ public void doGet( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException { EntityManager manager = SingletonEntityManager.getInstance(); try { // 単品商品を検索する。 req.setAttribute( "SingleProducts", manager.find( SingleProduct.class ) ); } catch ( SQLException e ) { // 検索に失敗する: // エラーメッセージを表示する。 req.setAttribute( "Message", "検索に失敗しました。" ); } // JSPへ転送する。 req.getRequestDispatcher( "../jsp/SingleProductList.jsp" ).forward( req, resp ); } }
SingleProductListInput.java - 単品商品一覧の入力値 - 追加
サーブレットのリクエスト・パラメータからの入力値取得する処理はこのクラスにまとめました。未入力や文字列解析エラーの考慮などが複雑なためです。
また、このクラスはgetterを持っているため、読み取り用のBeanとしても使用できます。実際、JSPでの検索条件の再表示のため、Beanとして使用しています。
package sample.servlet; import javax.servlet.http.HttpServletRequest; import java.text.DateFormat; import java.text.ParseException; import java.util.Date; /** * 単身商品一覧画面の入力値。 * Queryの作成、および、JSPでの入力値の再表示のために使用する *。 * Beanみたいなものだが、コンストラクタ中でリクエスト・パラメータから入力値を取得する。 */ public class SingleProductListInput { // 入力値 private Long productNoFrom = null; private Long productNoTo = null; private Date saleTermFrom = null; private Date saleTermTo = null; private Integer priceFrom = null; private Integer priceTo = null; // エラーフラグ private boolean error = false; /** * コンストラクタ。 * リクエスト・パラメータから入力値を取得・変換し、private変数に設定する。 * * 未指定の入力値や変換に失敗した入力値はnullにする。 * * いずれかの入力値の変換に失敗した場合でも、他の値の変換を継続する。 * また、ひとつでも変換に失敗した場合、エラーフラグをtrueにする。 * そうでなければ、エラーフラグはfalseとする。 */ public SingleProductListInput( HttpServletRequest req ) { String temp; // 商品番号(FROM)が指定されていることを確認する。 temp = req.getParameter( "productNoFrom" ); if ( temp.length() > 0 ) { try { // 商品番号(FROM)をLongに変換する。 productNoFrom = Long.valueOf( temp ); } catch ( NumberFormatException e ) { // 変換できない: エラー error = true; } } // 商品番号(TO)が指定されていることを確認する。 temp = req.getParameter( "productNoTo" ); if ( temp.length() > 0 ) { try { // 商品番号(TO)をLongに変換する。 productNoTo = Long.valueOf( temp ); } catch ( NumberFormatException e ) { // 変換できない: エラー error = true; } } DateFormat dateFormat = DateFormat.getDateInstance(); // 販売期間(FROM)が指定されていることを確認する。 temp = req.getParameter( "saleTermFrom" ); if ( temp.length() > 0 ) { try { // 販売期間(FROM)を日付に変換する。 saleTermFrom = dateFormat.parse( temp ); } catch ( ParseException e ) { // 変換できない: エラー error = true; } } // 販売期間(TO)が指定されていることを確認する。 temp = req.getParameter( "saleTermTo" ); if ( temp.length() > 0 ) { try { // 販売期間(TO)を日付に変換する。 saleTermTo = dateFormat.parse( temp ); } catch ( ParseException e ) { // 変換できない: エラー error = true; } } // 単価(FROM)が指定されていることを確認する。 temp = req.getParameter( "priceFrom" ); if ( temp.length() > 0 ) { try { // 単価(FROM)をIntegerに変換する。 priceFrom = Integer.valueOf( temp ); } catch ( NumberFormatException e ) { // 変換できない: エラー error = true; } } // 単価(TO)が指定されていることを確認する。 temp = req.getParameter( "priceTo" ); if ( temp.length() > 0 ) { try { // 単価(TO)をIntegerに変換する。 priceTo = Integer.valueOf( temp ); } catch ( NumberFormatException e ) { // 変換できない: エラー error = true; } } } /** * 商品番号(FROM)を取得する。 */ public Long getProductNoFrom() { return productNoFrom; } /** * 商品番号(TO)を取得する。 */ public Long getProductNoTo() { return productNoTo; } /** * 販売期間(FROM)を取得する。 */ public Date getSaleTermFrom() { return saleTermFrom; } /** * 販売期間(TO)を取得する。 */ public Date getSaleTermTo() { return saleTermTo; } /** * 単価(FROM)を取得する。 */ public Integer getPriceTo() { return priceTo; } /** * 単価(TO)を取得する。 */ public Integer getPriceFrom() { return priceFrom; } /** * 変換エラーが発生したことを確認する。 */ public boolean isError() { // エラーフラグを返す。 return error; } }
AndCondition.java - AND条件 - 追加
EntityManager.find()の引数にQueryオブジェクトを渡すことで、検索条件を指定できます。Queryオブジェクトのwhere()メソッドに対して、以下のような条件を指定することができます。
- productNo >= ?
- productNo >= ? and product <= ?
- productNo >= ? and price >= ?
複雑な絞り込みもできるため便利なのですが、条件の文字列の作成自体は自前でやらなければなりません。そこで、AND条件を作成するためのユーティリティ・クラスを作成しました。このクラスで、Where句の結合と複数個のパラメータの管理を行います。
実際に、サーブレット中で以下のような形で使用しています。
// クエリを作成する。 Query query = Query.select(); // AND条件を作成する。 AndCondition andCondition = new AndCondition(); // AND条件を追加する。 andCondition.add( "productNo >= ?", input.getProductNoFrom() ); andCondition.add( "productNo <= ?", input.getProductNoTo() ); andCondition.add( "saleTo >= ?", input.getSaleTermFrom() ); andCondition.add( "saleFrom <= ?", input.getSaleTermTo() ); // クエリにAND条件を設定する。 query.where( andCondition.getWhereClause(), andCondition.getWhereParams() ); // クエリで検索をおこなう。 SingleProduct[] products = manager.find( SingleProduct.class, query );
以下、ソースファイルです。
package sample.aoutil; import java.util.ArrayList; import java.util.List; /** * AND条件 */ public class AndCondition { private Listclauses = new ArrayList (); // Where句 private List
SingleProductList.jsp - 単品商品一覧画面(JSP) - 変更
以下、ソースファイルです。
<%@ page contentType="text/html; charset=Shift_JIS" %> <%@ taglib uri="/tags/struts-bean" prefix="bean" %> <%@ taglib uri="/tags/struts-logic" prefix="logic" %> <html> <head> <title>単品商品一覧 - ActiveObjectsサンプル</title> </head> <body> <h3>単品商品一覧</h3> <%-- メッセージ欄 --%> <logic:notEmpty name="Message"> <table border=1> <tr><th>メッセージ</th></tr> <tr><td><bean:write name="Message"/></td></tr> </table> <br> </logic:notEmpty> <%-- 検索条件欄 --%> <form method="post" action="sample.servlet.SingleProductListServlet"> <table border="1"> <tr> <td colspan="2">検索条件</td> </tr> <tr> <td>商品番号</td> <td> <input type="text" name="productNoFrom" value="<bean:write name="Input" property="productNoFrom" format="00000000" ignore="true"/>"> ≦商品番号≦ <input type="text" name="productNoTo" value="<bean:write name="Input" property="productNoTo" format="00000000" ignore="true"/>"> </td> </tr> <tr> <td>販売期間</td> <td> <input type="text" name="saleTermFrom" value="<bean:write name="Input" property="saleTermFrom" format="yyyy/MM/dd" ignore="true"/>"> ≦販売期間≦ <input type="text" name="saleTermTo" value="<bean:write name="Input" property="saleTermTo" format="yyyy/MM/dd" ignore="true"/>"> </td> </tr> <tr> <td>単価</td> <td> <input type="text" name="priceFrom" value="<bean:write name="Input" property="priceFrom" format="#" ignore="true"/>"> ≦単価≦ <input type="text" name="priceTo" value="<bean:write name="Input" property="priceTo" format="#" ignore="true"/>"> </td> </tr> </table> <input type="submit" value="検索"> </form> <br> <%-- 検索結果欄 --%> <logic:present name="SingleProducts"> <table border="1"> <tr> <th>商品番号</th> <th>販売開始日</th> <th>販売終了日</th> <th>単価</th> <th>商品名</th> </tr> <logic:iterate id="sp" name="SingleProducts" scope="request"> <tr> <td><bean:write name="sp" property="productNo" format="00000000"/></td> <td><bean:write name="sp" property="saleFrom" format="yyyy/MM/dd"/></td> <td><bean:write name="sp" property="saleTo" format="yyyy/MM/dd"/></td> <td><bean:write name="sp" property="price" format="#,##0円"/></td> <td><bean:write name="sp" property="name"/></td> </tr> </logic:iterate> </table> </logic:present> <logic:notPresent name="SingleProducts"> 検索結果を表示できません。 </logic:notPresent> <%-- デバッグ用 net.java.ao.Query 表示 --%> <hr> <logic:notEmpty name="Query"> <logic:notEmpty name="Query" property="whereParams"> <bean:size id="size" name="Query" property="whereParams"/> <logic:notEqual name="size" value="0"> <h4>[デバッグ用] 以下の条件で検索しています。</h4> <table border="1"> <tr><th colspan="3">where句</th></tr> <tr><td colspan="3"><bean:write name="Query" property="whereClause"/></td></tr> <tr><th colspan="3">パラメータ</th></tr> <tr><th>no</th><th>クラス</th><th>値</th></tr> <logic:iterate id="param" indexId="no" name="Query" property="whereParams"> <tr> <td><%= no %></td> <td><%= param.getClass().getName() %></td> <td><%= param.toString() %></td> </tr> </logic:iterate> </table> </logic:notEqual> <logic:equal name="size" value="0"> <h4>[DEBUG]無条件で検索しています。</h4> </logic:equal> </logic:notEmpty> <br> <logic:empty name="Query" property="whereParams"> <h4>[DEBUG]無条件で検索しています。</h4> </logic:empty> </logic:notEmpty> <logic:empty name="Query"> <h4>[DEBUG]無条件で検索しています。</h4> </logic:empty> </body> </html>
【デバッグ機能】Queryの内容のを画面に表示する。
JSPソースの最後の方の、「<%-- デバッグ用 net.java.ao.Query 表示 --%>」の行以降は、ActiveObjectsのQueryの内容を見るデバッグ用のコードになっています。今までのサンプルでは見せていなかったのですが、画面上には検索したクエリの内容を表示しています。
【エラー処理】SQLException
EntityManagerのfind()呼び出し時の、SQLExceptionに対して暫定的な対応を入れました。本当はもっとちゃんとした対応が必要だと思います。
以下、エラー時の画面です。
【重要】EntityManager.find(Class type, Query query)の注意点
この形のfind()メソッドでの全件検索で少しひっかかりました。以下、うまくいかない方法、うまくいく方法をそれぞれ上げます。
失敗1 - 引数queryにnullを渡してはいけません。
例
manager.find( SingleProduct.class, null );
NullPointerExceptionが発生します。以下、スタックトレースです。
java.lang.NullPointerException net.java.ao.EntityManager.find(EntityManager.java:600) sample.servlet.SingleProductListServlet.doPost(SingleProductListServlet.java:93) javax.servlet.http.HttpServlet.service(HttpServlet.java:637) javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
失敗2 - 引数queryのwhere()に空文字列を指定してはいけません。
例
Query query = Query.select().where( "" ); manager.find( SingleProduct.class, query );
空のwhere句を持つselect文が発行されるため、失敗します。以下、発行されるselect文の例です。
SELECT id FROM singleProduct WHERE
成功
以下のどちらかであればうまくいきます。
例1
Query query = Query.select(); manager.find( SingleProduct.class, query );
例2
Query query = Query.select().where( null ); // わざとらしいですが・・・・ manager.find( SingleProduct.class, query );
別のDBに切り替えてみた
ここまではPostgreSQLで動かしていました。そこで、MySQLでも同じコードが動くかどうか試してみました。
前回の通り、DBを切り替えるためには、この三か所の変更だけでOKです。実際にはWebアプリケーションのクラスパスにはMySQLのJDBCドライバを置いたにしているので、2箇所だけ変更しました。
以下、実際の画面です。
パッと見
MySQLでも問題なく動いているようです。
おわりに
以上のように、検索機能を追加してみました。
DBの構造を意識する必要がないため、思いついたことをjavaで表現するには非常に楽でした。
PostgreSQLからMySQLへのDBの切り替えは、実はこの日記を書きながら行いました。上記のとおり3ステップの変更のみなので、これは非常に簡単です。
DBを意識する必要はほぼないので、javaに慣れている人にはActiveObjectsはおススメできそうです。
- 追記
今回は、これを作成するのに、DBの対話式端末は一切使いませんでした。確認用のSQLの発行等は一切しませんでした。実のところ、すっかり忘れていたました。