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

前回のログでは、Google App Engine/Java(GAE/J)上で、DWR2からデータストアを利用してみた。この際、最後の「Delete ALLファンクション(1トランザクションで全てのPersonエンティティーを削除する)」で失敗してしまった
これは、全てのPersonエンティティーを「Rootエンティティーとして登録してしまった」、つまり、「Personエンティティー全て異なったエンティティー・グループに属していた」ことが原因。
また、IDがシステムで勝手に採番されるのも、ちょっと気持ち悪い。

これを回避するために、前回のサンプルを以下のように改造する。

  • Rootエンティティーを設けて、全てのPersonエンティティーをその子孫として登録する。これにより、全てのPersonエンティティーは、Rootエンティティーの規定するエンティティー・グループに属することになる。
  • Rootエンティティーに、エンティティー総数をカウントするプロパティーを持たせ、それをもとに採番する。

画面は以下のように、前回と変わらない。

課題は、「いつRootエンティティーを登録するか」ということ。
ここでは、簡易にContextListner(の実験も兼ねて、それ)で行うことにした。このcontextInitializedメソッドは、

ローカルサーバー 開発プロジェクトをローカルサーバーにattachしたタイミング
GAE/J デプロイしたタイミング

で呼び出される。カウンターとPersonプロパティーのIDに一貫性を持たせた方が分かりやすいので、Personエンティティーもこのタイミングで初期化(登録があれば、全て削除)することとした。実際のアプリでは、これでは不味いので、管理画面のようなものが必要かもしれない。

以下の作業は、GAE/Jのプラグインがインストール済みのEclipseGanymede)で行った。

プロジェクトの作成

前回のサンプルのプロジェクトをコピーして、新規プロジェクトを作成する。この際、war/WEB-INFディレクトリ下のappengine-generatedディレクトリは削除する。

エンティティー

Rootエンティティーを新規に作成し、Personエンティティーも(その子孫になるように)変更する。


RootTO.java:Rootエンティティー。システムカウンター用のプロパティーを持つようにする。

package test.entities;

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

import com.google.appengine.api.datastore.Key;

/*********************************
 * 
 * エンティティーグループを定義するRootエンティティー
 * 
 * @author
 *     tetsuya_odaka (EzoGP) <br>
 *********************************/

@PersistenceCapable(identityType=IdentityType.APPLICATION)
public class RootTO {

    /*
     * rootエンティティーのキーはKeyか、String
     */
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;
    
    @Persistent
    private String name;
    
    @Persistent
    private Long count;

    /**
     * コンストラクタ
     * 
     */
    public RootTO(String name, Long count) {
        this.name = name;
        this.count = count;
    }

    public Key getKey() {
        return key;
    }

    public void setKey(Key key) {
        this.key = key;
    }

    public String getName() {
        return name;
    }

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

    public Long getCount() {
        return count;
    }

    public void setCount(Long count) {
        this.count = count;
    }
}


Person.java前回のサンプルでIDという名前だった主キーはkey(Key型)に、連番はnumber(Long型)に変更した。

package test.entities;

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;

import com.google.appengine.api.datastore.Key;

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

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private Long number;

    @Persistent
    private String name;

    @Persistent
    private String address;

    @Persistent
    private float salary;

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

    public Person(String name, String address, float salary) {
        this.name = name;
        this.address = address;
        this.salary = salary;
    }
    /**
     * Setter/Getter
     */

    public Key getKey() {
        return key;
    }

    public void setKey(Key key) {
        this.key = key;
    }
    
    public Long getNumber() {
        return number;
    }
    
    public void setNumber(Long number) {
        this.number = number;
    }

    public int getIntNumber() {
        return number.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.key.toString() + this.name + this.address+
                Float.toString(this.salary);
    }
}

People.javaの改造

エンティティーの追加と変更を受けて、以下のように変更した。ロギングはLog4Jで行っている。

package test;

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 com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

import test.entities.Person;
import test.entities.RootTO;

/**
 * Personの集合(HashMap)を表すオブジェクト。
 * DWRで利用する。
 * 
 */
public class People{
    private static final Log log = LogFactory.getLog(People.class);
    
    private Map<Integer,Person> people=null;

    /**
     * コンストラクタ
     */
    public People() {
        // Personの読み込み
        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("number asc");

        try{
            List<Person> list = (List<Person>)query.execute();
            for(Person person : list){
                people.put(person.getIntNumber(), 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) {
        
        log.debug("setPerson person id : " + person.getNumber());
        
        if(person.getNumber().intValue() != -1){
            this.updatePerson(person);
        }else{
            this.registerPerson(person);
        }
        // mapの再構築
        this.loadPeople();
        return;
    }

    /*
     *  Personを登録する。 
     */
    private void registerPerson(Person person) {
        
        PersistenceManager pm 
            = PMF.get().getPersistenceManager();
        long count = 0;
        try{
            pm.currentTransaction().begin();
            RootTO root = pm.getObjectById(RootTO.class, "key");
            count = root.getCount()+1;
            root.setCount(count);
            pm.currentTransaction().commit();
            log.debug("root count updated to: " + new Long(count).toString());
        }finally{
            if(pm.currentTransaction().isActive()){
                // ロールバック
                pm.currentTransaction().rollback();
            }
            pm.close();
        }

        if(count > 0){        
            Person storePer =
                new Person(person.getName(),
                            person.getAddress(),
                            person.getSalary());
         
            KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key");
            kb.addChild(Person.class.getSimpleName(), 
                    Person.class.getSimpleName()+ new Long(count).toString());
            Key key = kb.getKey();
               
            storePer.setKey(key);
            storePer.setNumber(count);
            
            pm = PMF.get().getPersistenceManager();
            try{
                pm.makePersistent(storePer);
                log.debug("person entered: "+storePer.toString());
            }finally{
                pm.close();
            }
        }
        return;
    }

    /*
     *  Personを更新する。 
     */
    private void updatePerson(Person person) {
        
        PersistenceManager pm 
            = PMF.get().getPersistenceManager();

        try{
            // キーの生成
            KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key");
            kb.addChild(Person.class.getSimpleName(), 
                Person.class.getSimpleName()+person.getNumber().toString());
            Key key = kb.getKey();

            // オブジェクトの取得
            Person storePer = pm.getObjectById(Person.class, key);
            storePer.setName(person.getName());
            storePer.setAddress(person.getAddress());
            storePer.setSalary(person.getSalary());
            log.debug("person updated: "+storePer.toString());
        }finally{
            pm.close();
        }
        return;
    }

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

        try{
            // キーの生成
            KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key");
            kb.addChild(Person.class.getSimpleName(), 
                Person.class.getSimpleName()+person.getNumber().toString());
            Key key = kb.getKey();

            // オブジェクトの取得
            Person storePer = pm.getObjectById(Person.class, key);
            pm.deletePersistent(storePer);
            log.debug("person deleted: "+key.toString());
        }finally{
            pm.close();
        }
        // mapの再構築
        this.loadPeople();
        return;
    }
    
    /**
     *  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();
        return;
    }
}

edit_table_demo.htmlの変更

ここまでの変更を、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">

<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.number);
      			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 deleteAllClicked() {
	dwr.engine.beginBatch();
	People.deleteAllPerson()
	fillTable();
	dwr.engine.endBatch();
	
}

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

function writePerson() {
	var person 
  		= {number: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({ number:-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="number">-1</span>)</small>
      <input type="button" value="Save" 
      				onclick="writePerson()"/>
      <input type="button" value="Clear" 
      				onclick="clearPerson()"/>
      <input type="button" value="All Delete" 
      				onclick="deleteAllClicked()"/>
   </td>
  </tr>
</table>

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

ContextListnerの作成

ContextListnerを作成して、RootエンティティーとPersonエンティティーの初期化を行う。

package test;

import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import test.entities.Person;
import test.entities.RootTO;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

public class CreateRootContextListner implements ServletContextListener {

    @Override
    public void contextDestroyed(ServletContextEvent arg0) {
    }

    /**
     * DataStoreの初期設定を行います。
     * 
     */
    @SuppressWarnings("unchecked")
    @Override
    public void contextInitialized(ServletContextEvent arg0) {
        /*
         * Rootエンティティーの初期設定
         *  カウンターを0にリセットします。
         */
        RootTO to = new RootTO("root",new Long(0));
        Key key =KeyFactory.createKey(RootTO.class.getSimpleName(), "key");
        to.setKey(key);
        
        PersistenceManager pm = PMF.get().getPersistenceManager();
        // rootエンティティーを登録
        try{
            pm.makePersistent(to);
        }finally{
            pm.close();
        }
        
        /*
         * Personエンティティーの初期設定
         *  Personエンティティーを全て削除します。
         */
        pm = PMF.get().getPersistenceManager();
        Query query = pm.newQuery(Person.class);
        try{
            List<Person> list = (List<Person>)query.execute();
            pm.deletePersistentAll(list);
        }finally{
            pm.close();
        }
        
        
    }
}

web.xmlの変更

ContextListnerを起動するために、Listner定義の最後に、以下の一文を追加する。

	<listener>
  		<listener-class>
    	test.CreateRootContextListner
  		</listener-class>
	</listener>

その他

dwr.xml、appengine-web.xml、beans.xmlは変更なし。

テストとデプロイ

ローカルサーバーで一通りの機能を動かす。画面右下の「All Delete」が動くことを確認する。


確認できたら、GAE/Jにデプロイする。