僕は普段GAE/Goで開発をしているのですが、自社でdatastoreの機能をまとめたライブラリを作ることになって、GoDocを読んでいるときに初めて知ったのですが、datastoreにMutationがあるのをしりました!
それでMutationの使い方や使い所の記事を探したのですが、SpannerのMutationの記事と公式ドキュメントしか出て来なかったので、自分で記事を書くことにしました!
Mutationとは?
自分が探した限り、datastoreのmutationに関する説明は書いていなかったので、何とも言えないんですけど、
Insert, Update, Upsert, Deleteを一括で行うもので、副作用を起こすものですね
で、何がよくなったかというと
- Insert, Update, Upsertが分離された!
- 副作用を起こす処理をまとめられるようになった!
この二点が大きいと感じています!
どこを気をつけるべきなのか?
まずはどうやって書くことができるのかをみていこうと思います
今までは更新とかは全てPutを使っていて、ある意味Upsertだけだったのですが以下のようにかけるようになりました!
type Book struct {
ID int64
Name string
}
func main() {
ctx := context.Background()
book := &Book{
ID: 1,
Name: "hoge",
}
idKey := datastore.IDKey("Book", 1, nil)
insert := datastore.NewInsert(idKey, book)
book.Name = "hoge1"
update := datastore.NewUpdate(idKey, book)
book2 := book
book2.ID = 2
upsert1 := datastore.NewUpsert(idKey, book2)
book2.Name = "hoge2"
upsert2 := datastore.NewUpsert(idKey, book2)
delete := datastore.NewDelete(book)
datastoreClient, err := datastore.NewClient(ctx, projectID)
if err != nil {
fmt.Errorf("%v", err)
}
if err := datastoreClient.Mutate(ctx, insert, update, upsert1, upsert2, delete); err != nil {
fmt.Errorf("%v", err)
}
}
go特有の、if err != nil {...}が一度だけMutationを呼ぶことでかなりスッキリ見えますよね。
さらに、ここからが問題で、そもそも、先ほども書きましたが、英語も日本語でも記事もブログも少ないのですが、RESTのドキュメントを覗くとMutationは以下のように書いてあります
The type of commit to perform. Defaults to TRANSACTIONAL.
commitを実行するタイプです。デフォルトではTRANSACTIONALとなってます。
これを読んで僕は、
「Spanaerとかと同じで、途中でエラーになったらロールバックしてくれるのか!」
でも、これ実は罠なんですよ!
これを検証して、途中でエラーになるようにしました
type Book struct {
ID int64
Name string
}
func main() {
ctx := context.Background()
book := &Book{
ID: 1,
Name: "hoge",
}
idKey := datastore.IDKey("Book", 1, nil)
insert1 := datastore.NewInsert(idKey, book)
book.Name = "hoge1"
update := datastore.NewUpdate(idKey, book)
insert2 := datastore.NewInsert(idKey, book) // 二回目にインサートするので、エラーになるはず
delete := datastore.NewDelete(book)
datastoreClient, err := datastore.NewClient(ctx, projectID)
if err != nil {
fmt.Errorf("%v", err)
}
if err := datastoreClient.Mutate(ctx, insert1, update, insert2, delete); err != nil {
fmt.Errorf("%v", err)
}
}
これを実行すると、途中でエラーになるので、ロールバックされて、Mutationは実行される前の状態になっていて欲しい!
ところが、このままだとそうはいきません、、、、、、
updateまで実行された状態になってしまいます!!!
ということで、なんでこうなるかというとMutationの中を見るとわかります!
func (c *Client) Mutate(ctx context.Context, muts ...*Mutation) (ret []*Key, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.Mutate")
defer func() { trace.EndSpan(ctx, err) }()
pmuts, err := mutationProtos(muts)
if err != nil {
return nil, err
}
req := &pb.CommitRequest{
ProjectId: c.dataset,
Mutations: pmuts,
Mode: pb.CommitRequest_NON_TRANSACTIONAL, // <- ここです!!!!
}
resp, err := c.client.Commit(ctx, req)
if err != nil {
return nil, err
}
// Copy any newly minted keys into the returned keys.
ret = make([]*Key, len(muts))
for i, mut := range muts {
if mut.key.Incomplete() {
// This key is in the mutation results.
ret[i], err = protoToKey(resp.MutationResults[i].Key)
if err != nil {
return nil, errors.New("datastore: internal error: server returned an invalid key")
}
} else {
ret[i] = mut.key
}
}
return ret, nil
}
// https://github.com/googleapis/google-cloud-go/blob/master/datastore/datastore.go#L651
NON_TRANSACTIONALなんですよ!!!
なので、このMutation関数を普通に使ってもロールバックはされません、、、
ではどうしたら良いのかというと、トランザクションをはって使う必要があります。
幸い、TransactionにもMutationがはえているので、それを使うようにするとロールバックされるようになります!
type Book struct {
ID int64
Name string
}
func main() {
ctx := context.Background()
book := &Book{
ID: 1,
Name: "hoge",
}
idKey := datastore.IDKey("Book", 1, nil)
insert1 := datastore.NewInsert(idKey, book)
book.Name = "hoge1"
update := datastore.NewUpdate(idKey, book)
insert2 := datastore.NewInsert(idKey, book) // 二回目にインサートするので、エラーになるはず
delete := datastore.NewDelete(book)
datastoreClient, err := datastore.NewClient(ctx, projectID)
if err != nil {
fmt.Errorf("%v", err)
}
if _, err := datastoreClient.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
if _, err := tx.Mutate(insert1, update, insert2, delete); err != nil {
return multiErrorToSingleError(err)
}
return nil
}); err != nil {
fmt.Errorf("%v", err)
}
}
こうしておけば、トランザクションがはられて、ロールバックされるので、失敗する前の状態になることができます!!!
まとめ
- datastoreにもMutationあるよ!
- デフォルトではNON_TRANSACTIONALなので、ロールバックはされないですよ!
- ロールバックして欲しいならば、自分でトランザクションをはりましょうね!
参考
- https://pkg.go.dev/cloud.google.com/go/datastore?tab=doc
- https://github.com/googleapis/google-cloud-go/blob/datastore/v1.1.0/datastore/transaction.go
- https://github.com/googleapis/google-cloud-go/blob/master/datastore/datastore.go
- https://cloud.google.com/datastore/docs/reference/data/rest/v1/projects/commit