Google App Engine/JavaのData Storeを使ってみる
先のログでは、先走ってGoogle App Engine/Java(GAE/J)のデータストアにおけるカスケード・デリート(Cascade Delete。もしくは、リカーシブ・デリート。Recursive Delete)の問題を書いてしまったのだが、とりあえず、話を整理するために、順をおって、GAE/Jからデータストアを使ってみる。
試してみるのは、以下。
- エンティティー・グループを規定するRootエンティティーをストアする。
- このRootエンティティーのChildをストアする。
- キーによって、Childをフェッチ(取得)する。
- JDOQLによって、Childをフェッチする。
- Childを削除する。
- Rootを削除する。
GAE/Jのマニュアルによれば、親を定義しないエンティティーがRootエンティティーとなり、エンティティー・グループを規定する。ストアされたデータに対するトランザクション処理は、エンティティーグループの範囲内という制限があるので、複数のRootエンティティー(=複数のエンティティー・グループ)に対するトランザクションは定義できない。したがって、Rootは出来るだけ少なくする、というのがGAEを使う上での戦略として正しいのだろう。
また、クエリ(JDOQL)を発行する際には、インデックス定義が必要となるので「必ずローカルサーバーでテストせよ」と書かれている。ローカルサーバー(GAE/JのEclipseプラグインに付属)でテストすることによって、必要なインデックスが自動的に作成されるためである。このインデックスは、war下にappengine-generated/datastore-indexes-auto.xmlという名前で作成される。プロジェクトをデプロイする際に、このインデックスも一緒にデプロイされる。
正式な(??)インデックス定義は、WEB-INF下のdatastore-indexes.xmlとなるが、デフォルトの状態(プロジェクト作成時)では作成されない。これがない場合、appengine-generated/datastore-indexes-auto.xmlが自動的に作成される。したがって、データをストアを使う場合、ローカルサーバーでのテストが必須となる。
サンプルの概要
以下のような画面から、上の6点についてテストしてみる。
データストアを利用するコードは、(手数を減らすため)全て、サーブレット上に実装した。
(後から気がついたのだが、invokerを使わず、web.xmlにサーブレット定義を増やしていく方法は、更新の反映時にEclipseを再起動しなければならず開発の効率が悪い)。
プロジェクトの作成
Web Application Projectを新規に作成する。GWTは使わないので、チェックをはずす。
必要であれば、ロギングの定義を変えるなどする。
Rootエンティティーの登録
まず、Rootとするエンティティーを以下のようにtest.entitiesパッケージに作成した。
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エンティティー * * tetsuya_odaka (EzoGP) <br> *********************************/ @PersistenceCapable(identityType=IdentityType.APPLICATION) public class RootTO { /* * 主キー; * (メモ)親エンティティーのキーはKeyか、String。 */ @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key parentKey; @Persistent private String firstName; @Persistent private String lastName; /** * コンストラクタ */ public RootTO(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } /** * setter/getter */ public Key getParentKey() { return parentKey; } public void setParentKey(Key parentKey) { this.parentKey = parentKey; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
そして、これを登録するサーブレットは以下。
package test; import java.io.IOException; import java.io.PrintWriter; import javax.jdo.PersistenceManager; import javax.servlet.http.*; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import test.entities.RootTO; @SuppressWarnings("serial") public class GaeDataStoreCreateParentServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { RootTO to = new RootTO("tetsuya","odaka"); /* * keyを生成 */ // 「文字」を種(たね)にするときは、数字が先頭だと怒られる。 Key key =KeyFactory.createKey(RootTO.class.getSimpleName(), "parentKey"); to.setParentKey(key); PersistenceManager pm = PMF.get().getPersistenceManager(); // 親エンティティーを登録 boolean flg = false; try{ pm.makePersistent(to); flg = true; }finally{ pm.close(); } resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>Parentエンティティーの登録</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>Parentエンティティーの登録</h2>"); if(flg){ out.println("Parentエンティティーを登録しました。"); }else{ out.println("Parentエンティティーに失敗しました。"); } out.println("</body></html>"); } }
ストアの操作には、PersistenceManagerのインスタンスが必要となる。
この目的で、マニュアルに従い、シングルトンにして、スタティック呼び出しでインスタンスの取得が出来るクラスを(PMF)作成する。
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; } }
Childエンティティーの登録
上で登録したRootエンティティーの子として、以下のエンティティー(Friends.java)を登録する。実験的に、Rootエンティティー(子から見た親、祖先)のキーをプロパティーに持たせた。
package test.entities; import java.util.Date; 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 javax.jdo.annotations.Extension; import com.google.appengine.api.datastore.Key; /********************************* * * Childエンティティー * * tetsuya_odaka (EzoGP) <br> *********************************/ @PersistenceCapable(identityType=IdentityType.APPLICATION) public class Friends { // 主キー @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; // 親キー(@Extensionを使って自動でセットする) @Persistent @Extension(vendorName="datanucleus",key="gae.parent-pk",value="true") private Key parentKey; @Persistent private String firstName; @Persistent private String lastName; @Persistent private Date entryDate; /** * コンストラクタ * */ public Friends(String firstName, String lastName,Date entryDate) { this.firstName = firstName; this.lastName = lastName; this.entryDate = entryDate; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public Date getEntryDate() { return entryDate; } public void setEntryDate(Date entryDate) { this.entryDate = entryDate; } public Key getParentKey() { return parentKey; } public void setParentKey(Key parentKey) { this.parentKey = parentKey; } }
次は、これをストアするサーブレット。JDOQLの実験をしたいので、3つのエンティティーをストアする。試しに日本語も(エンコーディングを変えることなく)ストアしてみた。
この際、それぞれのエンティティーは、「種類(Kind)=Friends」という括りでストアされる。先のログで「種類=テーブル」と呼んだのは、このためである。
主キーはRootエンティティーからのパス。また、Friends.javaに定義した親エンティティーのキー(プロパティー;parentKey)は、
@Extension(vendorName="datanucleus",key="gae.parent-pk",value="true")
のアノテーションを使うことで、プログラムで明示的にセットする必要はない。
以下が、サーブレットである。
package test; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import javax.jdo.PersistenceManager; import javax.servlet.http.*; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import test.entities.Friends; import test.entities.RootTO; @SuppressWarnings("serial") public class GaeDataStoreCreateChildrenServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { /** * エンティティの生成 */ Friends child1 = new Friends("jiro","suzuki",new Date()); Friends child2 = new Friends("taro","sato",new Date()); // 日本語のテスト Friends child3 = new Friends("三郎","山田",new Date()); // キーの生成(1) KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend1"); Key key1 = kb.getKey(); // キーのセット(1) child1.setKey(key1); // キーの生成(2) kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend2"); Key key2 = kb.getKey(); // キーのセット(2) child2.setKey(key2); // キーの生成(3) kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend3"); Key key3 = kb.getKey(); // キーのセット(3) child3.setKey(key3); boolean flag=false; PersistenceManager pm = PMF.get().getPersistenceManager(); try{ // (注記)parentKeyは自動でセットされる pm.makePersistent(child1); pm.makePersistent(child2); pm.makePersistent(child3); flag=true; }finally{ pm.close(); } resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); // 出力部 out.println("<html><head>"); out.println("<title>Childエンティティーの登録</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>Childエンティティーの登録</h2>"); if(flag){ out.println("Childエンティティーを3件、登録しました。"); }else{ out.println("Childエンティティーの登録に失敗しました。"); } out.println("</body></html>"); } }
Childエンティティーをキーを元に取得する。
上で登録したFriends.javaを、主キーを生成して取得する。
主キーは、親(Rootエンティティー)からのパスで表現されるKeyオブジェクトとなるので、KeyFactory.Builderで生成する。
サーブレットとしては、以下を用意した。
package test; import java.io.IOException; import java.io.PrintWriter; import javax.jdo.PersistenceManager; import javax.servlet.http.*; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import test.entities.Friends; import test.entities.RootTO; @SuppressWarnings("serial") public class GaeDataStoreGetChildServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { PersistenceManager pm = PMF.get().getPersistenceManager(); /** * Childのキーの生成 * Parent(Root)エンティティーからのパスを含むKeyオブジェクトを生成する */ KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend2"); Key key = kb.getKey(); // Childエンティティーの取得 Friends friend = pm.getObjectById(Friends.class, key); String firstName = friend.getFirstName(); String lastName = friend.getLastName(); String keyString = friend.getKey().toString(); String parentKeyString = friend.getParentKey().toString(); pm.close(); resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>Childエンティティーの取得</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>キーによるChildエンティティーの取得</h2>"); out.println("<table border=1>"); out.println("<tr><td>Childエンティティー</td><td>"+Friends.class.getSimpleName()+"</td></tr>"); out.println("<tr><td>主キー</td><td>"+keyString+"</td></tr>"); out.println("<tr><td>親キー</td><td>"+parentKeyString+"</td></tr>"); out.println("<tr><td>名</td><td>"+firstName+"</td></tr>"); out.println("<tr><td>姓</td><td>"+lastName+"</td></tr>"); out.println("</table></body></html>"); } }
この出力結果は以下となる。主キー、親の主キーの形式が確認できる。
JDOQLによるChildの取得
ストアした3つのFriendsエンティティーをJDOQLで取得する。以下は、そのサーブレット。
query.execute()で、Friendsエンティティーのリストが取得できるので、それを拡張for文で取り出して、結果を表形式で出力する。
package test; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import javax.jdo.PersistenceManager; import javax.jdo.Query; import javax.servlet.http.*; import test.entities.Friends; @SuppressWarnings("serial") public class GaeDataStoreQueryServlet extends HttpServlet { @SuppressWarnings("unchecked") public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { PersistenceManager pm = PMF.get().getPersistenceManager(); /** * JDOQLクエリの発行 * firstNameプロパティーの昇順 */ Query query = pm.newQuery(Friends.class); query.setOrdering("firstName asc"); resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>Childエンティティーのクエリー</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>クエリーによるChildエンティティーの取得</h2>"); try{ List<Friends> list = (List<Friends>)query.execute(); for(Friends friend : list){ String firstName = friend.getFirstName(); String lastName = friend.getLastName(); String keyString = friend.getKey().toString(); String parentKeyString = friend.getParentKey().toString(); out.println("<table border=1>"); out.println("<tr><td>Childエンティティー</td><td>"+Friends.class.getSimpleName()+"</td></tr>"); out.println("<tr><td>主キー</td><td>"+keyString+"</td></tr>"); out.println("<tr><td>親キー</td><td>"+parentKeyString+"</td></tr>"); out.println("<tr><td>名</td><td>"+firstName+"</td></tr>"); out.println("<tr><td>姓</td><td>"+lastName+"</td></tr>"); out.println("</table></br>"); } }finally{ query.closeAll(); pm.close(); } out.println("</body></html>"); } }
結果は、以下の画面となる。
ストアした日本語もエンコードを気にすることなく、復元できている。
トランザクションによるchildエンティティーの削除
上でストアした3つのエンティティーは、主キーのパスが示すとおり、Rootエンティティーが規定するエンティティー・グループに属する。
したがって、1つのトランザクションで取得、削除とった行為が可能となる。
以下は、これを行うサーブレット。
package test; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import javax.jdo.PersistenceManager; import javax.jdo.Query; import javax.servlet.http.*; import test.entities.Friends; @SuppressWarnings("serial") public class GaeDataStoreTransactionServlet extends HttpServlet { @SuppressWarnings("unchecked") public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>トランザクション</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>トランザクションによるchildエンティティーの削除</h2>"); PersistenceManager pm = PMF.get().getPersistenceManager(); /** * Queryオブジェクトの生成 */ Query query = pm.newQuery(Friends.class); query.setOrdering("firstName asc"); try{ // トランザクションの開始 pm.currentTransaction().begin(); List<Friends> list = (List<Friends>)query.execute(); for(Friends friend : list){ String keyString = friend.getKey().toString(); String parentKeyString = friend.getParentKey().toString(); out.println("<table border=1>"); out.println("<tr><td>childエンティティー</td><td>"+Friends.class.getSimpleName()+"</td></tr>"); out.println("<tr><td>主キー</td><td>"+keyString+"</td></tr>"); out.println("<tr><td>親キー</td><td>"+parentKeyString+"</td></tr>"); out.println("<tr><td colspan=2>削除しました</td>"); out.println("</table></br>"); // childエンティティーの削除 pm.deletePersistent(friend); } // 全部削除できたらコミット pm.currentTransaction().commit(); }finally{ if(pm.currentTransaction().isActive()){ // ロールバック pm.currentTransaction().rollback(); } pm.close(); } out.println("</body></html>"); } }
Rootエンティティーの削除
以下は、ストアしたRootエンティティーを削除するサーブレット。ここでは、トランザクションを使わずに処理を行う(自動コミットとなる)。
package test; import java.io.IOException; import java.io.PrintWriter; import javax.jdo.PersistenceManager; import javax.servlet.http.*; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import test.entities.RootTO; @SuppressWarnings("serial") public class GaeDataStoreDeleteParentServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { PersistenceManager pm = PMF.get().getPersistenceManager(); // Rootエンティティーの取得 Key key = KeyFactory.createKey(RootTO.class.getSimpleName(), "parentKey"); RootTO to = pm.getObjectById(RootTO.class, key); // Rootエンティティーの削除 try{ pm.deletePersistent(to); }finally{ pm.close(); } resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>Parentエンティティーの削除</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>Parentエンティティーの削除</h2>"); out.println("Parentエンティティーを削除しました。"); out.println("</body></html>"); } }
web.xml
以上のサーブレットをwar/WEB-INF/web.xmlに定義する。
<?xml version="1.0" encoding="utf-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <!--// Parentエンティティーのストア --> <servlet> <servlet-name>GaeDataStoreCreateParent</servlet-name> <servlet-class>test.GaeDataStoreCreateParentServlet</servlet-class> </servlet> <!--// Parentエンティティーの削除 --> <servlet> <servlet-name>GaeDataStoreDeleteParent</servlet-name> <servlet-class>test.GaeDataStoreDeleteParentServlet</servlet-class> </servlet> <!--// Childエンティティーのストア --> <servlet> <servlet-name>GaeDataStoreCreateChidren</servlet-name> <servlet-class>test.GaeDataStoreCreateChildrenServlet</servlet-class> </servlet> <!--// Childエンティティーをキーで取得 --> <servlet> <servlet-name>GaeDataStoreGetChild</servlet-name> <servlet-class>test.GaeDataStoreGetChildServlet</servlet-class> </servlet> <!--// Childエンティティーをクエリで取得 --> <servlet> <servlet-name>GaeDataStoreQuery</servlet-name> <servlet-class>test.GaeDataStoreQueryServlet</servlet-class> </servlet> <!--// Childエンティティーをトランザクションで削除 --> <servlet> <servlet-name>GaeDataStoreTransaction</servlet-name> <servlet-class>test.GaeDataStoreTransactionServlet</servlet-class> </servlet> <!--// 以下はサーブレット・マッピング --> <servlet-mapping> <servlet-name>GaeDataStoreCreateParent</servlet-name> <url-pattern>/createparent</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>GaeDataStoreDeleteParent</servlet-name> <url-pattern>/deleteparent</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>GaeDataStoreCreateChidren</servlet-name> <url-pattern>/createchidren</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>GaeDataStoreGetChild</servlet-name> <url-pattern>/getchild</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>GaeDataStoreQuery</servlet-name> <url-pattern>/query</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>GaeDataStoreTransaction</servlet-name> <url-pattern>/transaction</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>
index.html
処理の起点となるindex.htmlを以下にしめす。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <!-- The HTML 4.01 Transitional DOCTYPE declaration--> <!-- above set at the top of the file will set --> <!-- the browser's rendering engine into --> <!-- "Quirks Mode". Replacing this declaration --> <!-- with a "Standards Mode" doctype is supported, --> <!-- but may lead to some differences in layout. --> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title>GAE:データストアのテスト</title> </head> <body> <h2>GAE/Jのデータストアの基本的なテスト</h2> <table> <tr> <td><a href="createparent"/>parent(root)エンティティーをストアします</td> </tr> <tr> <td><a href="createchidren"/>3つのchildエンティティーをストアします</td> </tr> <tr> <td><a href="getchild"/>childエンティティーをキーを元に取得します</td> </tr> <tr> <td><a href="query"/>全てのchildエンティティーをJDOQLのクエリで取得します</td> </tr> </table> <br><br> <table> <tr> <td><a href="transaction"/>トランザクションを使って全てのchildエンティティーを削除します</td> </tr> <tr> <td><a href="deleteparent"/>parent(root)エンティティーを削除します</td> </tr> </table> </body> </html>
ローカル環境でのテスト
先に述べたように、データストアを利用する際にはローカルサーバーでテストを行うのが無難。
ローカルサーバーでストアのテストをすると、war/WEB-INF下にappengine-generatedディレクトリが作成されて、
ができる。これらは、ローカル環境で削除&再作成が自由にできる。以下は、ここまでのディレクトリ構成のスクリーンショット。
デプロイと管理コンソール
ここまで準備ができれば、GAE環境にデプロイすればよい。
GAEにデプロイすると、管理コンソールでindexの状況をみたり、ストアされたエンティティーを更新したりすることができるようになる。(管理コンソールの左ペインの、「Data Store」下にある「indexes」、「Data Viewer」がそれである。
GAE/Jのインデックスの更新は、逐次的に行われる。デプロイ後、インデックスのStatusが、全て「Serverd」となれば、Indexesが使えることになる。
また、Data Viewerは、ストアの状況が反映されるまでに、しばらく時間がかかる(この例では5分位だった)。なので、ストアしたデータが現れない場合、時間をおいてみる必要がある。
以下は、Data Viewerのスクリーンショット。プロパティーにセットした日本語も表示される。