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のプラグインがインストール済みのEclipse(Ganymede)で行った。
エンティティー
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>¥<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>