GAEのスタンダード環境からGo言語でDatastoreの基本操作を行う方法

今回は Google App Engine (公式ページ) のスタンダード環境から Go言語を使って Datastore (公式ページ) へ接続し、基本的なデータ操作を行う方法をご紹介します。

コンテンツの転載は固くお断りいたします。

必要なパッケージ


import (
	"context"
	"net/http"
	"time"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
)

上のコードでは今回のプログラムで必要なパッケージをインポートしています。『time』と『google.golang.org/appengine/log』パッケージ以外は App Engine のスタンダード環境から Datastore を操作するのに必須です。

スタンダード環境以外の環境から操作する場合には、また違ったパッケージが必要なようですのでご注意下さい。

今回扱うデータ


// Page 記事メタデータ
type Page struct {
	Domain  string // 記事が存在するドメイン
	Slug    string // スラグ
	Title   string // タイトル
	Date    time.Time // 公開日時
	Publish bool // 記事を公開するかどうか
}

今回扱うデータはブログサービスで使えそうなものを用意してみました。より複雑なものとなっていますが、こういった構造体は実際に Datastore を活用して運用している当ブログでも使用しています。

上の構造体はブログ内に存在する記事のメタデータを扱うために存在し、その記事が(複数のドメインを所持している場合)どのドメインのものなのか、スラグは何か、タイトルは? 公開日時はいつか、公開中であるか非公開であるか、そういったデータを保管できます。

記事を新規作成したら、この構造体を元に Datastore に追加し、ブログにアクセスがあれば対象の記事が Datastore に存在するか検索し、ヒットすればこのデータを元に処理を行う形になります。

Datastore では上のような構造体が、MySQL のテーブルような存在となります。

データの追加


// New 記事メタデータの追加
func (p *Page) New(ctx context.Context) (*datastore.Key, error) {
	key := datastore.NewIncompleteKey(ctx, "Page", nil) // 新しいキーを取得
	return datastore.Put(ctx, key, p) // 取得したキーにデータを追加
}

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r) // App Engine 用のコンテキストを取得

    // Page構造体を指定したデータで初期化し、ポインタを取得する
	p := &Page{
		Domain:  "ja.louisaandlily.com",
		Slug:    "abc",
		Title:   "エービーシー",
		Date:    time.Now(),
		Publish: true,
	}

	_, err := p.New(ctx) // ページ追加
	if err != nil { //エラーがあればログ表示
		log.Errorf(ctx, "New page: %v", err)
	}
}

上のコードは Datastore に先程の構造体データを追加するためのものです。『handler』関数が何か分からない場合は『GAEで初めてのGo言語製ウェブサーバーの作り方』の記事を、そしてログの出力方法については『GAEのスタンダード環境でGo言語を使ってログを出力する方法』で書きましたので確認してみて下さい。

新しいキーの取得


key := datastore.NewIncompleteKey(ctx, "Page", nil)

Datastore ではデータとそれに対応するキーが1対1で管理されています。新規にデータを追加する場合は、新しいキーを用意する必要があり、『datastore』パッケージの『NewIncompleteKey』関数を使えば自動的に最適なキーが生成されます。通常はこの関数を使うと良いでしょう。

引数は1番目にコンテキスト、2番目にデータの種類、3番目は追加するデータに親を指定する場合に、そのキーをセットします。

2番目のものは MySQL のテーブル名に相当します。

Putする


key, err := datastore.Put(ctx, key, p)

Put 関数はデータの追加と更新する際に使用する関数で、引数の1番目にコンテキスト、2番目に対応するキー、3番目に追加するデータのポインタを渡します。

戻り値は1番目にキー、2番目にエラー値です。

クエリーを作成してデータを1つ取得


// Get 記事メタデータの取得
func (p *Page) Get(ctx context.Context) (*datastore.Key, error) {
	// クエリー作成
	q := datastore.NewQuery("Page").Filter("Domain =", p.Domain).Filter("Slug =", p.Slug)

	// クエリー実行
	it := q.Run(ctx)

	return it.Next(p) // クエリー結果からデータを取得
}

func handler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	p := &Page{
		Domain: "ja.louisaandlily.com",
		Slug:   "abc",
	}

	_, err := p.Get(ctx)
	if err != nil {
		log.Errorf(ctx, "New page: %v", err)
	}
}

上のコードでは、ドメイン名とスラグに一致するデータを取得しています。尚、今回の例では、返ってくるデータはゼロか1つというのを前提に実行しています。

ドメイン名に一致する全ての記事のメタデータを取得する例は後ほどご紹介します。

クエリーの新規作成


q := datastore.NewQuery("Page")

Datastore でデータを検索して取得する際にはクエリーを作成する必要があり、それは『NewQuery』関数を使うことで実現できます。

引数は1つで、種類を文字列で渡します。

フィルターの追加


q := datastore.NewQuery("Page").Filter("Domain =", p.Domain).Filter("Slug =", p.Slug)

NewQuery で作成したクエリーに、Filter関数を使ってフィルターを追加していきます。今回はドメイン名とスラグ両方が一致するデータを取得しています。

Filter関数の引数は2つで、1番目にプロパティ名と比較演算子、2番目に比較するデータを渡します。比較演算子は『=』以外、例えば『>』『<』『>=』『<=』を使用する場合はインデックスを作成する必要があります。

また、現在は AND検索のみがサポートされており、OR検索は出来ません。今回の例はドメイン名、スラグの2つのプロパティで検索しており、その2つのデータを満たす結果のみが返却されます。

ドメイン名、スラグのどちらか1方だけ一致するような OR検索は出来ません。

クエリーの実行


it := q.Run(ctx)

クエリーを実行するには、作成したクエリーに『Run』関数を使用します。引数にはコンテキストを渡します。戻り値はイテレータです。

結果を取得


key, err := it.Next(p)

イテレータに『Next』関数を使用すると結果を取得できます。引数にデータを格納するためのポインタを渡します。戻り値は、データに対応するキーとエラー値です。

クエリーを作成してデータを複数取得


var pages []Page
for {
	var page Page
	_, err := it.Next(&p) // 次のデータを取得
	if err != nil {
		if err == datastore.Done { // データが無くなったらループを抜ける
			break
		}
		return err // エラー発生!
	}
	pages = append(pages, page) // スライスにデータを追加
}

複数の結果があると考えられる場合には、datastoreパッケージの Done というエラーが発生するまでループして、it.Next を使ってデータを取得し続けます。

it.Next 関数を実行してエラーが無ければデータを取得出来たということですので、スライスに追加していくと良いでしょう。

データの更新


var p Page
key, err := it.Next(&p) // クエリー結果からデータを1つ取得
if err != nil {
	// エラー処理
}

// 取得したデータに新しいタイトルをセット
p.Title = newTitle

// キーを元に Datastore にあるデータを更新
_, err = datastore.Put(ctx, key, &p)
if err != nil {
	// エラー処理
}

データの更新はとてもシンプルで、今回の例では ドメインとスラグ両方に一致するデータを1件取得し、取得した構造体に新しいタイトルをセットし、Put関数を使って Datastore にあるデータを更新しています。

MySQL のように条件と一致したデータを指定したデータで直接更新することは出来ません。なので、一度データを取得してから新しいデータをセットし、キーを元に丸ごと Put します。

データの削除


err := datastore.Delete(ctx, key)

Datastore からデータを削除するためには、クエリーなどを実行して対象のキーを取得し、Delete関数を呼ぶことで削除が可能です。

プロパティの追加と削除


var oldP OldPage
// 古いデータを取得
key, err := it.Next(&oldP)
if err != nil {
	// エラー処理
}

// 新しい構造体を用意する
newP := &NewPage{
	Domain:   oldP.Domain,
	Slug:     oldP.Slug,
	Date:     oldP.Date,
	Modified: time.Now(),
	Publish:  oldP.Publish,
}

// 新しい構造体で更新
key, err = datastore.Put(ctx, key, newP)

プロパティ構成の更新もシンプルです。更新したいデータのキーを取得し、新しい構造体に必要なデータを入れて、そのキーに対して Put すれば更新できます。

さいごに

今回は最低限必要となるであろう機能のみをご紹介しましたが、キー情報を元にデータを取得したり、複数のデータを同時に更新する方法や、複数のデータを一括で削除する方法などなど、他にも沢山あります。

詳しくは『The datastore package (GCP公式ページ)』に載ってありますので、参考にしてみてください。