引き続き、Createに関するコールバックを一つずつ定義されている順番に読んでいく。今回はbeforeCreateCallback。
// beforeCreateCallback will invoke `BeforeSave`, `BeforeCreate` method before creating
func beforeCreateCallback(scope *Scope) {
if !scope.HasError() {
scope.CallMethod("BeforeSave")
}
if !scope.HasError() {
scope.CallMethod("BeforeCreate")
}
}
beforeCreateCallbackの実装は、BeforeSaveとBeforeCreateという二つのメソッドを呼び出す処理で構成されている。
// HasError check if there are any error
func (scope *Scope) HasError() bool {
return scope.db.Error != nil
}
いずれも、呼び出す前にHasError()メソッドを呼び、前の処理でエラーが発生していないかを確認し、もしエラーが発生していた場合はメソッドの呼び出しを行わないようにしている。scope.dbはgorm.DBオブジェクトである。これ以前の処理、beginTransactionCallback内で行われるscope.Begin()、その内部で呼び出されるdb.Begin()で処理に失敗した場合は、このscope.db.Errorにエラー情報がセットされ、ここのメソッド呼び出しは行われなくなるようだ。
// CallMethod call scope value's method, if it is a slice, will call its element's method one by one
func (scope *Scope) CallMethod(methodName string) {
if scope.Value == nil {
return
}
if indirectScopeValue := scope.IndirectValue(); indirectScopeValue.Kind() == reflect.Slice {
for i := 0; i < indirectScopeValue.Len(); i++ {
scope.callMethod(methodName, indirectScopeValue.Index(i))
}
} else {
scope.callMethod(methodName, indirectScopeValue)
}
}
CallMethodの中身を見てみよう。
まず、冒頭部分にscope.Valueが空だった場合に、早期離脱し以降の処理をスキップするような処理が書かれている。以降のコードで直接scope.Valueを参照している箇所がないのにここでscope.Valueをチェックしているのは、後に呼び出しているscope.IndirectValue()の中でscope.Valueが参照されるためである。わかりにくい。
CallMethodはscopeのIndirectValue()の中身を引数にcallMethodを呼び直す、ということをしている。IndirectValue()でとってきた値がSliceの場合は、その要素の数分、一つずつ引数に渡してcallMethodを呼んでいる。
// IndirectValue return scope's reflect value's indirect value
func (scope *Scope) IndirectValue() reflect.Value {
return indirect(reflect.ValueOf(scope.Value))
}
IndirectValue()はscope.Valueの中身をindirectしたものを返している。
func indirect(reflectValue reflect.Value) reflect.Value {
for reflectValue.Kind() == reflect.Ptr {
reflectValue = reflectValue.Elem()
}
return reflectValue
}
indirectは、引数で与えられたオブジェクトがreflect.Ptr(つまりポインタ)だった場合、その参照先のオブジェクトを返す、いわゆるDereferenceをしている。ポインタの先がさらにポインタだった場合は、ポインタ以外のものが見つかるまで参照先を追い続けるような実装になっている。
なお、db.Create呼び出し時に作成されたScopeのValueには、db.Create()の引数つまりDBの保存したいモデルのインスタンスがセットされているので、ここのcallMethodで渡される引数indirectScopeValueの中身は保存したいモデルのインスタンスの実体、ということになる。
func (scope *Scope) callMethod(methodName string, reflectValue reflect.Value) {
// Only get address from non-pointer
if reflectValue.CanAddr() && reflectValue.Kind() != reflect.Ptr {
reflectValue = reflectValue.Addr()
}
if methodValue := reflectValue.MethodByName(methodName); methodValue.IsValid() {
switch method := methodValue.Interface().(type) {
case func():
method()
case func(*Scope):
method(scope)
case func(*DB):
newDB := scope.NewDB()
method(newDB)
scope.Err(newDB.Error)
case func() error:
scope.Err(method())
case func(*Scope) error:
scope.Err(method(scope))
case func(*DB) error:
newDB := scope.NewDB()
scope.Err(method(newDB))
scope.Err(newDB.Error)
default:
scope.Err(fmt.Errorf("unsupported function %v", methodName))
}
}
}
callMethodでは、リフレクションの仕組みを使って、reflectValue(=scope.Valueの実体)にある”methodName”で指定された名前のメソッドを呼び出している。”methodName”に該当するメソッドがない場合は特にエラーも発生させず、ただ単にスキップさせている。
つまり、モデルの方にBeforeSaveやBeforeCreateというメソッドを定義しておくと、このタイミングでそれが呼び出される、ということである。これらのメソッドは使いたい時だけ使えれば良いので、必要なければモデルに定義しなくてもよく、その場合はただ単に処理がスキップされるだけになる。
面白いのは、リフレクションによってメソッドの型に応じた引数の準備と呼び出しが行えるようになっていることである。コールバックメソッド定義側(アプリケーション側)で、その時のgorm.DBあるいはScopeを参照したければ、引数の型として定義しておけば参照できる、という仕組みが面白い。(後で混乱の原因にもなりそうだが)
また、返り値でerrorを返すようなメソッドを定義し、そこでエラーを返すようにすれば、scope.Errによってscope.db.Errorにその内容がセットされ、以降先ほどのscope.HasError()がエラーを返すようになり、処理を中断することができるようになるわけだが、エラーを返す可能性がない場合は返り値なしでメソッドを定義しておけば無駄なreturnがなくなりコードをスッキリさせることができる。
というわけで、beforeCreateCallbackはトランザクションの開始直後、Create()の引数に渡されたモデルにBeforeSave, BeforeCreateというメソッドが定義されていれば、それを呼び出す処理を行なっていた。
「GORMのソースコードを読む4 – Createその3」への1件のフィードバック