ActiveObjectsを使ってみた(1)

はじめに

ここまでActiveObjectsについていろいろ調べてきました。その結果、どういう仕組みなのかはなんとなーくわかってきたのですが、使いやすいのかどうなのかはさっぱりわかりませんでした。そこで、AOを実際に使ってみて、良し悪しを確かめてみたいと思います。

題材

今回は題材はなんでもよかったのですが、DBを使うようなものを自力で思いつかなかったため、情報処理技術者試験の過去問題に出た内容を実装してみることにしました。
「平成17年のテクニカルエンジニア・データベースの午後Iの問2」のシステムを実装してみます。
過去問

システムの概要

この過去問題は通信販売のシステムに関する問題です。
システムとその周囲の動作を適当にまとめると、

  • 顧客がFAX送付する注文書(および会員登録申込書)の内容を、販売会社社員がシステムに登録する。
  • 登録された注文をもとに、システムが発送書を作成する。
  • 販売会社社員は発送書の内容に基づき、商品を梱包して顧客に発送する。

といった感じだと思っています。(間違っているかも)

必要と思われるエンティティ

おそらく以下は必要でしょう。

  • 顧客エンティティ
  • 商品エンティティ(Polymorphic)
  • 単品商品エンティティ
  • パック商品エンティティ
  • 注文エンティティ
  • 注文明細エンティティ
  • 発送エンティティ
  • 発送明細エンティティ

過去問題の問題文の中に、「"単品商品"テーブルと"パック商品"テーブルは、"商品"テーブルとして一つにまとめた方がよい」という記述がありますが、ここではあえて分けて作ります。というのは、商品エンティティでポリモフィックを試したいからです。

【今回のメイン】単品商品のメンテナンス画面を作ってみる

まずはじめに、単品商品の一覧表示画面と単品商品の追加画面だけ作ってみました。以下は、実際の画面です。

一覧表示してみます。(単品商品なし)

単品商品を追加します。


単品商品の追加後に一覧表示してみます。

単品商品をさらに追加してから、一覧表示してみます。

追加時に入力を間違えてみます。


エンティティのソースファイル

エンティティは二つだけ作成しました。

Product.java - 商品
package sample.entity;

import java.util.Date;
import net.java.ao.Entity;
import net.java.ao.Polymorphic;
import net.java.ao.Generator;
import net.java.ao.Preload;
import net.java.ao.schema.AutoIncrement;
import net.java.ao.schema.Unique;
import sample.generator.ProductNoGenerator;

@Polymorphic	// ポリモフィックなので、テーブルは作られません。
public interface Product extends Entity {
	// 商品番号
	@Unique	// 一意制約
	@Generator(ProductNoGenerator.class)	// 一意の商品番号を作成します。
	int getProductNo();
	
	// 販売開始日
	void setSaleFrom( Date date );
	Date getSaleFrom();
	
	// 販売終了日
	void setSaleTo( Date date );
	Date getSaleTo();
	
	// 単価
	void setPrice( Long Price );
	Long getPrice();
}
SingleProduct.java - 単品商品
package sample.entity;

public interface SingleProduct extends Product {	// ポリモフィックなProductを継承
	// 商品名
	void setName( String name );
	String getName();
}

画面のソースファイル

上の画像のとおり、作成した画面は二つです。サーブレットJSPがそれぞれ二つづつあります。

SingleProductListServlet.java - 単品商品一覧画面(サーブレット)
package sample.servlet;

import java.sql.SQLException;
import net.java.ao.EntityManager;
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.PrintWriter;
import java.io.IOException;


public class SingleProductListServlet extends HttpServlet {
	
	public void doGet( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException {
		try {
			// EntityManagerを取得する。
			EntityManager manager = new EntityManager( "jdbc:mysql://localhost/sample", "root", "root" );

			// SingleProductを全部取得し、JSPに渡す。
			req.setAttribute( "SingleProducts", manager.find( SingleProduct.class ) );

			// JSPを表示する。
			req.getRequestDispatcher( "../jsp/SingleProductList.jsp" ).forward( req, resp );
		} catch ( SQLException e ) {
			// ※一覧の例外処理はきちんとしてない!!
			throw new ServletException( e );
		}
	}
}
SingleProductListServlet.java - 単品商品一覧画面(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>
<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>
</body>
</html>
SingleProductAddServlet.java - 単品商品追加画面(サーブレット)
package sample.servlet;

import java.sql.SQLException;
import net.java.ao.EntityManager;
import net.java.ao.Transaction;
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.PrintWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import java.text.ParseException;

public class SingleProductAddServlet extends HttpServlet {
	
	// GETリクエスト - 入力画面のみ表示する。
	public void doGet( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException {
		req.getRequestDispatcher( "../jsp/SingleProductAdd.jsp" ).forward( req, resp );
	}
	
	// POSTリクエスト - 登録処理をしてから、入力画面を表示する。
	public void doPost( final HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException {
		
		
		req.setCharacterEncoding( "Shift-JIS" );

		// EntityManagerを取得する。
		EntityManager manager = new EntityManager( "jdbc:mysql://localhost/sample", "root", "root" );
		
		
		try {
			// トランザクションを開始する。
			new Transaction( manager ) {
				public Object run() throws SQLException {

					//// 単品商品を作成する。 ////
					SingleProduct product = getEntityManager().create( SingleProduct.class );

					///// 入力値を取得し、単品商品に設定する。 /////
					DateFormat format = DateFormat.getDateInstance();
					boolean parseFailed = false;	// 解析失敗フラグ
					
					try {
						// 販売開始日を取得する。
						product.setSaleFrom( format.parse( req.getParameter( "saleFrom" ) ) );
					} catch ( ParseException e ) {
						// 販売開始日の解析に失敗する:
						product.setSaleFrom( null );
						parseFailed = true;
					}
					try {
						// 販売終了日を取得する。
						product.setSaleTo( format.parse( req.getParameter( "saleTo" ) ) );
					} catch ( ParseException e ) {
						// 販売終了日の解析に失敗する:
						product.setSaleTo( null );
						parseFailed = true;
					}
					try {
						// 販売価格
						product.setPrice( Long.parseLong( req.getParameter( "price" ) ) );
					} catch ( NumberFormatException e ) {
						// 販売価格の解析に失敗する:
						product.setPrice( 0L );
						parseFailed = true;
					}

					// 商品名を取得する。
					product.setName( req.getParameter( "name" ) );

					// すべての入力値の解析に成功したことを確認する。
					if ( parseFailed ) {
						// いずれかの入力値の解析に失敗した:
						req.setAttribute( "Message", "エラー:入力が不正です。" );
						req.setAttribute( "SingleProduct", product );
						// トランザクションロールバック
						throw new SQLException();
					}
					
					///// 単品商品の値の整合性を確認する。/////
					
					// 販売期間が逆転していないことを確認する。
					if ( product.getSaleFrom().after( product.getSaleTo() ) ) {
						// 逆転している:
						req.setAttribute( "Message", "エラー:販売期間が逆転しています。" );
						req.setAttribute( "SingleProduct", product );
						// トランザクションロールバック
						throw new SQLException();
					}
					// 販売価格が1以上であることを確認する。
					if ( product.getPrice() <= 0 ) {
						// 0以下である:
						req.setAttribute( "Message", "エラー:販売価格が0より小さいです。" );
						req.setAttribute( "SingleProduct", product );
						// トランザクションロールバック
						throw new SQLException();
					}
					// 商品名が入力されていることを確認する。
					if ( product.getName().equals( "" ) ) {
						// 入力されていない:
						req.setAttribute( "Message", "エラー:商品名が未入力です。" );
						req.setAttribute( "SingleProduct", product );
						// トランザクションロールバック
						throw new SQLException();
					}
					
					///// 単品商品を保存する。 ////
					product.save();
					
					// 戻り値不要のためnull
					return null;
				}
			}.execute();
		} catch ( SQLException e ) {
			// トランザクションロールバックされる:
			req.getRequestDispatcher( "../jsp/SingleProductAdd.jsp" ).forward( req, resp );
			return;
		}

		// 成功!
		req.setAttribute( "Message", "成功:追加しました。" );
		req.getRequestDispatcher( "../jsp/SingleProductAdd.jsp" ).forward( req, resp );
	}
}


SingleProductAdd.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>
<table border="1">
<form method="POST" action="../servlet/sample.servlet.SingleProductAddServlet">
<tr>
<th>販売開始日</th><td><input type="text" name="saleFrom" value="<logic:notEmpty name="SingleProduct"><bean:write name="SingleProduct" property="saleFrom" format="yyyy/MM/dd" ignore="true"/></logic:notEmpty>"></td>
</tr>
<tr>
<th>販売終了日</th><td><input type="text" name="saleTo" value="<logic:notEmpty name="SingleProduct"><bean:write name="SingleProduct" property="saleTo" format="yyyy/MM/dd" ignore="true"/></logic:notEmpty>"></td>
</tr>
<tr>
<th>単価</th><td><input type="text" name="price" value="<logic:notEmpty name="SingleProduct"><bean:write name="SingleProduct" property="price" format="###" ignore="true"/></logic:notEmpty>"></td>
</tr>
<tr>
<th>商品名</th><td><input type="text" name="name" value="<logic:notEmpty name="SingleProduct"><bean:write name="SingleProduct" property="name"/></logic:notEmpty>"></td>
</tr>
</table>
<br>
<input type="submit" value="追加">
</form>
</body>
</html>

【重要】マイグレーション

この処理は、毎回コンパイルと同時に実行しています。そのため、実行時にはプログラム(エンティティ)とDBのテーブル構造は必ず一致しています。

Migration.java - マイグレーション用クラス
package sample;

import java.sql.SQLException;
import net.java.ao.EntityManager;
import sample.entity.*;

public class Migration {
	
	public static void main( String[] args ) {
		// EntityManagerを取得する。
		EntityManager manager = new EntityManager( "jdbc:mysql://localhost/sample", "root", "root" );
		
		try {
			// 商品と単品商品のエンティティから、テーブル構造を作成する。
			// ※今後エンティティが増えたら、ここに追加する!!!
			manager.migrate( Product.class, SingleProduct.class );
		} catch ( SQLException e ) {
			// ここのエラー処理はたぶんこのまま放置。ビルドのタイミングでながすだけだから、どーでもいい。
			throw new RuntimeException( e );
		}
	}
}

今回のまとめ

ここまで、ほとんどDBを意識する必要がありませんでした。DB(mysql)で操作したのは、create databaseとselect/delete文ぐらいです。selectは確認用で、deleteは開発中のゴミデータを消す目的です。
基本的にjavaでエンティティ(オブジェクト)を書くだけでDBを使用できます。DBのテーブル設計よりjavaオブジェクト指向の方が好きで、すべてをjavaで表現したい人にお勧めできるかもしれません。