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クラスにシリアライズして保存しておくというだけ。以下、略。