Google Apps Engine/JavaのData Storeで非所有関係を作ってみる(1:1)
GAE/Jのデータストアのマニュアルによれば、非所有関係はサポートしていないので、(プロパティーに)外部キーのようにキーを保管して、アプリケーションでハンドルせよ、と書いてある(これ)。それはそれでいいので、サンプルを作ってみた。
前々回のログで作ったサンプルを元にして改造をする。以下の画面から、
- Googleのマニュアルに従い、Friendsのプロパティーとして、FriendsInfoの主キーを(外部キーとして)持たせる。
- FriendsエンティティーとFriendsInfoエンティティーを、Rootからみて同一階層にストアする。
を行い、以下をテストする。
- Friendsを主キーをストアから復元し、外部キーを辿って、FriendsInfoのプロパティーを検索できること。
- FriendsとFriendsInfoがカスケード削除されないこと
- FriendsとFriendsInfoが1つのトランザクションで削除できること。
プロジェクトの作成
前々回のサンプルと殆ど同じ(サーブレット数と名前の変更をしない)ので、Eclipse上でプロジェクトをコピーしてしまう。
バージョンを変えたければ、プロジェクトを選択し、「Google」=>「Apps Engine Settings」でバージョンを変更する。
エンティティーの変更
前回のサンプルで作った3つのエンティティー(RootTO.java、Friends.java、FriendsInfo.java)のうち、Friends.javaとFriendsInfo.javaを変更する。
Friends.java: FriendsInfoエンティティーの主キーを「外部キー」的にもたせるよう、プロパティーを追加する(Key fireignKey)。
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; /********************************* * * エンティティー * * @author * tetsuya_odaka (EzoGP) <br> *********************************/ @PersistenceCapable(identityType=IdentityType.APPLICATION) public class Friends { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent @Extension(vendorName="datanucleus",key="gae.parent-pk",value="true") private Key parentKey; @Persistent private String firstName; @Persistent private String lastName; @Persistent private Key foreignKey; @Persistent private Date entryDate; /** * コンストラクタ * */ public Friends(String firstName, String lastName,Date entryDate) { this.firstName = firstName; this.lastName = lastName; this.entryDate = entryDate; } /** * setter/getter */ 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 Date getEntryDate() { return entryDate; } public void setEntryDate(Date entryDate) { this.entryDate = entryDate; } public Key getForeignKey() { return foreignKey; } public void setForeignKey(Key foreignKey) { this.foreignKey = foreignKey; } public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public Key getParentKey() { return parentKey; } public void setParentKey(Key parentKey) { this.parentKey = parentKey; } }
FriendsInfo.java: 変更不要と思っていたが、以下のように定義されたフィールドを持つと、削除時に「Attempt was made to modify the primary key of an object of type .... identified by key ..... Primary keys are immutable.」という例外が出てしまう(参考)。
@Persistent @Extension(vendorName="datanucleus",key="gae.parent-pk",value="true")
なので、これの定義をはずす。(結果として、parentKeyはnullとなる)
package test.entities; import java.util.Date; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PrimaryKey; import javax.jdo.annotations.Persistent; import com.google.appengine.api.datastore.Key; /********************************* * * エンティティー * * tetsuya_odaka (EzoGP) <br> *********************************/ @PersistenceCapable(identityType=IdentityType.APPLICATION) public class FriendsInfo { @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent // nullとなる。 private Key parentKey; @Persistent private String attrName; @Persistent private Date entryDate; /** * コンストラクタ * */ public FriendsInfo(String attrName ,Date entryDate) { this.attrName = attrName; this.entryDate = entryDate; } /** * setter/getter */ public String getAttrName() { return attrName; } public void setAttrName(String attrName) { this.attrName = attrName; } public Date getEntryDate() { return entryDate; } public void setEntryDate(Date entryDate) { this.entryDate = entryDate; } public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public Key getParentKey() { return parentKey; } public void setParentKey(Key parentKey) { this.parentKey = parentKey; } }
FriendsエンティティーとFriendsInfoエンティティーのストア
上記の変更にあわせて、2つのエンティティーのストア方法を変更する。
非所有関係なので、Rootエンティティーの下位に、それぞれのエンティティーを個別にストアする。この際、FriendsエンティティーのKey foreignKeyプロパティーには、FriendsInfoエンティティーの主キーをセットする。
GaeDataStoreCreateChildrenServlet.java
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.FriendsInfo; import test.entities.RootTO; @SuppressWarnings("serial") public class GaeDataStoreCreateChildrenServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { /** * 所有されるエンティティの生成 */ FriendsInfo fi1 = new FriendsInfo("属性1",new Date()); FriendsInfo fi2 = new FriendsInfo("属性2",new Date()); FriendsInfo fi3 = new FriendsInfo("属性3",new Date()); // FriendsInfoキーの生成(1) KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(FriendsInfo.class.getSimpleName(), "info1"); Key key1 = kb.getKey(); // FriendsInfoキーのセット(1) fi1.setKey(key1); // FriendsInfoキーの生成(2) kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(FriendsInfo.class.getSimpleName(), "info2"); Key key2 = kb.getKey(); // FriendsInfoキーのセット(2) fi2.setKey(key2); // FriendsInfoキーの生成(3) kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(FriendsInfo.class.getSimpleName(), "info3"); Key key3 = kb.getKey(); // FriendsInfoキーのセット(3) fi3.setKey(key3); PersistenceManager pm = PMF.get().getPersistenceManager(); /** * 所有する側のエンティティの生成 */ Friends child1 = new Friends("次郎","鈴木",new Date()); Friends child2 = new Friends("太郎","佐藤",new Date()); Friends child3 = new Friends("三郎","山田",new Date()); // キーの生成(1) kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend1"); Key key4 = kb.getKey(); // キーのセット(1) child1.setKey(key4); // キーの生成(2) kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend2"); Key key5 = kb.getKey(); // キーのセット(2) child2.setKey(key5); // キーの生成(3) kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend3"); Key key6 = kb.getKey(); // 子のキーのセット(3) child3.setKey(key6); // FriendsInfoの主キーのセット child1.setForeignKey(key1); child2.setForeignKey(key2); child3.setForeignKey(key3); boolean flag=false; try{ pm.makePersistent(fi1); pm.makePersistent(fi2); pm.makePersistent(fi3); 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>Friends,FriendsInfoエンティティーの登録</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>Friends,FriendsInfoエンティティーの登録</h2>"); if(flag){ out.println("Friends,FriendsInfoエンティティーを登録しました。"); }else{ out.println("Friends,FriendsInfoエンティティーの登録に失敗しました。"); } out.println("</body></html>"); } }
主キーによるFriendsエンティティーの取得
主キーを生成して、Friendsエンティティーを取得し、foreignKeyプロパティーからFriendsInfoエンティティーを手繰る。
GaeDataStoreGetFriendsServlet.java
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.FriendsInfo; import test.entities.RootTO; @SuppressWarnings("serial") public class GaeDataStoreGetFriendsServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { PersistenceManager pm = PMF.get().getPersistenceManager(); // Friendsの主キーの生成 KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(Friends.class.getSimpleName(), "friend2"); Key key = kb.getKey(); // Friendsオブジェクトの取得 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(); Key foreignKey = friend.getForeignKey(); // FriendsInfoオブジェクトの取得 FriendsInfo friendsInfo = pm.getObjectById(FriendsInfo.class, foreignKey); String attrName = friendsInfo.getAttrName(); pm.close(); resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>Friendsエンティティーの取得</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>キーによるFriendsエンティティーの取得</h2>"); out.println("<table border=1>"); out.println("<tr><td>Friendsエンティティー</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("<tr><td>属性名</td><td>"+attrName+"</td></tr>"); out.println("</table></body></html>"); } }
デプロイ後、以下の画面となる。
主キーによるFriendsInfoエンティティーの取得
主キーを生成して、FriendsInfoエンティティーを取得する。
GaeDataStoreGetFriendsInfoServlet.java
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.FriendsInfo; import test.entities.RootTO; @SuppressWarnings("serial") public class GaeDataStoreGetFriendsInfoServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { PersistenceManager pm = PMF.get().getPersistenceManager(); // キーの生成 KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "parentKey"); kb.addChild(FriendsInfo.class.getSimpleName(), "info2"); Key key = kb.getKey(); // オブジェクトの取得 FriendsInfo friendInfo = pm.getObjectById(FriendsInfo.class, key); String attrName = friendInfo.getAttrName(); String keyString = friendInfo.getKey().toString(); pm.close(); resp.setContentType("text/html; charset=utf-8"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>FriendsInfoエンティティーの取得</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>キーによるFriendsInfoエンティティーの取得</h2>"); out.println("<table border=1>"); out.println("<tr><td>FriendsInfoエンティティー</td><td>"+FriendsInfo.class.getSimpleName()+"</td></tr>"); out.println("<tr><td>主キー</td><td>"+keyString+"</td></tr>"); out.println("<tr><td>属性名</td><td>"+attrName+"</td></tr>"); out.println("</table></body></html>"); } }
JDOQLによるFriendsとFriendsInfoの取得。
FriendsエンティティーのリストをQueryで取得したら、foreignKeyプロパティーからFriendsInfoエンティティーを手繰る。
GaeDataStoreQueryServlet.java
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; import test.entities.FriendsInfo; @SuppressWarnings("serial") public class GaeDataStoreQueryServlet 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>Friends,FriendsInfoエンティティーのクエリー</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>クエリーによるFriends,FriendsInfoエンティティーの取得</h2>"); out.println("<h3>Friendsエンティティー</h3>"); PersistenceManager pm = PMF.get().getPersistenceManager(); /** * Friendsエンティティーに対するクエリの実行 */ Query query = pm.newQuery(Friends.class); query.setOrdering("firstName asc"); try{ List<Friends> list = (List<Friends>)query.execute(); for(Friends friend : list){ String firstName = friend.getFirstName(); String lastName = friend.getLastName(); String attrName = friend.getFriendsInfo().getAttrName(); String keyString = friend.getKey().toString(); String parentKeyString = friend.getParentKey().toString(); out.println("<table border=1>"); out.println("<tr><td>Friendsエンティティー</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("<tr><td>属性名</td><td>"+attrName+"</td></tr>"); out.println("</table></br>"); } }finally{ query.closeAll(); } out.println("<br><br>"); out.println("<h3>FriendsInfoエンティティー</h3>"); /** * FriendsInfoエンティティーに対するクエリの実行 */ query = pm.newQuery(FriendsInfo.class); try{ List<FriendsInfo> list = (List<FriendsInfo>)query.execute(); for(FriendsInfo friendsInfo : list){ String keyString = friendsInfo.getKey().toString(); String attrName = friendsInfo.getAttrName(); out.println("<table border=1>"); out.println("<tr><td>FriendsInfoエンティティー</td><td>"+FriendsInfo.class.getSimpleName()+"</td></tr>"); out.println("<tr><td>主キー</td><td>"+keyString+"</td></tr>"); out.println("<tr><td>属性名</td><td>"+attrName+"</td></tr>"); out.println("</table></br>"); } }finally{ query.closeAll(); pm.close(); } out.println("</body></html>"); } }
デプロイ後、以下の画面となる。
Friendsエンティティーの削除(カスケード削除)
「カスケード削除されないこと」を確認する。
GaeDataStoreCascadeDaleteServlet.java
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 GaeDataStoreCascadeDaleteServlet 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>エンティティーのカスケードデリート</h2>"); PersistenceManager pm = PMF.get().getPersistenceManager(); Query query = pm.newQuery(Friends.class); try{ List<Friends> list = (List<Friends>)query.execute(); pm.deletePersistentAll(list); }finally{ query.closeAll(); pm.close(); } out.println("</body></html>"); } }
トランザクションで、Friendsエンティティーと(それに紐づく)FriendsInfoエンティティーを削除する
FriendsエンティティとFriendsInfoエンティティは、同一のエンティティー・グループに属しているので、1つのトランザクションで削除できることを確認する。
GaeDataStoreTransactionServlet.java
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 com.google.appengine.api.datastore.Key; import test.entities.Friends; import test.entities.FriendsInfo; @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>トランザクションによるFriends,FriendsInfoエンティティーの削除</h2>"); PersistenceManager pm = PMF.get().getPersistenceManager(); Query query = pm.newQuery(Friends.class); try{ // トランザクションの開始 pm.currentTransaction().begin(); List<Friends> list = (List<Friends>)query.execute(); for(Friends friend : list){ Key foreignKey = friend.getForeignKey(); // FriendsInfoオブジェクトの取得 FriendsInfo friendsInfo = pm.getObjectById(FriendsInfo.class, foreignKey); // FriendsInfoの削除 pm.deletePersistent(friendsInfo); // Friendsの削除 pm.deletePersistent(friend); } // 全部削除できたらコミット pm.currentTransaction().commit(); out.println("処理が正常に終了しました。"); }finally{ if(pm.currentTransaction().isActive()){ // ロールバック pm.currentTransaction().rollback(); out.println("処理が異常終了しました。"); } query.closeAll(); pm.close(); } out.println("</body></html>"); } }
テストとデプロイ
ローカル環境でテストしたら、いつも通りにGAEにupする。