Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 12883] ファイルの概要

このコミットは、Go言語のmisc/dashboardアプリケーションにおいて、Google App EngineのDatastoreからデータを読み込む際に発生するdatastore.ErrFieldMismatchエラーを適切に処理するための変更です。以前のコミットでPackageエンティティから不要なフィールドが削除されたにもかかわらず、Datastoreにはそれらの古いフィールドを持つエンティティがまだ存在するため、読み込み時にこのエラーが発生していました。本コミットは、このエラーを明示的にチェックし、無視することで、アプリケーションが古いスキーマのデータも問題なく処理できるようにします。

コミット

commit e6c5e2a363584465ab5038d8af9e70f3b98d5ba88
Author: David Symonds <dsymonds@golang.org>
Date:   Thu Apr 12 09:55:37 2012 +1000

    misc/dashboard: cope with removed Package fields.
    
    adg removed some now-unwanted fields in Package a while ago,
    but there are still datastore entities with those fields,
    so we must explicitly check for ErrFieldMismatch and ignore it.
    
    R=golang-dev, rsc
    CC=adg, golang-dev
    https://golang.org/cl/6007043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e6c5e2a36358465ab5038d8af9e70f3b98d5ba88

元コミット内容

misc/dashboard: cope with removed Package fields. (misc/dashboard: 削除されたPackageフィールドに対応する。)

adg removed some now-unwanted fields in Package a while ago, but there are still datastore entities with those fields, so we must explicitly check for ErrFieldMismatch and ignore it. (adgが以前、Packageから不要なフィールドをいくつか削除したが、Datastoreにはまだそれらのフィールドを持つエンティティが存在するため、明示的にErrFieldMismatchをチェックして無視する必要がある。)

R=golang-dev, rsc CC=adg, golang-dev https://golang.org/cl/6007043

変更の背景

この変更の背景には、Google App EngineのDatastoreにおけるデータモデル(スキーマ)の進化があります。Go言語のmisc/dashboardアプリケーションは、Goプロジェクトのビルドやテストの状態を監視するためのダッシュボードであり、そのデータはApp EngineのDatastoreに保存されています。

以前のコミット(adgによって行われたとされる)で、Packageエンティティ(Datastoreに保存されるデータ構造)から一部のフィールドが「不要になった」として削除されました。しかし、DatastoreはNoSQLデータベースであり、リレーショナルデータベースのような厳密なスキーマ強制がありません。そのため、データモデルが変更された後も、既にDatastoreに保存されている既存のエンティティは、古いスキーマ(削除されたフィールドを含む)のまま残存します。

アプリケーションがこれらの古いエンティティを読み込もうとすると、Goのappengine/datastoreパッケージは、Goの構造体とDatastoreのエンティティのフィールドが一致しない場合にdatastore.ErrFieldMismatchエラーを返します。このエラーは通常、予期せぬデータ構造の不一致を示すものですが、このケースでは意図的にフィールドが削除された結果であるため、アプリケーションの正常な動作を妨げないように、この特定のエラーを無視する必要がありました。

このコミットは、このようなスキーマの不一致によってアプリケーションがクラッシュしたり、不正確な動作をしたりするのを防ぐための、堅牢性向上のための対応です。

前提知識の解説

Google App Engine (GAE)

Google App Engineは、Googleが提供するPaaS(Platform as a Service)であり、開発者がインフラストストラクチャの管理を気にすることなく、スケーラブルなウェブアプリケーションやバックエンドサービスを構築・デプロイできるクラウドプラットフォームです。Go言語はApp Engineでサポートされているランタイムの一つです。

Google Cloud Datastore (旧 App Engine Datastore)

Google Cloud Datastoreは、Google App Engineの主要なストレージサービスとして提供されていたNoSQLドキュメントデータベースです。現在はGoogle Cloud Datastoreとして独立したサービスになっていますが、App Engineアプリケーションのバックエンドとして広く利用されています。 Datastoreは、キーと値のペアでデータを保存するスキーマレスなデータベースであり、エンティティと呼ばれるデータオブジェクトを扱います。各エンティティは、プロパティ(フィールド)の集合を持ち、これらのプロパティは様々なデータ型を持つことができます。

appengine/datastoreパッケージ

Go言語でGoogle App EngineのDatastoreを操作するための標準ライブラリです。このパッケージを通じて、エンティティの保存、取得、更新、削除、クエリの実行などが行われます。

datastore.ErrFieldMismatch

appengine/datastoreパッケージが返すエラーの一つです。このエラーは、DatastoreからエンティティをGoの構造体に読み込む際に、構造体には存在しないフィールドがDatastoreのエンティティに存在する場合、またはその逆の場合に発生します。 具体的には、Datastoreエンティティのプロパティ名がGoの構造体のフィールド名と一致しない、あるいは型が互換性がない場合にこのエラーが返されます。このコミットのケースでは、Goの構造体からフィールドが削除されたにもかかわらず、Datastoreにはそのフィールドを持つ古いエンティティが残っていたために発生しました。

スキーマの進化 (Schema Evolution)

データベースのスキーマ(データ構造)が時間とともに変化していくプロセスを指します。特にNoSQLデータベースでは、リレーショナルデータベースのような厳格なスキーマ定義がないため、アプリケーションのバージョンアップに伴ってデータモデルが変更されることがよくあります。この際、古いデータと新しいデータモデルの互換性をどのように保つか、あるいは古いデータをどのように処理するかが重要な課題となります。datastore.ErrFieldMismatchは、このスキーマの進化の過程で発生しうる典型的な問題の一つです。

技術的詳細

このコミットの技術的詳細の核心は、Goのappengine/datastoreパッケージが返すdatastore.ErrFieldMismatchエラーのハンドリングにあります。

Goのdatastoreパッケージは、DatastoreからエンティティをGoの構造体にマッピングする際に、リフレクションを使用します。エンティティの各プロパティは、対応するGo構造体のフィールドにマッピングされます。もしDatastoreエンティティに存在するプロパティが、Go構造体には存在しない場合、またはその逆の場合、datastore.ErrFieldMismatchエラーが発生します。

このエラーは、通常、開発者がデータモデルの不一致に気づくための重要なシグナルです。しかし、この特定のシナリオでは、開発者が意図的にPackage構造体からフィールドを削除したため、このエラーは「予期された」不一致を示します。つまり、古いデータが新しい構造体に完全にマッピングできないのは既知の事実であり、アプリケーションのロジック上、その古いフィールドはもはや必要ないため、エラーとして扱う必要がないのです。

コミットでは、以下のパターンでエラーをチェックし、無視しています。

if _, ok := err.(*datastore.ErrFieldMismatch); ok {
    // Some fields have been removed, so it's okay to ignore this error.
    err = nil
}

このコードスニペットは、Goの型アサーションとエラーハンドリングのイディオムを示しています。

  1. err.(*datastore.ErrFieldMismatch): err変数が*datastore.ErrFieldMismatch型に変換可能かどうかを試みます。
  2. _, ok := ...: 変換が成功した場合、oktrueになり、変換された値は_(ブランク識別子)に代入されます(このケースでは値自体は不要)。変換が失敗した場合、okfalseになります。
  3. if ok: oktrueの場合、つまりエラーがdatastore.ErrFieldMismatch型であった場合にのみ、ブロック内のコードが実行されます。
  4. err = nil: ここが最も重要な部分です。datastore.ErrFieldMismatchエラーが検出された場合、err変数をnilに設定し直します。これにより、後続のエラーチェック(例: if err != nil)では、この特定のエラーが無視され、アプリケーションの実行が続行されます。

このアプローチは、Datastoreのスキーマ進化を許容し、古いデータが新しいアプリケーションバージョンでも問題なく読み込まれるようにするための一般的なパターンです。ただし、この方法を適用する際には、無視するフィールドが本当にアプリケーションのロジックに影響を与えないことを慎重に確認する必要があります。影響を与える可能性のあるフィールドの不一致を誤って無視すると、データの破損や予期せぬ動作につながる可能性があります。

コアとなるコードの変更箇所

変更は主にmisc/dashboard/app/build/build.gomisc/dashboard/app/build/init.goの2つのファイルにわたります。

misc/dashboard/app/build/build.go

  1. func (p *Package) LastCommit(c appengine.Context) (*Commit, error): datastore.GetAllの呼び出し後、エラーチェックの前にdatastore.ErrFieldMismatchを無視するロジックが追加されました。

    --- a/misc/dashboard/app/build/build.go
    +++ b/misc/dashboard/app/build/build.go
    @@ -49,6 +49,10 @@ func (p *Package) LastCommit(c appengine.Context) (*Commit, error) {
      	Order("-Time").
      	Limit(1).
      	GetAll(c, &commits)
    +if _, ok := err.(*datastore.ErrFieldMismatch); ok {
    +	// Some fields have been removed, so it's okay to ignore this error.
    +	err = nil
    +}
     if err != nil {
      	return nil, err
     }
    
  2. func GetPackage(c appengine.Context, path string) (*Package, error): datastore.Getの呼び出し後、エラーチェックの前にdatastore.ErrFieldMismatchを無視するロジックが追加されました。

    --- a/misc/dashboard/app/build/build.go
    +++ b/misc/dashboard/app/build/build.go
    @@ -65,6 +69,10 @@ func GetPackage(c appengine.Context, path string) (*Package, error) {
      if err == datastore.ErrNoSuchEntity {
      	return nil, fmt.Errorf("package %q not found", path)
      }
    +if _, ok := err.(*datastore.ErrFieldMismatch); ok {
    +	// Some fields have been removed, so it's okay to ignore this error.
    +	err = nil
    +}
      return p, err
     }
    
  3. func Packages(c appengine.Context, kind string) ([]*Package, error): datastore.NewQueryでクエリを実行し、イテレータから次のエンティティを取得するt.Next(pkg)の呼び出し後、エラーチェックの前にdatastore.ErrFieldMismatchを無視するロジックが追加されました。

    --- a/misc/dashboard/app/build/build.go
    +++ b/misc/dashboard/app/build/build.go
    @@ -297,7 +305,12 @@ func Packages(c appengine.Context, kind string) ([]*Package, error) {
      	q := datastore.NewQuery("Package").Filter("Kind=", kind)
      	for t := q.Run(c); ; {
      		pkg := new(Package)
    -		if _, err := t.Next(pkg); err == datastore.Done {
    +		_, err := t.Next(pkg)
    +		if _, ok := err.(*datastore.ErrFieldMismatch); ok {
    +			// Some fields have been removed, so it's okay to ignore this error.
    +			err = nil
    +		}
    +		if err == datastore.Done {
      			break
      		} else if err != nil {
      			return nil, err
    

misc/dashboard/app/build/init.go

  1. func initHandler(w http.ResponseWriter, r *http.Request): datastore.Getの呼び出し後、エラーチェックの前にdatastore.ErrFieldMismatchを無視するロジックが追加されました。

    --- a/misc/dashboard/app/build/init.go
    +++ b/misc/dashboard/app/build/init.go
    @@ -42,7 +42,12 @@ func initHandler(w http.ResponseWriter, r *http.Request) {
      	c := appengine.NewContext(r)
      	defer cache.Tick(c)
      	for _, p := range defaultPackages {
    -		if err := datastore.Get(c, p.Key(c), new(Package)); err == nil {
    +		err := datastore.Get(c, p.Key(c), new(Package))
    +		if _, ok := err.(*datastore.ErrFieldMismatch); ok {
    +			// Some fields have been removed, so it's okay to ignore this error.
    +			err = nil
    +		}
    +		if err == nil {
      			continue
      		} else if err != datastore.ErrNoSuchEntity {
      			logErr(w, r, err)
    

コアとなるコードの解説

このコミットのコアとなる変更は、datastore.ErrFieldMismatchエラーを明示的に捕捉し、それを無視する(エラーをnilに設定する)というパターンを、Datastoreからデータを読み込む可能性のある複数の箇所に適用している点です。

具体的には、以下のDatastore操作が行われる場所でこのエラーハンドリングが追加されています。

  1. datastore.GetAll: 複数のエンティティを一度に取得する際に使用されます。クエリ結果として返されるエンティティの中に、古いスキーマのものが含まれている可能性があるため、ここでエラーを無視します。
  2. datastore.Get: 単一のエンティティをキーで取得する際に使用されます。特定のPackageエンティティが古いスキーマである可能性があるため、ここでエラーを無視します。
  3. datastore.NewQuery(...).Run(...).Next(...): クエリ結果をイテレータで順次処理する際に使用されます。イテレータが次のエンティティを読み込むたびにdatastore.ErrFieldMismatchが発生する可能性があるため、ループ内でこのエラーを無視します。

これらの変更により、misc/dashboardアプリケーションは、Datastoreに存在する古いPackageエンティティ(削除されたフィールドを含むもの)を読み込もうとした際に、datastore.ErrFieldMismatchによって処理が中断されることなく、正常に動作を継続できるようになります。アプリケーションは、新しいPackage構造体で定義されているフィールドのみを読み込み、古い不要なフィールドは単に無視されます。これは、アプリケーションの堅牢性を高め、データモデルの進化に対応するための重要な修正です。

関連リンク

  • https://golang.org/cl/6007043 - このコミットで言及されている、Packageフィールドが削除された元の変更セットへのリンク(GoのGerritコードレビューシステム)。

参考にした情報源リンク

  • Go App Engine Datastore Documentation (当時のバージョンに基づく)
  • Google Cloud Datastore Concepts (スキーマレスデータベースの概念理解のため)
  • Go言語のエラーハンドリングに関する一般的な知識
  • Go言語のリフレクションに関する一般的な知識
  • datastore.ErrFieldMismatchに関するGoの公式ドキュメントや関連する議論(Stack Overflowなど)