FrontPage

2008/01/24からのアクセス回数 6900

Cart問題

Agile Web Development with Railsの例題と同じ問題をSpringを使って実装を試みたときの メモです。

もう一つの目的は、Spring-MVCプラグインがどの程度実際の問題解決に役立つかを検証することです。

プラグインのインストールについては、Spring-MVCプラグイン機能追加2(Validation)を参照してください。

プロジェクトの作成

mavenを使ってプロジェクトを生成します。

  • groupIdは、example.cart
  • artifactIdは、cart

とします。ecliseでプロジェクトを管理できるようにeclipseプラグインも起動します。

mvn archetype:create \
	-DgroupId=example.cart \
	-DartifactId=cart \
	-DarchetypeArtifactId=spring-mvc-archetype \
	-DarchetypeGroupId=jp.co.pwv.spring-mvc-archetype \
	-DarchetypeVersion=1.1.1

cd cart

mvn eclipse:eclipse -DdownloadSources=true

データベースは、HsqlDBのサーバを使用するため、src/main/webapp/WEB-INF/db.propertiesの内容を修正します。

db.url=jdbc:hsqldb:hsql://localhost

HsqlDBのインストールは、こちら を参照してください。

HsqlDBサーバの起動は、HSqlDBをインストールしたディレクトリの直下にあるdemoフォルダに移動して、

./runServer.sh

と入力してください。これでHsqlDBサーバ起動しています。

しかし、HSqlDBサーバにT_MEMBERのテーブルができていません。Cartサーブレットを起動すると自動的にT_MEMBERテーブルが作成されますので、jettyプラグインを使ってCartサーブレットを起動します。

mvn jetty:run

ブラウザーでhttp://localhost:8080/cart/ と入力してください。addリンクだけのページが表示された成功です。

CTRL-Cでjettyが終了します。

session scopeについて

長くなったので別タイトルにしました。

Product(製品の)の管理

最初にProductを管理するページを作成します。

さしあたり、管理機能として

  • 製品の一覧表示
  • 製品の追加、編集、削除 ができるようにします。

Productドメインモデルの作成

最初にProductのドメインモデルを作成します。 ドメインモデルは、example.cart.domainパッケージ内に定義します。

eclipseで以下のように入力した後、getter/setterを自動生成してください。

package example.cart.domain;

public class Product {
	private Integer	id;
	private String	title;
	private String	description;
	private String	image_url;
}

GenMVCプラグインの起動

GenMVCプラグインのscaffoldゴールを指定して、ProductのDao、 Controller、 View、データベーステーブル を自動生成します。

その前に、GenMVCプラグインは、Productのクラスファイルを見に行くので、mavenのpackageを実行します。

mvn package

mvn GenMVC:scaffold

このコマンドで、

[INFO] ------------------------------------------------------------------------
[INFO] Building Unnamed - example.cart:cart:war:1.0-SNAPSHOT
[INFO]    task-segment: [GenMVC:scaffold]
[INFO] ------------------------------------------------------------------------
[INFO] [GenMVC:scaffold]
[INFO] pkgName:example.cart
[INFO] runtime.classpath:/Users/take/Documents/workspace/cart/target/classes
[INFO] cls: example.cart.domain.Member
[INFO] template fullpath:velocity/IDao.vm
[INFO] template fullpath:velocity/Dao.vm
[INFO] template fullpath:velocity/edit_stub.vm
[INFO] template fullpath:velocity/list_stub.vm
[INFO] template fullpath:velocity/hbm.vm
[INFO] cls: example.cart.domain.Product
[INFO] template fullpath:velocity/IDao.vm
[INFO] template fullpath:velocity/Dao.vm
[INFO] template fullpath:velocity/Manager.vm
[INFO] template fullpath:velocity/EditController.vm
[INFO] template fullpath:velocity/OpsController.vm
[INFO] template fullpath:velocity/edit.vm
[INFO] template fullpath:velocity/edit_stub.vm
[INFO] template fullpath:velocity/list.vm
[INFO] template fullpath:velocity/list_stub.vm
[INFO] template fullpath:velocity/hbm.vm
[INFO] template fullpath:velocity/servlet-stub.vm
[INFO] template fullpath:velocity/sql.vm
[INFO] template fullpath:velocity/applicationContext.vm
[INFO] template fullpath:velocity/form-messages.vm
[INFO] template fullpath:velocity/validation.vm

と出力され、必要なファイルがすべて生成されます。

再度、jettyプラグインを実行して、

mvn jetty:run

ブラウザーでhttp://localhost:8080/cart/productops/list.htmと入力してください。

以下のような画面が表示されますので、

product_empty.jpg

これだけで、Productのリスト表示、編集の画面が生成されます。

iddescriptionimage_urltitle
1グンタースブルム村の畑はフランスのシャブリ地区に似た石灰分の多い土壌となっています。ワイングートドクター・シュネルはこの優れた土壌でぶどうの皮に付いた天然酵母だけを使いすべて木樽発酵という古典的な方法でワインを造っています。このワインは手入れの良く行き届いた単一畑で有機農法栽培されたヴァイサーブルグンダーぶどうから誕生した豊かでおだやかなやさしい感じの白ワインです。/images/Dr.Schnell_Gunt_Weiss_QbAB.jpgGuntersblumer Weisserburgunder QbA
2このワインは手入れの良く行き届いたシュタイグテラッセ単一畑で有機農法栽培されたシルヴァーナーぶどうから誕生した豊かでおだやかなやさしい感じの白ワインです。/images/Dr.Schnell_Steig_Silver1L_QbAL.jpgGuntersbulmer Steigtrasse Silvaner
3このワインは手入れの良く行き届いたグンタースブルム村のフォゲルスゲルテン(鳥の園)単一畑で有機農法栽培されたリースリングぶどうから誕生した豊かでおだやかなやさしい感じの白ワインです。/images/VogelRirSpt-L.jpgGuntersbulmer Vogelsgaerten Riesling Spaetlese

と入力したのが、以下の表示例です

product_list.jpg

属性の追加

scaffoldの後にProductに属性を追加したくなることはよくあります。

Product の属性を変更したときの手順は以下の通りです。

  • テーブルの削除
  • velocity/*_stub.vmファイルのバックアップ
  • validation.xmlファイルのバックアップ

通常は、これで十分ですが、以下のファイルを修正した場合にはバックアップを取ってください。

  • webapp/WEB-INF/hbm-dir/Product.hbm.xml
  • main/resources/form-messages.properties

今回は、自動生成されたファイルを全く変更していないので、テーブルの削除だけを行います。

テーブルの削除

開発の途中ではデータベースのテーブルを変更したり、値を参照します。このような用途に便利なのが Ecl,ipseのプラグインDbEditです。 DbEditのインストール方法はここ を参照してください。

DbEditのTableタグを開くと以下のようにT_MEMBERとT_PRODUCTの2つのテーブルが作られています。

DbEdit.jpg

GenMVCプラグインでは、クラス名の前にT_を付けたテーブルが作成されます。 T_PRODUCTを削除するには、 T_PRODUCTで右マウスクリックから削除を選択してください。

  • すでに沢山のデータが入っていたらどうすれば良いのか? この答えは、テストケースのデバッグで紹介する予定です。

Productクラスの変更

Productに価格(price)を追加します。

以下のように属性priceを追加し、getter/setterを自動生成するだけです。

	private Double	price;

日本では価格に小数点はないのですが、ここでは例としてDouble型を使いました。

それでは、先ほどと同様にGenMVCプラグインを起動します。

mvn package

mvn GenMVC:scaffold

画面(Velocityテンプレート)の変更

GenMVCプラグインが生成する画面は、属性の出力順がProductクラスの定義順に並んでいないので、実際には手で修正する必要があります。

ProductのVelocityテンプレートは、main/webapp/WEB-INF/velocity/productops/以下にあります。

  • list_stub.vm
  • list.vm

が一覧を表示するテンプレートです。

list.vmを見ると

parse ( "productops/list_stub.vm" )

だけです。 これは、GenMVCプラグインがlist.vmを変更しないようするためにlist_stub.vmをインクルードする 2段階で処理しています。

従ってユーザvelocityテンプレートを変更する場合には、list_stub.vmをlist.vmにコピーして編集します。 以下に順序を入れ替えたlist.vmを示します。

<html>
  <head>
  	<title>Product</title>
  </head>
  <body>
  <h1>Listing product</h1>
    <table>
		<tr>
		<td>id</td>
	        <td>title</td>
	        <td>description</td>
	        <td>image_url</td>
	        <td>price</td>
		</tr>	
#foreach (${product} in ${productList})
		<tr>
		<td>${product.id}</td>
	        <td>${product.title}</td>
	        <td>${product.description}</td>
	        <td>${product.image_url}</td>
	        <td>${product.price}</td>
			#set( $editLink = "/editproduct.htm?id=${product.id}" )
	        <td><a href="#springUrl(${editLink})">[edit]</a></td>
			#set( $deleteLink = "/productops/delete.htm?id=${product.id}" )
	        <td><a href="#springUrl(${deleteLink})">[delete]</a></td>
        </tr>
#end
    </table>
    <a href='#springUrl("/editproduct.htm")'>add</a>
  </body>
</html>

同様に編集画面editProduct.vmも順序を変え、Descriptionをtextareaに変えてます。

<html>
<head>
	<title>Products</title>
</head>

<body>
Edit Product
	<form method="post" action="#springUrl("/editproduct.htm")">
		#springFormHiddenInput( "product.id" "" )
		<table>
	        <tr>
	            <td>title:</td>
	            <td>#springFormInput( "product.title" "size='35'" )</td>
	            <td>
				#springBind("product.title")
				<font color="red">${status.errorMessage}</font>
	            </td>
	        </tr>
	        <tr>
	            <td>description:</td>
	            <td>#springFormTextarea( "product.description" "rows='4' cols='40'" )</td>
	            <td>
				#springBind("product.description")
				<font color="red">${status.errorMessage}</font>
	            </td>
	        </tr>
	        <tr>
	            <td>image_url:</td>
	            <td>#springFormInput( "product.image_url" "size='35'" )</td>
	            <td>
				#springBind("product.image_url")
				<font color="red">${status.errorMessage}</font>
	            </td>
	        </tr>
	        <tr>
	            <td>price:</td>
	            <td>#springFormInput( "product.price" "size='10'" )</td>
	            <td>
				#springBind("product.price")
				<font color="red">${status.errorMessage}</font>
	            </td>
	        </tr>
	        <tr>
	            <td colspan="3">
	                <input type="submit" value="Save Changes"/>
	            </td>
	        </tr>
		</table>
	</form>
</html>
</body>

Validationの変更

現在の入力フォームは、各フィールドが必須だけのチェックしかしていません。 priceに文字を入力して、Save Chageボタンを押すと

validation_error1.jpg

が出力されます。

これでは、エラーの原因が分かりづらいので、validation.xmlを修正してpriceをdoubleとしてふさわしい値になるようにしようと、

			<field property="price" depends="required">
				<arg0 key="product.price" />
			</field>

			<field property="price" depends="required,double">
				<arg0 key="product.price" />
			</field>

としたが、ダメでした。

原因は、Validationが行われる前に、Productの値がHTTPのパラメータからセットされるためです。

Validationエラーへの対応

ソースをトレースした結果、bindAndValidationを使用する場合、最初にHTTPリクエストから値をセットするcommandオブジェクトへのbind操作が先行します、ここでStringからDoubleへの変換に失敗するため、その後のValidationのエラーチェックでは値がセットされていないので、requiredのエラーが追加されますが、表示されません。

対応策としては、ProductionのPriceをDoubleからStringに変えるという方法しかありません。 これでは、GenMVCプラグインを起動すると間違ったCreate table文が生成されてしまいます。

commandクラスの追加

そこで、domainクラスに対応するcommandクラスをGenMVCプラグインで生成し、その属性をすべてString型としました。

更に、EditProductControllerでcomanndオブジェクトとdomainオブジェクトの値を変換するメソッドcommandToDomain, domainToCommandを追加しました。こんなに簡単にデータの設定ができるのは、CustomDataBinderの威力です。

	protected void bind(Object target, Object source)  throws Exception {
		CustomDataBinder binder = new CustomDataBinder(target, source);
		this.prepareBinder(binder);
		binder.bind();				
	}
	
	protected Object commandToDomain(Object source) throws Exception {
		Order object = manager.findById(((ProductCommand)source).getId());
		bind(object, source);
		return (object);
	}

	protected Object domainToCommand(Object source) throws Exception {
		Object object = new ProductCommand();
		bind(object, source);
		return (object);
	}

これでようやく、priceのValidationが正常に行えるようになりました。

validation_error2.jpg

スタイルシートの設定

最後にスタイルシート使って衣装替えをします。 スタイルシートについては、詳しくないのでAgile Web Development with Railsの例題のスタイルを借用します。

スタイルシートを追加したlist.vmは、以下の通りです。

<html>
  <head>
  	<link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css">
  	<title>Product</title>
  </head>
  <body>
<div id="product-list">
  <h1>Listing product</h1>
  
    <table cellpadding="5" cellspacing="0">
#foreach (${product} in ${productList})
#if($velocityCount %2 == 1)
		<tr valign="top" class="list-line-odd">
#else
		<tr valign="top" class="list-line-even">
#end
			<td>
				<img class="list-image" src="#springUrl(${product.image_url})" />
			</td>
			<td width="60%">
				<span class="list-title">${product.title}</span><br/>
#if ($product.description.length() > 80)
	#set($size = 80)
#else
	#set($size = $product.description.length())
#end
				$product.description.substring(0,$size)
			</td>
			<td class="list-action">
				#set( $editLink = "/editproduct.htm?id=${product.id}" )
				<a href="#springUrl(${editLink})">[edit]</a><br/>
				#set( $deleteLink = "/productops/delete.htm?id=${product.id}" )
				<a href="#springUrl(${deleteLink})">[delete]</a>
			</td>
        </tr>
#end
    </table>
    <a href='#springUrl("/editproduct.htm")'>New product</a>
</div>
  </body>
</html>

ここで、スタイルシートの指定を

  	<link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css">

でしているところと、説明文を一部カットするためにStringのsubstringメソッドをテンプレートから呼び出しているところに注意してください。このようにVelocityを使うとテンプレートからjavaのメソッド呼び出しができます。

スタイルシートは、src/main/webapp/cssディレクトリに入れてください。

画像ファイルは、src/main/webapp/imagesディレクトリに入れてください。

データベースは、HsqlDBのdata以下のtest.scriptを以下のファイルと入れ替えてください。

スタイルシートの出力結果は、以下の通りです。

styled_list.jpg

カタログページの作成

次にカタログ表示ページを作成します。Agile Web Development with Railsではカタログの表示にstoreという新しいコントローラを作成していますが、ここではProductOpsControllerを借用します。 その理由は、ProductOpsControllerがProductを扱うコントローラであり、MultiActionControllerのサブクラスなのでメソッド名と同じテンプレートを作成するだけでカタログページが作れるとメリットを示すためです。

メソッドとテンプレートの追加

手順は以下の通りです。

  • ProductOpsControllerのlistメソッドをコピーしてcatalogにメソッド名を変更
  • velocity/productops/list.vmをコピーしてcatalog.vmにファイル名を変更
    public ModelAndView catalog(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return new ModelAndView().addObject(manager.findAll());
    }

catalog.vmは以下の通りです。

<html>
<head>
	<link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css">
  	<title>Store</title>
</head>
<body id="store">
	<h1>Your wine catalog</h1>
  
#foreach (${product} in ${productList})
	<div class="entry">
		<img src="#springUrl(${product.image_url})" />
		<h3>${product.title}</h3>
		$product.description <br>
		<span class="price">$numberTool.format("##0", $product.price)</span>
	</div>
#end
</body>
</html>

ブラウザーでhttp://localhost:8080/cart/productops/catalog.htmと入力すると 以下のようなカタログページが表示されます。

catalog1.jpg

カートへの追加ボタンの追加

最後にカートへの追加ボタンを入れます。

価格の後に次の行を挿入します。

		<form method="post" action="#springUrl("/cartops/add.htm")">
			#springFormHiddenInput( "product.id" "" )
			<input type="submit" value="Add to Cart"/>
		</form>

画面では次のように表示されます。

catalog2.jpg

注文項目(LineItem)の追加

カートの処理に進む前に注文項目を作成します。

ここでのポイントはカートの注文項目がそのまま注文にリンクされるようにすることです。

それでは、注文項目のdomainクラスを作りましょう。

package example.cart.domain;

public class LineItem {
	private Integer	id;
	private Integer	quantity = new Integer(1);
	private Integer	productId;
	private Product	product;
	private Integer	orderId;
	
	public void addQuantity(Integer quantity) {
		this.quantity =  new Integer(this.quantity.intValue() + quantity.intValue());
	}

	public Double getPrice() {
		return (new Double(quantity.intValue()*product.getPrice().doubleValue()));
	}
}

として、getter/setterを自動生成してください。

  • addQuantityは、個数を追加する
  • getPriceは注文項目の小計を返す

この後は、いつものようにGenMVC:scaffoldを実行します。

mvn package
mvn GenMVC:scaffold

カートの処理

カートの処理を行う、CartServiceとカートに対する要求を処理するCartOpsControllerを追加します。

CartServiceの追加

最初にCartServiceを追加します。CartServiceでは

  • lineItemMapで注文項目を管理
  • lineItemの追加、合計の計算

を行います。

package example.cart.service;

import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import example.cart.domain.LineItem;

public class CartService {
	private Map	lineItemMap = new TreeMap();
	
	public void addLineItem(LineItem item) {
		Integer key = item.getProductId();
		LineItem lineItem = (LineItem)lineItemMap.get(key);
		if (lineItem == null)
			lineItemMap.put(key, item);
		else
			lineItem.addQuantity(item.getQuantity());
	}
	
	public Double getTotal() {
		double total = 0;
		Iterator itr = lineItemMap.values().iterator();
		while (itr.hasNext()) {
			LineItem lineItem = (LineItem)itr.next();
			total += lineItem.getPrice().doubleValue();
		}
		return (new Double(total));
	}

	public Map getLineItemMap() {
		return lineItemMap;
	}
}

ここで、session-def.xmlが正しくweb.xmlに追加されていることを確認してください。

	  <param-value> 
		/WEB-INF/custom-editor.xml
		/WEB-INF/db-def.xml
		/WEB-INF/session-def.xml
		/WEB-INF/applicationContext.xml
	  </param-value> 

session-def.xmlでcartServiceを以下の様に定義します。

	<!-- a HTTP Session-scoped bean exposed as a proxy -->
	<bean id="cartService" class="example.cart.service.CartService" scope="session" />

これで、セッション毎に新しいcartServiceを取得することができるようになります。

CartOpsControllerの追加

ProductOpsContollerをコピーしてCarOptsControllerを作成します。

  • setupCartServiceでは、session scopeオブジェクトをWebApplicationContextから取得し、cartServiceに セットします
	private CartService		cartService;
	private ProductManager	manager;

	private void setupCartService(HttpServletRequest request) {
    	    	ApplicationContext co = WebApplicationContextUtils.
    	    	    	getRequiredWebApplicationContext( request.getSession().getServletContext());
    	    	cartService = (CartService)co.getBean("cartService");	
	}

listでは、

  • lineItemのリストをcartServiceから取得し、"lineItemList"の名前で登録します
  • テンプレートは、デフォルトでcartops/list.vmがセットされます
    public ModelAndView list(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	setupCartService(request);
    	return new ModelAndView().addObject("lineItemList", cartService.getLineItemMap().values());
    }

addでは、

  • HTTP要求からProductのidを取得し、ProductManagerを使ってProductを取得します
  • 新たにLineItemを生成し、productId, productをセットし、cartServiceに追加します
  • lineItemのリストをcartServiceから取得し、"lineItemList"の名前で登録します
  • テンプレートにcartops/list.vmをセットします
    public ModelAndView add(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Integer productId = new Integer(ServletRequestUtils.getRequiredIntParameter(request, "id"));
    	setupCartService(request);
    	Product product = manager.findById(productId);
    	LineItem item = new LineItem();
    	item.setProductId(productId);
    	item.setProduct(product);
    	cartService.addLineItem(item);
    	
    	ModelAndView modelAndView = new ModelAndView();
    	modelAndView.addObject("lineItemList", cartService.getLineItemMap().values());
    	modelAndView.setViewName("cartops/list");
        return modelAndView;
    }

cartops/list.vmの追加

cartopsフォルダをsrc/main/webapp/WEB-INF/velocityに作成し、list.vmを追加します。

簡単な項目一覧をlist.vmで出力します。

<html>
  <head>
  	<title>Cart</title>
  </head>
  <body>
  <h1>Your Wine Cart</h1>
  <ul>
#foreach ($lineItem in $lineItemList)
	<li> $lineItem.quantity &times; $lineItem.product.title </li>
#end
  </ul>
  </body>
</html>

コンフィグファイルへのCartOpsControllerの追加

CartOpsControllerは、GenMVCプラグインの影響を受けないようにserver-def.xmlに定義します。

	<bean id="cartOpsController" class="example.cart.web.CartOpsController"
          parent="baseProductController"/>

jettyプラグインを起動し、

mvn jetty:run

ブラウザーからカタログ画面を表示(http://localhost:8080/cart/productops/catalog.htm)し、Add to Cartボタンを押すと以下のような画面に遷移します。

cart1.jpg

価格とEmpty cartボタンの追加

次に小計と合計の表示とEmpty cartボタンを追加し、ひとまずcartの完成としましょう。

  • list, addにtotalを追加登録します
        	modelAndView.addObject("total", cartService.getTotal());
    
  • emptyCartを追加します
        public ModelAndView emptyCart(HttpServletRequest request, HttpServletResponse response) throws Exception {
         	setupCartService(request);
         	cartService.getLineItemMap().clear();
        	ModelAndView modelAndView = new ModelAndView();
        	modelAndView.setViewName("redirect:/productops/catalog.htm");
            return modelAndView;
        }
    

cartops/list.vmにempty cartボタンを追加します。

<form method="post" action="#springUrl("/cartops/emptyCart.htm")">
	<input type="submit" value="Empty cart"/>
</form>

最終的な画面は、以下のようになります。

cart2.jpg

チェックアウトの処理

最後にチェックアウトの処理を追加します。

Orderの作成

注文書には、以下の項目を入れます。

  • name 購入者の氏名
  • address 購入者の住所
  • email 購入者のe-mailアドレス
  • payType 支払い方法

domainクラスとしてOrderを作成します。

package example.cart.domain;

import java.util.List;

public class Order {
	private Integer	id;
	private String	name;
	private String	address;
	private String	email;
	private String	paytype;
	private List	lineItemList;
	
}

を入力して、getter/setterを自動生成します。

そして、GenMVC:scaffoldを実行します。

mvn package
mvn GenMVC:scaffold

テーブルの関連づけ

すべてのテーブルが出そろったので、テーブルの関連づけをします。

詳しくは、モデル中心プログラミング を参照してください。

  • LineItemから参照しているProductを結合する場合、LineItem.hbm.xmlのclass定義を以下のように変更します。
    	<class 
    		name="example.cart.domain.LineItem"
    		table="T_LINEITEM">
    		<id name="id">
    			<generator class="increment"/>
    		</id>
    		<property name="orderId"/>
    		<property name="productId" insert="false" update="false"/>
    		<property name="quantity"/>
    		<many-to-one
    			name="product"
    			column="productId"
    			cascade="save-update"
    			class="example.cart.domain.Product"/>
    	</class>
    
  • Orderと複数のLineItem結合する場合、Order.hbm.xmlに以下の定義を追加します。
    		<bag name="lineItemList"
    			cascade="all"
    			table="T_LINEITEM">
    			<key column="orderId" foreign-key="ID"/>
    			<one-to-many class="example.cart.domain.LineItem"/>
    		</bag>
    

最後に、src/main/webapp/WEB-INF/hbm-dir/Order.hbm.xml, LineItem-hbm.xmlのバックアップを取ってください。

checkoutの追加

OrderOpsControllerにcheckoutメソッドを追加します。 それと同時にcartService属性を追加して、session scopeのcartServiceから注文項目を取り出します。

	private CartService		cartService;

	private void setupCartService(HttpServletRequest request) {
    	    	ApplicationContext co = WebApplicationContextUtils.
    	    	getRequiredWebApplicationContext( request.getSession().getServletContext());
    	    	cartService = (CartService)co.getBean("cartService");	
	}
    public ModelAndView checkout(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	setupCartService(request);
    	Order order = new Order();
    	order.setLineItemList(new ArrayList(cartService.getLineItemMap().values()));
    	manager.saveOrUpdate(order);
    	
    	ModelAndView modelAndView = new ModelAndView();
    	modelAndView.setViewName("redirect:/editorder.htm?id=" + order.getId());
        return modelAndView;
    }

次にvelocity/cartopgs/list.vmにcheckoutボタンを追加します。

<form method="post" action="#springUrl("/orderops/checkout.htm")">
	<input type="submit" value="Checkout"/>
</form>

これだけの変更でカートの項目一覧と注文がデータベースにセットされてしまいます。

注文一覧の画面

注文が空の状態から始めます。 ブラウザーからhttp://localhost:8080/cart/orderops/list.htmと入力します。

order1.jpg

カートに2つの商品を入れます

ブラウザーからhttp://localhost:8080/cart/productops/catalog.htmと入力し、カートに入れます。

order2.jpg

注文情報を入力します

order3.jpg

注文一覧に1個の注文が入っています

order4.jpg

データベースの内容を確認します

order5.jpg

チェックアウトの衣装替え

EditOrderControllerは、戻り場所をコンフィグファイルで固定となるため、EditOrderControllerのサブクラスとして定義InputOrderControllerを作成することにします。

package example.cart.web;

import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.validation.Errors;

public class InputOrderController extends EditOrderController {
	private Map	payTypeOptions;

	protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception {
		Map refData = new HashMap();
		refData.put("payTypeOptions", payTypeOptions);
		return refData;
	}
	public void setPayTypeOptions(Map payTypeOptions) {
		this.payTypeOptions = payTypeOptions;
	}
}

payTypeのオプションはマップで指定するため、Velocityの中では定義できません。そこで、Springのコンフィグから取得し、referenceDataメソッドでマップにセットしVelocityに渡すことにしました。

inputOrder.vmのVelocityテンプレートを以下の様に修正します。

<html>
<head>
  	<link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css">
	<title>Input Order</title>
</head>

<body>
#springBind("order.name")
<font color="red">${status.errorMessage}</font><br>
#springBind("order.address")
<font color="red">${status.errorMessage}</font><br>
#springBind("order.email")
<font color="red">${status.errorMessage}</font><br>

<div class="cart-form">
	<fieldset>
		<legend>Please Enter Your Details</legend>
		<form method="post" action="#springUrl("/inputorder.htm")">
			#springFormHiddenInput( "order.id" "" )
			<p>
				<label>Name:</label>
				#springFormInput( "order.name" "size='40'" )
			</p>			
			<p>
				<label>Address:</label>
				#springFormTextarea( "order.address" "rows='3' cols='40'" )
			</p>			
			<p>
				<label>E-Mail:</label>
				#springFormInput( "order.email" "size='40'" )
			</p>			
			<p>
				<label>Pay with:</label>
				#springFormSingleSelect( "order.paytype" $payTypeOptions "" )
			</p>			
			<input type="submit" value="Place Order" class="submit"/>
		</form>
	</fieldset>
</div>
</body>
</html>

これで、InputOrderControllerが完成しましたので、checkoutメソッドからの遷移先を inputOrder.htmに変更します。

    	modelAndView.setViewName("redirect:/inputorder.htm?id=" + order.getId());

Springのコンフィグファイルは、server-def.xmlに以下の項目を追加します。

	<bean id="inputOrderController" class="example.cart.web.InputOrderController"
          parent="orderController">
        <property name="formView" value="inputOrder"/>
        <property name="successView" value="redirect:orderops/list.htm"/>
	<property name="payTypeOptions">
		<map>
			<entry key="check" value="Check"/>
			<entry key="cc" value="Credit card"/>
			<entry key="po" value="Purchase order"/>		
		</map>					
	</property>
    </bean>

このようにparent属性でEditOrderControllerを指定すると、差分だけを記述するだけですみます。 payTypeOptionsにpayTypeOptionsの選択肢をセットします。

注文入力画面は以下のようになります。

order6.jpg

注文確認と注文確定、キャンセル処理

最後に、注文確認と注文確定、キャンセル処理を組み込みます。

注文確認

OrderOpsControllerに以下のようなcheckorderメソッドを追加します。

    public ModelAndView checkorder(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	setupCartService(request);
        int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
        Order order = manager.findById(new Integer(id));
    	
    	ModelAndView modelAndView = new ModelAndView();
    	modelAndView.addObject(order);
    	modelAndView.addObject("total", cartService.getTotal());
        return modelAndView;
    }
  • cartServiceは、合計を計算するのを省略するために使いました
  • データベースからorderを取得し、それをmodelAndViewに登録
  • totalをmodelAndViewに登録 の処理をしています。

Velocityテンプレートのvelocity/orderops/checkorder.vmは以下の通りです。

<html>
<head>
	<link rel="stylesheet" href="#springUrl('/css/cart.css')" type="text/css">
	<title>Cart</title>
</head>
<body>
<div class="cart-title">Bill to:</div>  
<table>
	<tr>
		<td>Name:</td>
		<td>$order.name</td>
	</tr>
	<tr>
		<td>Address:</td>
		<td>$order.address</td>
	</tr>
	<tr>
		<td>E-Mail:</td>
		<td>$order.email</td>
	</tr>
	<tr>
		<td>Paywith:</td>
		<td>$payTypeOption.${order.paytype}</td>
	</tr>
</table>

<div class="cart-title">Your order items:</div>  
<table>
#foreach ($lineItem in $order.lineItemList)
	<tr>
		<td>$lineItem.quantity &times;</td>
		<td>$lineItem.product.title</td>
		<td class="item-price">$numberTool.format("##0", $lineItem.price)</td>
	</tr>
#end
	<tr class="total-line">
		<td colspan="2">Total</td>
		<td class="totale-cell">$numberTool.format("##0", $total)</td>
	</tr>
</table>
<div>
	<form method="post" action="#springUrl("/orderops/confirm.htm")">
		#springFormHiddenInput( "order.id" "" )
		<input type="submit" value="Confirm order"/>
	</form>
	
	<form method="post" action="#springUrl("/orderops/cancel.htm")">
		#springFormHiddenInput( "order.id" "" )
		<input type="submit" value="Cancel order"/>
	</form>
</div>

</body>
</html>

InputOrderからの遷移画面をcheckorderにするために、InputOrderControllerのdoSubmitActionを以下の様に追加します。

	protected void doSubmitAction(Object object) throws Exception {
		OrderCommand command = (OrderCommand)object;
		this.setSuccessView("redirect:orderops/checkorder.htm?id=" + command.getId());
		super.doSubmitAction(object);
	}

注文確認画面は以下の通りです。

order7.jpg

注文の確定

OrderOpsControllerに以下のようなconfirmメソッドを追加します。

    public ModelAndView confirm(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	setupCartService(request);
    	cartService.getLineItemMap().clear();
    	
    	ModelAndView modelAndView = new ModelAndView();
    	modelAndView.setViewName("redirect:/productops/catalog.htm");
        return modelAndView;
    }

注文確定の主な処理は、カートを空にし、カタログページに飛ぶことです。

キャンセル

OrderOpsControllerに以下のようなcancelメソッドを追加します。

    public ModelAndView cancel(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	setupCartService(request);
    	cartService.getLineItemMap().clear();
        int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
        Order order = manager.findById(new Integer(id));
        manager.delete(order);
    	
    	ModelAndView modelAndView = new ModelAndView();
    	modelAndView.setViewName("redirect:/productops/catalog.htm");
        return modelAndView;
    }
  • 注文をデータベースから削除する
  • カートを空にする
  • カタログページにジャンプする の処理をしています。

lazily initializeエラーへの対処

hibernateのlazy initialize機能ではhibernateのsessionを使ってビューを描画したとき、many-to-one, one-to-manyで連結されている属性の値を必要になったときに取得しようとするが、そのときにsessionが閉じているので、lazily initializeエラーが発生します。

対応としては、

  • lazy="false"をbag, many-to-one設定に追加する
  • web.xmlにOpenSessionInViewFilterを追加する

がありますが、lazy="false"とするとパフォーマンスが落ちるので、OpenSessionInViewFilterを指定します。

web.xmlに以下の行を追加します。

	<filter>
		<filter-name>hibernateFilter</filter-name>
		<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
		<init-param>
			<param-name>singleSession</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>flushMode</param-name>
			<param-value>AUTO</param-value>
		</init-param>
	</filter>
	
	<filter-mapping>
		<filter-name>hibernateFilter</filter-name>
		<url-pattern>*.htm</url-pattern>
	</filter-mapping>

cart問題の全ソース

cart問題の全ソースを以下にアップします。ダウンロードして試してみてください。

このzipファイルからcartを動作させるには、

  • Mavenのインストールに従ってmaven2をインストールします
  • Spring-MVCプラグイン機能追加2(Validation)に従ってプラグインをインストールします
  • MavenとSpringによるMVC技法に従いjtaのインストールをします
  • HSqlDBをこちら からダウンロードします、test.scriptをバージョン1.7.1で作成したので、これに合わせます。
  • HsqlDbサーバを起動します(demoフォルダのrunServer.batをダブルクリック)
  • 添付のtest.scriptをダウンロードして、HsqlDBのdataフォルダにコピーします
  • mvn jetty:runを起動します

これで、ブラウザーからhttp://localhost:8080/cart/productops/catalog.htmを表示します。

コメント

この記事は、

選択肢 投票
おもしろかった 6  
そうでもない 4  
わかりずらい 5  


添付ファイル: filecart.zip 536件 [詳細] filecheckorder.vm 745件 [詳細] fileinputOrder.vm 823件 [詳細] fileInputOrderController.java 706件 [詳細] fileOrderOpsController.java 690件 [詳細] filelist.vm 698件 [詳細] filesession-def.xml 668件 [詳細] fileLineItem.java 700件 [詳細] fileCartOpsController.java 695件 [詳細] fileCartService.java 682件 [詳細] filecatalog.vm 701件 [詳細] fileimages.zip 549件 [詳細] filetest.script 697件 [詳細] filecart.css 594件 [詳細] fileproduct_empty.jpg 604件 [詳細] fileProduct.java 691件 [詳細] fileorder7.jpg 613件 [詳細] fileorder6.jpg 580件 [詳細] fileorder5.jpg 609件 [詳細] fileorder4.jpg 401件 [詳細] fileorder3.jpg 610件 [詳細] fileorder2.jpg 608件 [詳細] fileorder1.jpg 633件 [詳細] filecart2.jpg 601件 [詳細] filecart1.jpg 655件 [詳細] filecatalog2.jpg 590件 [詳細] filecatalog1.jpg 595件 [詳細] filestyled_list.jpg 592件 [詳細] filevalidation_error2.jpg 569件 [詳細] filevalidation_error1.jpg 594件 [詳細] fileproduct_list.jpg 582件 [詳細] fileDbEdit.jpg 621件 [詳細]

トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2011-09-29 (木) 03:45:50 (2040d)
SmartDoc