GAE/Jでロールバックを実装する(2)#appengine
前回はJDOでしたが、slim3がJDOではなくなったので新しいDatastore版を作りました。前回と異なるのは保存元のモデルをEntityGroupのルートエンティティにしてBackupをEntityGroupに加えたことです。以前まではEntityGroupの考え方がわかっておらず、こんな時にEntityGroupは使わないのだろうと思っていたのですが、EntityGroupはRDBの関連ではなくKeyで構成されていることからEntityGroupに含めてもいいという考えに至りました。
Keyで構成されているとはどういうことかは下記資料の18P〜22Pをご覧頂くのがいいと思います。
appengine java night #1
View more documents from Shinichi Ogawa.
Backup.class
import java.io.Serializable; import org.slim3.datastore.Attribute; import org.slim3.datastore.Datastore; import org.slim3.datastore.Model; import org.slim3.datastore.ModelMeta; import org.slim3.util.BeanDesc; import org.slim3.util.BeanUtil; import org.slim3.util.ClassUtil; import org.slim3.util.PropertyDesc; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Transaction; @Model public class Backup implements Serializable { private static final long serialVersionUID = 1L; @Attribute(primaryKey = true) private Key key; @Attribute(version = true) private Long version; private Long schemaVersion = new Long(0); private String modelName; private Key modelKey; private Long modelVersion; @Attribute(lob = true) private Object modelObject; // (略) getter, setter public static Key backup(Transaction tx, Object model) { BeanDesc desc = BeanDesc.create(model.getClass()); PropertyDesc keyProp = desc.getPropertyDesc("key"); PropertyDesc versionProp = desc.getPropertyDesc("version"); Key modelKey = (Key) keyProp.getValue(model); Backup bk = new Backup(); bk.setKey(Datastore.allocateId(modelKey, Backup.class)); bk.setModelName(model.getClass().getSimpleName()); bk.setModelKey(modelKey); bk.setModelVersion((Long) versionProp.getValue(model)); bk.setModelObject(model); Datastore.put(tx, bk); return bk.getKey(); } @SuppressWarnings("unchecked") public static <T> T get(Class<T> clazz, Key key, Long version) { BackupMeta m = new BackupMeta(); Backup bk = Datastore.query(m).filter( m.modelName.equal(clazz.getSimpleName()), m.modelKey.equal(key), m.modelVersion.equal(version)).asSingle(); return (T)bk.getModelObject(); } @SuppressWarnings("unchecked") public static <T> T rollback(Class<T> clazz, Key key, Long version) { T oldModel = get(clazz, key, version); BeanDesc desc = BeanDesc.create(oldModel.getClass()); PropertyDesc keyProp = desc.getPropertyDesc("key"); // 追記:slim3の最新にはDatastore.get(Class<M> modelClass, Key key)がありました。こっちを使った方がいいです。 T model = (T)Datastore.get(createModelMeta(clazz), (Key)keyProp.getValue(oldModel)); BeanUtil.copy(oldModel, model); PropertyDesc versionProp = desc.getPropertyDesc("version"); versionProp.setValue(model, version - 1); Datastore.put(model); return model; } private static ModelMeta<?> createModelMeta(Class<?> modelClass) { String metaClassName = modelClass.getName().replace(".model.", ".meta.") + "Meta"; return ClassUtil.newInstance(metaClassName, Thread .currentThread() .getContextClassLoader()); } }
使い方(テストコード)
import org.slim3.datastore.Datastore; import org.slim3.tester.DatastoreTestCase; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Transaction; public class BackupTest extends DatastoreTestCase { public void testBackup() { Transaction tx = Datastore.beginTransaction(); CronLog log = new CronLog(); log.setController("A"); Datastore.put(tx, log); Key key = Backup.backup(tx, log); // tx.commit(); Datastore.commit(tx); Backup bk = Datastore.get(new BackupMeta(), key); CronLog o = (CronLog)bk.getModelObject(); assertEquals("A", o.getController()); } public void testBackupTx() { try { Transaction tx = Datastore.beginTransaction(); CronLog log = new CronLog(); log.setController("A"); Datastore.put(tx, log); Backup.backup(tx, log); if ("".equals("")) { throw new Exception(); } // tx.commit(); Datastore.commit(tx); } catch (Exception ignore) { } assertEquals(0, count(Backup.class)); assertEquals(0, count(CronLog.class)); } public void testGet() { Transaction tx = Datastore.beginTransaction(); CronLog log = new CronLog(); log.setController("A"); Datastore.put(tx, log); Backup.backup(tx, log); // tx.commit(); Datastore.commit(tx); log.setController("B"); Datastore.put(log); // backup から取得 CronLog oldCronLog = Backup.get(CronLog.class, log.getKey(), log.getVersion() - 1); assertEquals("A", oldCronLog.getController()); } public void testRollback() { Transaction tx = Datastore.beginTransaction(); CronLog log = new CronLog(); log.setController("A"); Datastore.put(tx, log); Backup.backup(tx, log); // tx.commit(); Datastore.commit(tx); log.setController("B"); Datastore.put(log); assertEquals(new Long(2), log.getVersion()); // backupから戻す CronLog oldCronLog = Backup.rollback(CronLog.class, log.getKey(), log.getVersion() - 1); assertEquals("A", oldCronLog.getController()); assertEquals(1, count(CronLog.class)); // Datastoreから取得しても指定したバージョンに戻っている CronLog currentCronLog = Datastore.get(new CronLogMeta(), log.getKey()); assertEquals("A", currentCronLog.getController()); assertEquals(new Long(1), currentCronLog.getVersion()); } }