GAE/JでSerializableなクラスを永続化してみる(落とし穴注意)
ここにあるように、GAE/JではSeriarizableなクラスをBLOBとして永続化することができます。
JDO準拠なので、POJOなSerializableを永続化できる! スゲェ! Google様一生ついて行きます! って思ってたんですが、要注意物件でした。
まず、Serializableなフィールドに@Persistentのアノテーションをつけるときに、defaultFetchGroup = "true"とする必要があります。
@Persistent(serialized = "true", defaultFetchGroup = "true") public HashMap<String, Serializable> prop = new HashMap<String, Serializable>();
これをつけないと、PersistenceManager#getObjectByIdでオブジェクトを取得したときに、Serializableなフィールドがnullのままになってしまいます(c.f.
getObjectById Does Not Load Serialized Classes )。
次に、Serializableなフィールドは更新ができません。一旦永続化したら、Serializableフィールドを更新するためには全く新しいインスタンスをフィールドに割り当てる必要があります。
例えば、以下のようなクラスがあったとして、
class EntityData { @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) @PrimaryKey public Long id; @Persistent(serialized = "true", defaultFetchGroup = "true") public HashMap<String, Serializable> prop = new HashMap<String, Serializable>(); public void setProp(HashMap<String, Serializable> prop) { this.prop = prop; } public HashMap<String, Serializable> getProp() { return prop; } }
以下のように一旦作って永続化します。
PersistenceManager pm = ....; EntityData ed = new EntityData(); ed.setProp("nyara", "cat"); pm.makePersistent(ed); pm.close();
このとき、以下のようにして更新をかけても無効です。
PersistenceManager pm = ....; EntityData ed = (getExtentやQuery等、何らかの手段で先ほど永続したインスタンスを入手); ed.getProp().put("nyara", "is my cat"); pm.close();
以下のようにして、Serializableインスタンスに新しいインスタンスを割り当てる必要有りです。
PersistenceManager pm = ....; EntityData ed = (getExtentやQuery等、何らかの手段で先ほど永続したインスタンスを入手); ed. ed.setProp(new HashMap()); ed.getProp().put("nyara", "is my cat"); pm.close();
まあ考えてみれば当然かも知れません。直接フィールドを書き換えられたら、永続化し直すべきか否か判断できないですよね。
安全のためには、PersistenceMangerで永続化するSerializableクラスのインスタンスは、不変オブジェクトにしておくのが良いと思いますです。
そういや、ネイティブにサポートされてる永続化可能な型は全部不変なクラスだなあ……。
Joel本の記述を思い出してしまった、そんな一週間でした。抽象化怖い!
以下、検証用のテストケースです。
package jp.ne.hatena.matsuza.entities; import java.io.File; import java.io.Serializable; import java.util.HashMap; import javax.jdo.Extent; import javax.jdo.PersistenceManager; 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 jp.ne.hatena.matsuza.PMF; import jp.ne.hatena.matsuza.TestEnvironment; import junit.framework.TestCase; import org.junit.Test; import com.google.appengine.api.datastore.dev.LocalDatastoreService; import com.google.appengine.tools.development.ApiProxyLocalImpl; import com.google.apphosting.api.ApiProxy; @PersistenceCapable(identityType = IdentityType.APPLICATION) class EntityData { @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) @PrimaryKey public Long id; @Persistent(serialized = "true", defaultFetchGroup = "true") public HashMap<String, Serializable> prop = new HashMap<String, Serializable>(); public void setProp(HashMap<String, Serializable> prop) { this.prop = prop; } public HashMap<String, Serializable> getProp() { return prop; } } public class SerializableTest extends TestCase { @Override protected void setUp() throws Exception { super.setUp(); ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment()); ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")) { }); ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate(); proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY, Boolean.TRUE.toString()); } @Override protected void tearDown() throws Exception { ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate(); LocalDatastoreService datastoreService = (LocalDatastoreService) proxy .getService("datastore_v3"); datastoreService.clearProfiles(); super.tearDown(); } @SuppressWarnings("unchecked") @Test public void testPersistent() { { PersistenceManager pm = PMF.get().getPersistenceManager(); EntityData e = new EntityData(); // Serializableにデータを入れて、保存 e.getProp().put("key", "1"); pm.makePersistent(e); pm.close(); } { PersistenceManager pm = PMF.get().getPersistenceManager(); Extent<EntityData> ext = pm.getExtent(EntityData.class); EntityData e = ext.iterator().next(); // Serializableを取り出す。入れたデータが入っていることが確認できる。 assertEquals("1", e.getProp().get("key")); // Serializableインスタンスの中身を変更して保存 e.getProp().put("key", "2"); pm.close(); } { PersistenceManager pm = PMF.get().getPersistenceManager(); Extent<EntityData> ext = pm.getExtent(EntityData.class); EntityData e = ext.iterator().next(); // 変更内容が保存されていない // assertEquals("2", e.getProp().get("key")); assertEquals("1", e.getProp().get("key")); e.getProp().put("key", "3"); // setterに明示的にアクセスすれば永続化されるのかな? // 保存していたSerializableインスタンスをsetterに渡す。 e.setProp(e.getProp()); pm.close(); } { PersistenceManager pm = PMF.get().getPersistenceManager(); Extent<EntityData> ext = pm.getExtent(EntityData.class); EntityData e = ext.iterator().next(); // 変更内容が保存されていない // 多分、オブジェクトIDが等しいものがsetterでセットされても、永続化対象にならないのだろう // assertEquals("3", e.getProp().get("key")); assertEquals("1", e.getProp().get("key")); // Serializableインスタンスをclone()して、cloneされた新インスタンスを保存 HashMap<String, Serializable> newMap = (HashMap<String, Serializable>) e .getProp().clone(); newMap.put("key", "4"); e.setProp(newMap); pm.close(); } { PersistenceManager pm = PMF.get().getPersistenceManager(); Extent<EntityData> ext = pm.getExtent(EntityData.class); EntityData e = ext.iterator().next(); // 永続化できた! assertEquals("4", e.getProp().get("key")); } } }