GAE/Jでロールバックを実装する(2)#appengine

前回はJDOでしたが、slim3がJDOではなくなったので新しいDatastore版を作りました。前回と異なるのは保存元のモデルをEntityGroupのルートエンティティにしてBackupをEntityGroupに加えたことです。以前まではEntityGroupの考え方がわかっておらず、こんな時にEntityGroupは使わないのだろうと思っていたのですが、EntityGroupはRDBの関連ではなくKeyで構成されていることからEntityGroupに含めてもいいという考えに至りました。

Keyで構成されているとはどういうことかは下記資料の18P〜22Pをご覧頂くのがいいと思います。

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());
    }
}