GORMのソースコードを読む5 – Createその4

引き続き、Createに関するコールバックを一つずつ定義されている順番に読んでいく。今回はsaveBeforeAssociationsCallback。O/Rマッピングの実装が登場するので、多少読んでいくのが大変だが、一つずつ読んでいく。

func saveBeforeAssociationsCallback(scope *Scope) {
	for _, field := range scope.Fields() {
		...略...
	}
}

素直に一行目から読んで行こう。このメソッドは、引数で渡されたScopeのFields()で返される値に対して、一つずつ処理を行う構造になっている。

func (scope *Scope) Fields() []*Field {
	if scope.fields == nil {
		...略...
		scope.fields = &fields
	}

	return *scope.fields
}

Fields()メソッドは、Fieldのポインタのスライスを返す。計算量の節約のためか、scope.fieldsに値をキャッシュしているようだ。

type Field struct {
	*StructField
	IsBlank bool
	Field   reflect.Value
}

Fieldは上記のように定義されている。StructFieldが埋め込まれているようだ。

type StructField struct {
	DBName          string
	Name            string
	Names           []string
	IsPrimaryKey    bool
	IsNormal        bool
	IsIgnored       bool
	IsScanner       bool
	HasDefaultValue bool
	Tag             reflect.StructTag
	TagSettings     map[string]string
	Struct          reflect.StructField
	IsForeignKey    bool
	Relationship    *Relationship
}

StructFieldの定義はこのようになっている。これはGolangにおけるStructのフィールドとDBのレコードのカラム名などのマッピングを表現するための構造体のようだ。

Field, StructFieldの各々のフィールドの詳細についてはまだわからないが、scope.Fields()はCreateでこれから保存するオブジェクトのフィールドの情報のスライスが返されると想像される。詳しく中を見て行こう。

var (
	fields             []*Field
	indirectScopeValue = scope.IndirectValue()
	isStruct           = indirectScopeValue.Kind() == reflect.Struct
)

for _, structField := range scope.GetModelStruct().StructFields {
	if isStruct {
		...略...
		fields = append(fields, &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)})
	} else {
		fields = append(fields, &Field{StructField: structField, IsBlank: true})
	}
}
scope.fields = &fields

scope.fieldsが空だった場合、すなわちキャッシュがなかった場合の処理を詳しく見て行ってみよう。

scope.IndirectValue()は前のポストで見たように、Createの引数で渡されたオブジェクト(DBに保存しようとしているオブジェクト)のDereferenceを行う。つまり、indirectScopeValueにはこれからDBに保存するオブジェクトの実体が入る。

isStructにはこのオブジェクトが構造体かどうかを表す値が入る。スライスなど構造体以外のものが入っていた場合はfalseになる。このisStructはその下のループのイテレーションで毎回参照され処理の分岐に使われている。イテレーション処理中にこの値が変更されることはないので、ループの外側で分岐を書いた方がわかりやすいような気がするが、何か理由があるのだろうか。そもそも変更されないのでconstにした方が良いような?

ちょっと大変だがscope.GetModelStruct().StructFieldsについて掘り下げていく。scope.GetModelStruct()は長い関数なので、内容を多少省略しながら記していく。

func (scope *Scope) GetModelStruct() *ModelStruct

GetModelStruct()はModelStructのポインタを返す。

type ModelStruct struct {
	PrimaryFields    []*StructField
	StructFields     []*StructField
	ModelType        reflect.Type
	defaultTableName string
}

ModelStructは上記のように定義されている。StructFieldがモデルの各フィールド単位でのDBのテーブルのカラムとのマッピングを表すのに対し、ModelStructはモデル単位でのマッピングを表す構造体となる。

reflectType := reflect.ValueOf(scope.Value).Type()
for reflectType.Kind() == reflect.Slice || reflectType.Kind() == reflect.Ptr {
	reflectType = reflectType.Elem()
}

// Scope value need to be a struct
if reflectType.Kind() != reflect.Struct {
	return &modelStruct
}

GetModelStruct()の中に戻ろう。リフレクションの仕組みを使って、scope.Value(DBに保存しようとしているオブジェクト)の型を取得しようとしている。scope.Valueがスライスあるいはポインタの場合はElem()で要素/実体の型を参照できるようにしている。scope.Valueが構造体でもポインタでもスライスでもない場合、例えばintなどの場合は、そこで処理を終了して中身が空のModelStructを返している。

// Get Cached model struct
if value, ok := modelStructsMap.Load(reflectType); ok && value != nil {
	return value.(*ModelStruct)
}

GetModelStruct()にもキャッシュの仕組みがある。modelStructsMapはsync.Mapで定義されており、スレッドセーフな実装になっている。

modelStruct.ModelType = reflectType

キャッシュに値がない場合はモデルの解析が続行される。まずはModelTypeにモデルの型情報がセットされる。

// Get all fields
for i := 0; i < reflectType.NumField(); i++ {
	if fieldStruct := reflectType.Field(i); ast.IsExported(fieldStruct.Name) {
		...略...
	}
}

メソッドが長くて読むのが大変だが、リフレクションを使って、モデルのフィールドを一つずつチェックしていく。ast.IsExportedで、フィールドがExportされている(=Privateではない)かどうかチェックし、Exportされているフィールドのみをチェックしていく。

field := &StructField{
	Struct:      fieldStruct,
	Name:        fieldStruct.Name,
	Names:       []string{fieldStruct.Name},
	Tag:         fieldStruct.Tag,
	TagSettings: parseTagSetting(fieldStruct.Tag),
}

Exportされているフィールドの場合は、対応するStructFieldオブジェクトが作られる。

func parseTagSetting(tags reflect.StructTag) map[string]string {
	setting := map[string]string{}
	for _, str := range []string{tags.Get("sql"), tags.Get("gorm")} {
		tags := strings.Split(str, ";")
		for _, value := range tags {
			v := strings.Split(value, ":")
			k := strings.TrimSpace(strings.ToUpper(v[0]))
			if len(v) >= 2 {
				setting[k] = strings.Join(v[1:], ":")
			} else {
				setting[k] = k
			}
		}
	}
	return setting
}

parseTagSettingsでは、構造体に定義されたタグの解析が行われる。ここではsql, gormのタグの中身をそれぞれ見ていく。タグの中に;が含まれる場合は、;で区切った結果を一つずつ見ていく。タグの中身はkey:valueの形で表現されていることが想定されていて、一つ目のコロンよりも前をkeyに、それ以降をvalueとしてsettingというmapにセットしていき、全部パースし終わったところでそのmapを返すようにしている。keyは強制的に大文字化され、スペースが排除された状態で扱われる。これは表記ブレの対応だろう。

というわけで、StructFieldのTagSettingsには、そのフィールドにつけられたタグがmapの形にパースされてセットされる。

ちなみに、この実装ではkeyあるいはvalue内に;が含まれていた場合にパースに失敗してしまいそうだ。

if _, ok := field.TagSettings["-"]; ok {
    field.IsIgnored = true
} else {
    ...略...
}

// Even it is ignored, also possible to decode db value into the field
if value, ok := field.TagSettings["COLUMN"]; ok {
    field.DBName = value
} else {
    field.DBName = ToDBName(fieldStruct.Name)
}

その後の処理のブロックは非常に長いので、まず大まかな構造から見て行こう。上でパースしたタグの中に、keyが”-“のタグがあった場合は、IsIgnoredなフィールドとして扱われ、それ以上のパースは行われない。IsIgnoredがなんなのか、まだはっきりと処理やコメントには現れていないが、おそらくO/Rマッピングで無視される、ということなんだろう。

ここもコメントや説明がないのでまだ予想だが、field.DBNameはデータベースにおけるカラム名を表すフィールドと考えられる。タグでCOLUMNが指定されている場合は、その値を使う。指定されていない場合はToDBNameメソッドを使って、fieldStruct.Name(=reflectType.Field(i).Name)つまりGoにおけるStructのフィールド名から生成した値を利用する。ToDBNameの処理の中身についてはそこまで重要ではないので、今回は省略する。

if _, ok := field.TagSettings["PRIMARY_KEY"]; ok {
    field.IsPrimaryKey = true
    modelStruct.PrimaryFields = append(modelStruct.PrimaryFields, field)
}

if _, ok := field.TagSettings["DEFAULT"]; ok {
    field.HasDefaultValue = true
}

if _, ok := field.TagSettings["AUTO_INCREMENT"]; ok && !field.IsPrimaryKey {
    field.HasDefaultValue = true
}
...略...

少し戻って、IsIgnoredではない場合の処理を見て行こう。最初の3つの処理は、プライマリーキー、デフォルト値(があるかどうか)、自動インクリメントなどのDBの設定をタグの内容からセットしているだけである。

この後、フィールドの型に応じて処理が分岐し、StructField, ModelStructを作成していく処理が続いていく。まだまだ長くなるので続きは次のポストに書いていく。

Pocket

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です