ActiveObjectsを使ってみた(3) - 一覧に検索機能を追加する

ActiveObjectsを使ってみた(2) - PostgreSQLまでに作成した、「単品商品一覧画面」に、検索機能を追加しました。

まずは、画面イメージ

初期表示

すべての単品商品が表示されます。

画面上部のテキストボックスに検索条件を入力し、検索ボタンをクリックすることで検索が行えます。いくつかの条件で検索してみました。

商品番号が「3」以上の単品商品を検索

商品番号が「2」以上「4」以下の単品商品を検索

「2008/12/30」から「2009/01/10」の期間に販売されている単品商品を検索

単価が「5000」円以上の単品商品を検索

複雑な条件で検索

変な入力への対処

日付欄に文字を入れるなど、おかしな入力値で検索した場合、エラーを表示します。また、このとき一覧にはすべての単品を表示します。

変な値を入力する

検索ボタンをクリックする

今回変更・追加したプログラム

SingleProductListServlet.java - 単品商品一覧画面(サーブレット) - 変更

POSTリクエスト用のメソッドを追加しました。このメソッドでは以下のような処理を行っています。

  1. 入力値を取得する
  2. 入力値からAND条件を作成する
  3. AND条件からクエリを設定する。
  4. クエリを使用して単品商品を検索する。

以下、ソースファイルです。

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 List clauses = new ArrayList();	// Where句
	private List params = new ArrayList();	// Where句のパラメータ

	/**
	 * 条件を追加する。
	 */
	public void add( String clause, Object... params ) {
		// Where句を追加する。
		this.clauses.add( clause );
		// Where句のパラメータを追加する。
		for ( Object param : params ) {
			this.params.add( param );
		}
	}
	
	/**
	 * Where句を取得する。
	 */
	public String getWhereClause() {
		// Where句のListの内容を" and "で結合した結果を返す。
		StringBuilder bak = new StringBuilder();
		if ( clauses.size() > 0 ) {
			bak.append( clauses.get( 0 ) );
			for ( int i = 1; i < clauses.size(); ++i ) {
				bak.append( " and " + clauses.get( i ) );
			}
		}
		return bak.toString();
	}
	
	/**
	 * Where句のパラメータを取得する。
	 */
	public Object[] getWhereParams() {
		return params.toArray( new Object[0] );
	}
}


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に対して暫定的な対応を入れました。本当はもっとちゃんとした対応が必要だと思います。
以下、エラー時の画面です。

DB停止によるエラー時


【重要】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でも同じコードが動くかどうか試してみました。

  • EntityManagerの生成時に渡すURL/ユーザ名/パスワード
  • マイグレーション時のクラスパスに含めるJDBCドライバ
  • Webアプリケーションのクラスパスに含めるJDBCドライバ

前回の通り、DBを切り替えるためには、この三か所の変更だけでOKです。実際にはWebアプリケーションのクラスパスにはMySQLJDBCドライバを置いたにしているので、2箇所だけ変更しました。
以下、実際の画面です。

初期表示

条件を指定して検索

DBを止めてみる

パッと見

MySQLでも問題なく動いているようです。

おわりに

以上のように、検索機能を追加してみました。
DBの構造を意識する必要がないため、思いついたことをjavaで表現するには非常に楽でした。
PostgreSQLからMySQLへのDBの切り替えは、実はこの日記を書きながら行いました。上記のとおり3ステップの変更のみなので、これは非常に簡単です。
DBを意識する必要はほぼないので、javaに慣れている人にはActiveObjectsはおススメできそうです。

追記

今回は、これを作成するのに、DBの対話式端末は一切使いませんでした。確認用のSQLの発行等は一切しませんでした。実のところ、すっかり忘れていたました。