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する。