GORMのソースコードを読む2 – Createその1

OpenでDBの接続を行う部分のコードを読んだので、次はDBに対してクエリーを発行する部分を読んでいく。

db.Create(&Product{Code: "L1212", Price: 1000})

まずは一番わかりやすそうなCreateのコードを見てみよう。

DBのトランザクション、クエリー生成、O/Rマッピングなど、非常に長い内容になるため、いくつかのポストに分けて読んでいこうと思う。

モデルのInsertを行うCreateメソッドは、main.goに定義されている。

func (s *DB) Create(value interface{}) *DB {
	scope := s.NewScope(value)
	return scope.callCallbacks(s.parent.callbacks.creates).db
}

一連のトランザクションを扱うScopeを作成してから、Create関連の処理(コールバック)群を呼ぶ、という構造になっている。

Scopeの作成

// NewScope create a scope for current operation
func (s *DB) NewScope(value interface{}) *Scope {
	dbClone := s.clone()
	dbClone.Value = value
	return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value}
}

NewScope関数で新しいScopeを作成する際には、DBインスタンスの複製が行われる。理由はわからないが、gormでInsertやSelectを行う時のパラメータやエラー情報などはDBインスタンスに保持されるため、複数の処理を走らせた場合におかしなことにならないようにするためではないかと思われる。これらのパラメータはDBではなくScopeが持った方が自然なのではないかという気もするのだが、その辺りはよくわからない。

func (s *DB) clone() *DB {
	db := &DB{
		db:                s.db,
		parent:            s.parent,
		logger:            s.logger,
		logMode:           s.logMode,
		values:            map[string]interface{}{},
		Value:             s.Value,
		Error:             s.Error,
		blockGlobalUpdate: s.blockGlobalUpdate,
		dialect:           newDialect(s.dialect.GetName(), s.db),
	}

	for key, value := range s.values {
		db.values[key] = value
	}

	if s.search == nil {
		db.search = &search{limit: -1, offset: -1}
	} else {
		db.search = s.search.clone()
	}

	db.search.db = db
	return db
}

複製処理自体は基本的に値をコピーするのみで特殊なことはしていないが、唯一searchフィールドだけは、nilだった場合に&search{limit: -1, offset: -1}という値をセットしているようだ。これもなぜこのタイミングでセットしているのかはわからない。そして、searchフィールドでは循環参照が発生していそうな気がする。

dbClone.Value = value
return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value}

加えて、この部分だけ読むと、dbCloneがvalueを持っているので、Scopeが別にvalueを保持するのは冗長な感じがするが、何か理由があったのだろうか。

コールバックの呼び出し

次にコールバックの呼び出しの方を見てみる。

func (scope *Scope) callCallbacks(funcs []*func(s *Scope)) *Scope {
	for _, f := range funcs {
		(*f)(scope)
		if scope.skipLeft {
			break
		}
	}
	return scope
}

callCallbacksは、渡された関数を自身のScopeを引数にして順番に呼んでいるだけである。ただし、関数呼び出し後、自身のScopeのskipLeftがセットされていた場合は残りの関数の呼び出しはスキップされるようになっている。

引数で渡されているs.parent.callbacks.createsとは何か追っていこう。

func (s *DB) NewScope(value interface{}) *Scope

まず、分かりにくいがここのsはScopeではなくgorm.DBである。

db.parent = db (Open関数内での実装)

そして、Open関数で作られたDBインスタンスの場合、このdbのparentはdb自身となる。

callbacks: DefaultCallback,

callbacksの中身も、Open関数を確認してみるとDefaultCallbackが指定されている。

type Callback struct {
	creates    []*func(scope *Scope)
	updates    []*func(scope *Scope)
	deletes    []*func(scope *Scope)
	queries    []*func(scope *Scope)
	rowQueries []*func(scope *Scope)
	processors []*CallbackProcessor
}

そしてこのDefaultCallbackにはcreatesという関数ポインタの配列がある。したがって、s.parent.callbacks.createsが指すのはここである。

DefaultCallbackの内容

DefaultCallbackのcreatesの中身がどうなっているのかみてみよう。

var DefaultCallback = &Callback{}

DefaultCallbackはcallback.go内で定義されているこの変数だ。この定義時点では中身は空だが、callback_create.goやcallback_delete.goのinit()関数の中で内容が追加されている。

func init() {
	DefaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback)
	DefaultCallback.Create().Register("gorm:before_create", beforeCreateCallback)
	DefaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback)
	DefaultCallback.Create().Register("gorm:update_time_stamp", updateTimeStampForCreateCallback)
	DefaultCallback.Create().Register("gorm:create", createCallback)
	DefaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback)
	DefaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback)
	DefaultCallback.Create().Register("gorm:after_create", afterCreateCallback)
	DefaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback)
}

紛らわしいメソッド名だが、Create関数では”create”関連のCallbackProcessorのインスタンスが作られる。

func (c *Callback) Create() *CallbackProcessor {
	return &CallbackProcessor{kind: "create", parent: c}
}

そして、Register関数内でparent(つまりここではDefaultCallback)のprocessorsにそのCallbackProcessorが登録される。

func (cp *CallbackProcessor) Register(callbackName string, callback func(scope *Scope)) {
	if cp.kind == "row_query" {
		...略...
	}

	cp.name = callbackName
	cp.processor = &callback
	cp.parent.processors = append(cp.parent.processors, cp)
	cp.parent.reorder()
}

プロセッサーの登録後、Callbackの方で種類毎(create / update / delete / query / row_query)にプロセッサーの並び替えが行われる。Register()の度に毎回ソートが行われているのは、各プロセッサーにはbefore / afterというフィールドがあり、依存関係を指定することができるためだと思われる。

func (c *Callback) reorder() {
	var creates, updates, deletes, queries, rowQueries []*CallbackProcessor

	for _, processor := range c.processors {
		if processor.name != "" {
			switch processor.kind {
			case "create":
				creates = append(creates, processor)
			case "update":
				updates = append(updates, processor)
			case "delete":
				deletes = append(deletes, processor)
			case "query":
				queries = append(queries, processor)
			case "row_query":
				rowQueries = append(rowQueries, processor)
			}
		}
	}

	c.creates = sortProcessors(creates)
	c.updates = sortProcessors(updates)
	c.deletes = sortProcessors(deletes)
	c.queries = sortProcessors(queries)
	c.rowQueries = sortProcessors(rowQueries)
}

sortProcessorsの中では、CallbackProcessorの配列を、それぞれのbefore / afterの依存関係に基づいてソートするような処理になっているが、長くなるため省略。

プロセッサーの中身

実際のプロセッサーの中身がどのような内容になっているのかは、長くなるため次のポストで読んでいく。

次の記事: GORMのソースコードを読む3 – Createその2

Pocket

「GORMのソースコードを読む2 – Createその1」への1件のフィードバック

コメントを残す

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