LoginSignup
6
3

More than 3 years have passed since last update.

GAE/Go 2nd generationでDatastoreのmutationを使う上でトランザクションに気をつけろ!

Last updated at Posted at 2020-03-17

僕は普段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なので、ロールバックはされないですよ!
  • ロールバックして欲しいならば、自分でトランザクションをはりましょうね!

参考

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3