DWR2+SpringframeworkでGEA/Jのデータストアを使う(その1)

これまでのログでは、サーブレットからGAE/Jのデータストアを使ってみたが、DWR2+Springframeworkから使ってみる。

サンプルは、以前のログ「Google Apps Engine(Java)のDWRアプリでテキストファイルを読み込んでみる」で使ったものを発展させる。その際には、テキスト(CSV)ファイルからデータを読み込んで、セッションオブジェクトを永続化機構としたが、今回のサンプルでは、データストアを永続化機構とするように改造を加える。
以下の画面が(データストアにデータをストアしていない時の)初期画面。


画面の下半分(編集領域)から、データを登録していくと以下のような画面になる。


アクションボタンで「編集」を押すと、その行のデータが編集領域に移動して、データの更新を行うことができる(下画面)。「削除」を押すと、コンファームボックスが出た後に削除する。


これらの画面の編集と同時に、データをGAE/Jのデータストアに登録・更新・削除する。

(注記)今回のサンプルでは、Springframeworkは、DWRからbeanをインスタンス化するためだけに用いる(テスト目的)。したがって、純粋に「サンプルを動かす」という意味では必ずしも必要ではない。

プロジェクトの作成

Web Application Projectを新規に作成する。GWTは使わないので、チェックをはずす。
必要であれば、ロギングの定義を変えるなどする。
war/WEB-INF/libにdwr.jar、spring.jarをコピーする。

Personエンティティーの作成

test.entitiesパッケージにPerson.javaオブジェクトを作成する。このクラスは、GAE/Jのデータストア用のエンティティーであると同時に、画面へマッピングする(DWRでいうところの)converterとなる。

package test.entities;

import java.io.Serializable;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

/**
 * Personを表すオブジェクト。
 * DWRでconvertでFormにマッピングする。
 * 
 */
@PersistenceCapable(identityType=IdentityType.APPLICATION)
public class Person implements Serializable{

    private static final long serialVersionUID = 7369886791292665902L;

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private String name;

    @Persistent
    private String address;

    @Persistent
    private float salary;

    /**
     * クラスPersonのオブジェクトを構築します。
     * 
     */
    public Person() {
    }

    /**
     * Setter/Getter
     */
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }

    public int getIntId() {
        return id.intValue();
    }
   
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public float getSalary() {
        return salary;
    }

    public void setSalary(float salary) {
        this.salary = salary;
    }

    /**
     * プロパティーを文字列に連結して返す
     */
    public String toString(){
        return this.name + this.address+
                Float.toString(this.salary);
    }
}

Peopleクラスの作成

testパッケージに、DWRから操作するPeople.javaクラスを作成する。サンプルの性質上、DAOのような性格になる。

package test;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import test.entities.Person;

/**
 * Personの集合(HashMap)を表すオブジェクト。
 * DWRで利用する。
 * 
 */
public class People implements Serializable{
    private static final long serialVersionUID = -1738868638418288696L;

    private static final Log log = LogFactory.getLog(People.class);
    
    private Map<Integer,Person> people=null;

    /**
     * コンストラクタ
     * 
     */
    public People() {
        /*
         * データストアからPersonをloadする。
         */
        this.loadPeople();
    }
    
    @SuppressWarnings("unchecked")
    private void loadPeople(){
        people=
            new HashMap<Integer,Person>();

        PersistenceManager pm 
                = PMF.get().getPersistenceManager();
        /**
         * Personエンティティーに対するクエリの実行
         */
        Query query = pm.newQuery(Person.class);
        query.setOrdering("id asc");

        try{
            List<Person> list = (List<Person>)query.execute();
            for(Person person : list){
                people.put(person.getIntId(), person);
                log.debug("person loaded: "+person.toString());
            }
            
        }finally{
            query.closeAll();
            pm.close();
        }
    }

    /*
     *  Peopleのvalueを返却する。 
     */
    public Collection<Person> getAllPeople() {
        return people.values();
    }

    /*
     *  Personを登録、更新する。 
     */
    public void setPerson(Person person) {
        
        PersistenceManager pm 
            = PMF.get().getPersistenceManager();

        try{
            if(person.getId().intValue() != -1){
                Person storePer = pm.getObjectById(Person.class, person.getId());
                storePer.setName(person.getName());
                storePer.setAddress(person.getAddress());
                storePer.setSalary(person.getSalary());
                log.debug("person updated: "+person.toString());
            }else{
                person.setId(null);
                pm.makePersistent(person);
                log.debug("person entered: "+person.toString());
            }
        }finally{
            pm.close();
        }
        log.debug("person updated: "+person.toString());
        
        // mapの再構築
        this.loadPeople();
    }

    /**
     *  Personを削除する。 
     */
    public void deletePerson(Person person) {
        PersistenceManager pm 
        = PMF.get().getPersistenceManager();

        try{
            Person storePer = pm.getObjectById(Person.class, person.getId());
            if(storePer!=null){
                pm.deletePersistent(storePer);
                log.debug("person deleted: "+person.toString());
            }
        }finally{
            pm.close();
        }
        
        // mapの再構築
        this.loadPeople();
    }
    
    /**
     *  Personを一括削除する。 
     */
    @SuppressWarnings("unchecked")
    public void deleteAllPerson() {
        PersistenceManager pm 
        = PMF.get().getPersistenceManager();

        Query query = pm.newQuery(Person.class);

        try{
            pm.currentTransaction().begin();
            List<Person> list = (List<Person>)query.execute();
            pm.deletePersistentAll(list);
            pm.currentTransaction().commit();
            log.debug("person all deleted.");
        }finally{
            if(pm.currentTransaction().isActive()){
                // ロールバック
                pm.currentTransaction().rollback();
                log.debug("person all-delete failed.");
            }
            query.closeAll();
            pm.close();
        }
        
        // mapの再構築
        this.loadPeople();
    }
}

PersistenceManagerFactoryの作成

これまでのサンプル同様に、シングルトンで実装したPersistenceManagerFactoryを、testパッケージに配置する。

package test;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

/**
 * PersisntenceManagerFactory
 * 
 */
public final class PMF {
    private static final PersistenceManagerFactory pmf =
           JDOHelper.getPersistenceManagerFactory("transactions-optional");
    
    private PMF() {}

    public static PersistenceManagerFactory get(){
        return pmf;
    }
    
}

bean.xml

war/WEB-INF下のbean定義ファイル(beans.xml)は以下とした。

<?xml version="1.0" encoding="utf-8"?>
<beans default-lazy-init="true"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    	http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
	    http://www.springframework.org/schema/aop
    	http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
    	http://www.springframework.org/schema/util
    	http://www.springframework.org/schema/util/spring-util-2.5.xsd">

    <bean id="peopleBean" 
    		class="test.People">
    </bean>

</beans>

dwr.xml

war/WEB-INF/dwr.xmlは、以下とした。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN" "http://directwebremoting.org/schema/dwr20.dtd">

<dwr>
  <allow>
	    
    <create creator="spring" javascript="People">
	  <param name="beanName" value="peopleBean" />
    </create>

    <convert match="test.entities.Person" converter="bean">
    </convert>

  </allow>
</dwr>

web.xml

war/WEB-INF/web.xmlは以下の通り。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">

    <context-param>
        <description>
        Specify the beans.xml location.
        When you specify more than one, be separated by a camma.
        </description>
        <param-name>contextConfigLocation</param-name>
        <param-value>
        /WEB-INF/beans.xml
        </param-value>
    </context-param>

    <listener>
        <description>
        Spring Bootstrap.
        </description>
        <listener-class>
        	org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
	<listener>
  		<listener-class>
    	org.springframework.web.context.request.RequestContextListener
  		</listener-class>
	</listener>

  <servlet>
    <servlet-name>dwr-invoker</servlet-name>
    <display-name>DWR Servlet</display-name>
    <description>Direct Web Remoter Servlet</description>
    <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
    
    <init-param>
      <param-name>debug</param-name>
      <param-value>false</param-value>
    </init-param>
    
    <init-param>
      <param-name>activeReverseAjaxEnabled</param-name>
      <param-value>true</param-value>
    </init-param>
    
    <init-param>
      <param-name>initApplicationScopeCreatorsAtStartup</param-name>
      <param-value>true</param-value>
    </init-param>
    
    <init-param>
      <param-name>maxWaitAfterWrite</param-name>
      <param-value>-1</param-value>
    </init-param>

    <load-on-startup>1</load-on-startup>
    
  </servlet>

  <servlet-mapping>
    <servlet-name>dwr-invoker</servlet-name>
    <url-pattern>/dwr/*</url-pattern>
  </servlet-mapping>
  
  <welcome-file-list>
    <welcome-file>edit_table_demo.html</welcome-file>
  </welcome-file-list>
	
</web-app>

edit_table_demo.html(画面)

web.xmlでwelcome-fileに定義したedit_table_demo.htmlは以下となる。

<link rel="shortcut icon" href="../images/egp-favicon.ico" >

faviconの定義をしている。不要なら削除。

edit_table_demo.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>DWR+Spring+GAE/DataStoreのサンプリング</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="shortcut icon" href="../images/egp-favicon.ico" >

<style type="text/css">
body {
	margin:0;
	padding:0;
}

#container {
	margin: 2px;
	padding: 3px;
	line-height:1.5em;
	width: 500px;
	height: auto;
	border:1px dashed #999999;
}
</style>
	
<!-- People.jsはDWRによって自動生成される。 
     dwr/はドキュメントルートとの相対参照。
-->
<script src='dwr/interface/People.js'></script>
<!-- engine.jsは必須 -->
<script src='dwr/engine.js'></script>
<!-- util.jsは必須 -->
<script src='dwr/util.js'></script>

<script type="text/javascript">

function init() {
	  fillTable();
}

var peopleCache = { };
var viewed = -1;

function fillTable() {
	// PeopleオブジェクトのgetAllPeopleの呼び出し
  	People.getAllPeople(
  		// getAllPeopleのコールバック
  		function(people) {

  			// peoplebodyの全ての行を一旦削除する。
  			// オプションとなるオブジェクトにfilterを指定し、
  			// id != "pattern"の行(tr)を削除する。
    		dwr.util.removeAllRows("peoplebody", 
    	    	{ filter:function(tr) {
    	  			return (tr.id != "pattern");
    				}
				}
			);

			// peopleCacheをクリア
			peopleCache = { };
	   
			// 作表    	    
    		var person,idx;
    		for (var i = 0; i < people.length; i++) {
    			idx=i+1;
      			person = people[i];
      		    dwr.util.cloneNode("pattern", 
      	      			{ idSuffix:idx });
      			dwr.util.setValue("ids" + idx, person.id);
      			dwr.util.setValue("tableName" + idx, person.name);
      			dwr.util.setValue("tableSalary" + idx, person.salary);
      			dwr.util.setValue("tableAddress" + idx, person.address);
				// $("pattern"+id)でDOMを指定。
      			$("pattern" + idx).style.display 
      								= "table-row";
				// テーブルに読み込んだpersonをpeopleCacheに入れる。
				peopleCache[idx] = person;
    		}
  		}
  	);
}

function editClicked(eleid) {
	// editボタンは、id="edit"+person.idとなっているので、
	// 4桁目からのsubstring(つまり、id)をもとに
	// PersonオブジェクトをCacheからとってくる。
	var person = peopleCache[eleid.substring(4)];

	// Personオブジェクトをフォームにマップする。
	dwr.util.setValues(person);
}

function deleteClicked(eleid) {
	
	// deleteボタンは、id="delete"+person.idとなっているので、
	// 6桁目からのsubstring(つまり、id)をもとに
	// PersonオブジェクトをCacheからとってくる。
	var person = peopleCache[eleid.substring(6)];
	if (confirm(person.name + " さんを削除してよいですか?")) {

	// サーバーの処理をまとめる(start)。
	// http://directwebremoting.org/dwr/browser/engine/batch
	dwr.engine.beginBatch();
	// 削除
	People.deletePerson(person);
	fillTable();
	dwr.engine.endBatch();
	// サーバーの処理をまとめる(end)。
	}
}

function errh(msg) {
	  alert(msg);
}

function writePerson() {
	var person 
  		= { id:viewed, name:null, address:null, salary:null };

	// Formから値を取得して、Personオブジェクトを生成する。
	dwr.util.getValues(person);

	// サーバーの処理をまとめる(start)。
	dwr.engine.beginBatch();
	// 登録・更新
	People.setPerson(person);
	fillTable();
	dwr.engine.endBatch({
		  errorHandler:function(errorString, exception) {
						alert("エラーが発生しました。");
		  			   }
	});
	// サーバーの処理をまとめる(end)。
}

function clearPerson() {
	viewed = -1;
	dwr.util.setValues({ id:-1, name:null, address:null, salary:null });
}

</script>
</head>
    
<body onload="init()">
<div id="container">

<p>
DWR+Spring+GAE/Data Storeのデモです。<br>
GAE/Data Storeにあるデータの
登録、更新、削除ができます。
</p>

<h3>All People;データ</h3>
<table border="1" class="rowed grey">
  <thead>
    <tr>
      <th>ID</th>
      <th>名前</th>
      <th>給料</th>
      <th>住所</th>
      <th>アクション</th>
    </tr>
  </thead>

  <tbody id="peoplebody">
	<tr id="pattern" style="display:none;">
      <td>
        <span id="ids">ID</span>
      </td>
      <td>
        <span id="tableName">名前</span>
      </td>
      <td>&#x00a5;<span id="tableSalary">給料</span></td>
      <td>
        <small>
        	<span id="tableAddress">住所</span>
        </small>
      </td>
      <td>
        <input id="edit" type="button" value="編集" onclick="editClicked(this.id)"/>
        <input id="delete" type="button" value="削除" onclick="deleteClicked(this.id)"/>
      </td>
    </tr>
  </tbody>
</table>

<h3>Personの編集</h3>
<table class="plain">
  <tr>
    <td>名前:</td>
    <td><input id="name" type="text" size="30"/></td>
  </tr>
  <tr>
    <td>給料:</td>
    <td><input id="salary" type="text" size="20"/></td>
  </tr>
  <tr>
    <td>住所:</td>
    <td><input type="text" id="address" size="40"/></td>
  </tr>
  <tr>
    <td colspan="2" align="right">
      <small>(ID=<span id="id">-1</span>)</small>
      <input type="button" value="Save" 
      				onclick="writePerson()"/>
      <input type="button" value="Clear" 
      				onclick="clearPerson()"/>
      <input type="button" value="All Delete" 
      				onclick="People.deleteAllPerson()"/>
   </td>
  </tr>
</table>

</div>
</body>
</html>

ローカル環境でのテスト

ローカルサーバーでテストをする。実は、このサンプルには(テスト目的のため)動かない機能を付加してみた
画面右下にある「All Delete」がそれで、複数のエンティティーをストアした後でこれを押すと例外が発生し、Eclipseコンソールに

22:25:52,282 WARN  [org.directwebremoting.impl.DefaultRemoter] - Method execution failed: 
javax.jdo.JDOUserException: One or more instances could not be deleted
	at org.datanucleus.jdo.JDOPersistenceManager.deletePersistentAll(JDOPersistenceManager.java:809)
....
....
NestedThrowablesStackTrace:
javax.jdo.JDOFatalUserException: Illegal argument
	at org.datanucleus.jdo.NucleusJDOHelper.getJDOExceptionForNucleusException(NucleusJDOHelper.java:344)
....
....
NestedThrowablesStackTrace:
java.lang.IllegalArgumentException: can't operate on multiple entity groups in a single transaction. found both com.google.appengine.api.datastore.dev.LocalDatastoreService$Profile$EntityGroup@1b25a82 and com.google.appengine.api.datastore.dev.LocalDatastoreService$Profile$EntityGroup@541b02
....
....

というスタックトレースがでる。
この例外は、ストアしたエンティティーが異なったエンティティー・グループに属しているために発生したものである。
People.javaにおいてPersonエンティティーを生成する際に、その親(Root)を指定しないため、全てのエンティティーがRootエンティティー(=全てのエンティティーがそれぞれ異なったエンティティー・グループに属す)となっていることを意味している。

次回のログでは、この例外を取り除く。