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

昨日のseasarConでid:higayasuoさんとid:kazunori_279さんとお話をしたことで、そろそろ本気で補償トランザクションの実装を考えようと思う。まずJDOのトランザクション機能(EntityGroup)は使えないという前提。で、いきなりの結論がBigtableではDBの機能が少ないので開発者がDBを実装するような気分でなければいけない。例えばSQLにしてもjoinできないのでプログラムでjoinしようねとかorは使えないので二回SQL実行してマージしようねとかWhere句と異なるキーでorder byするならorder byはメモリソートしようねとか。これはまさに開発者がオプティマイザ部分を担当することに他ならない。そしてトランザクションも同様で自分でロールバックの仕組みを作る必要がある。というわけでとりあえずロールバックする仕組みの案をslim3で試してみた。

IndexController

package slim3.it.controller.rollback;

import javax.jdo.JDOHelper;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
import org.slim3.util.BeanUtil;
import org.slim3.util.ByteUtil;
import org.slim3.util.ThrowableUtil;

import slim3.it.dao.BlogDao;
import slim3.it.dao.RollbackSegmentDao;
import slim3.it.model.Blog;
import slim3.it.model.RollbackSegment;

public class IndexController extends Controller {

    private BlogDao blogDao = new BlogDao();
    private RollbackSegmentDao rollbackSegmentDao = new RollbackSegmentDao();

    @Override
    public Navigation run() {
        Blog blog = blogDao.findFirst();
        backup(blog); // RollbackSegmentに現在の状態を保存する
        blog.setTitle(blog.getTitle() + "a"); // 更新する
        blogDao.makePersistentInTx(blog);
        Blog oldVersionBlog = rollback(blog.getKey(), blog.getVersion() - 1); // 前回の更新前のBlogオブジェクトを取得する
        return forward("index.jsp");
    }

    private void backup(Object model) {
        try {
            RollbackSegment bk = new RollbackSegment();
            bk.setModelName(model.getClass().getSimpleName());
            bk.setModelKey(JDOHelper.getObjectId(model).toString());
            bk.setModelVersion((Long) JDOHelper.getVersion(model));
            Object o = model.getClass().newInstance();
            BeanUtil.copy(model, o);
            bk.setBytes(ByteUtil.toByteArray(o)); // シリアライズしたbyte[]を保存する
            rollbackSegmentDao.makePersistentInTx(bk);
        } catch (Throwable t) {
            ThrowableUtil.wrapAndThrow(t);
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T rollback(String modelKey, long version) {
        RollbackSegment seg =
            rollbackSegmentDao.findByModelKeyAndModelVersion(modelKey, version);
        return (T) ByteUtil.toObject(seg.getBytes());
    }

}

RollbackSegment

package slim3.it.model;

import java.io.Serializable;

import javax.jdo.annotations.Extension;
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 javax.jdo.annotations.Version;
import javax.jdo.annotations.VersionStrategy;

import com.google.appengine.api.datastore.Blob;

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
@Version(strategy = VersionStrategy.VERSION_NUMBER, column = "version")
public class RollbackSegment implements Serializable {

    private static final long serialVersionUID = 1L;

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
    private String key;

    @Persistent
    private Long version = 1L;

    @Persistent
    private String modelName;

    @Persistent
    private String modelKey;

    @Persistent
    private Long modelVersion;

    @Persistent
    private Blob blob;

    public byte[] getBytes() {
        if (blob == null) {
            return null;
        }
        return blob.getBytes();
    }

    public void setBytes(byte[] bytes) {
        this.blob = new Blob(bytes);
    }

    // (略)
}

RollbackSegmentDao

public class RollbackSegmentDao extends GenericDao<RollbackSegment> {

    private static final RollbackSegmentMeta m = new RollbackSegmentMeta();

    public RollbackSegmentDao() {
        super(RollbackSegment.class);
    }

    public RollbackSegment findByModelKeyAndModelVersion(String modelKey, long modelVersion) {
        return from()
            .where(m.modelKey.eq(modelKey), m.modelVersion.eq(modelVersion))
            .getSingleResult();
    }

}

仕組み

仕組みは簡単で前回のモデルの状態をRollbackSegmentクラスにシリアライズして保存しておくというだけ。以下、略。