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
<?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>¥<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エンティティー(=全てのエンティティーがそれぞれ異なったエンティティー・グループに属す)となっていることを意味している。
次回のログでは、この例外を取り除く。