雑なメモ書き

気楽にいきます

gormをしらべた(1)

  • https://gorm.io/docs/
  • gormを使っていたら中身が気になったので追ってみた
  • 公式のサンプルからとりあえず追う

dialectsMapはどこで初期化されているか

  • gorm.initからRegisterDialectが呼ばれている
  • ちなみに、このinitはpostgresなど各種にあり、それぞれ初期化が順に呼ばれる
func init() {
    RegisterDialect("common", &commonDialect{})
}
  • dialectsMapに渡されて初期化されている
// RegisterDialect register new dialect
func RegisterDialect(name string, dialect Dialect) {
    dialectsMap[name] = dialect
}

gorm.Open

  • 対象となるdbの種類とsourceを受け取り
  • sql.Openをしている
  • connectionが生きてるかどうかを判定するためにPingを送っている
func Open(dialect string, args ...interface{}) (db *DB, err error) {
    if len(args) == 0 {
        err = errors.New("invalid database source")
        return nil, err
    }
    var source string
    var dbSQL SQLCommon
    var ownDbSQL bool

    switch value := args[0].(type) {
    case string:
        var driver = dialect
        if len(args) == 1 {
            source = value
        } else if len(args) >= 2 {
            driver = value
            source = args[1].(string)
        }
        dbSQL, err = sql.Open(driver, source)
        ownDbSQL = true
    case SQLCommon:
        dbSQL = value
        ownDbSQL = false
    default:
        return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
    }

    db = &DB{
        db:        dbSQL,
        logger:    defaultLogger,
        callbacks: DefaultCallback,
        dialect:   newDialect(dialect, dbSQL),
    }
    db.parent = db
    if err != nil {
        return
    }
    // Send a ping to make sure the database connection is alive.
    if d, ok := dbSQL.(*sql.DB); ok {
        if err = d.Ping(); err != nil && ownDbSQL {
            d.Close()
        }
    }
    return
}

AutoMigrate

// Migrate the schema
db.AutoMigrate(&Product{})
  • AutoMigrateの中身がこれ
func (s *DB) AutoMigrate(values ...interface{}) *DB {
    db := s.Unscoped()
    for _, value := range values {
        db = db.NewScope(value).autoMigrate().db
    }
    return db
}
  • Unscopedというメソッドが実行されている
  • このメソッドは削除されたレコードを含む全てのレコードを返すようだ
// Unscoped return all record including deleted record, refer Soft Delete https://jinzhu.github.io/gorm/crud.html#soft-delete
func (s *DB) Unscoped() *DB {
    return s.clone().search.unscoped().db
}
  • 最初のcloneはこう
  • 新しいdbオブジェクトを生成している
  • 基本的に元のdbの情報を参照している大本は同じ物を参照しているので
  • 名前の通りcloneしている
  • newDialectはnewしているので別の物がいる
  • direct自体はsqlの方言を吸収するための物らしい
func (s *DB) clone() *DB {
    db := &DB{
        db:                s.db,
        parent:            s.parent,
        logger:            s.logger,
        logMode:           s.logMode,
        Value:             s.Value,
        Error:             s.Error,
        blockGlobalUpdate: s.blockGlobalUpdate,
        dialect:           newDialect(s.dialect.GetName(), s.db),
        nowFuncOverride:   s.nowFuncOverride,
    }

    s.values.Range(func(k, v interface{}) bool {
        db.values.Store(k, v)
        return true
    })

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

    db.search.db = db
    return db
}
  • s.dialect.GetName()はこの場合接続しているのがsqlite3なので
  • このようにsqlite3という文字列を返している
func (sqlite3) GetName() string {
    return "sqlite3"
}
  • newDialectはsqlite3とdbを引数に取る
  • dialectsMapにデータが入っているので
  • 特定dbのdirectへ接続情報をもたせて返す
  • valuesにはsync.Mapが入っているのでこのデータも新しいdbへ引き継がせる
func newDialect(name string, db SQLCommon) Dialect {
    if value, ok := dialectsMap[name]; ok {
        dialect := reflect.New(reflect.TypeOf(value).Elem()).Interface().(Dialect)
        dialect.SetDB(db)
        return dialect
    }

    fmt.Printf("`%v` is not officially supported, running under compatibility mode.\n", name)
    commontDialect := &commonDialect{}
    commontDialect.SetDB(db)
    return commontDialect
}
  • unscopedはsearchのUnscopedへtrueを立てて返す
func (s *search) unscoped() *search {
    s.Unscoped = true
    return s
}

First

  • Firstを呼んだ場合
  • 最初にNewCospeが呼ばれる
  • search.Limitを1に設定
func (s *DB) First(out interface{}, where ...interface{}) *DB {
    newScope := s.NewScope(out)
    newScope.Search.Limit(1)

    return newScope.Set("gorm:order_by_primary_key", "ASC").
        inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db
}

NewScope

  • 現在実行している命令のスコープを作成して返す
func (s *DB) NewScope(value interface{}) *Scope {
    dbClone := s.clone()
    dbClone.Value = value
    scope := &Scope{db: dbClone, Value: value}
    if s.search != nil {
        scope.Search = s.search.clone()
    } else {
        scope.Search = &search{}
    }
    return scope
}

callCallbacks

  • callCallbacksはs.parent.callbacks.queriesに設定されているクエリを順に実行する
    • queryCallback
    • preloadCallback
    • afterQueryCallback
func (scope *Scope) callCallbacks(funcs []*func(s *Scope)) *Scope {
    defer func() {
        if err := recover(); err != nil {
            if db, ok := scope.db.db.(sqlTx); ok {
                db.Rollback()
            }
            panic(err)
        }
    }()
    for _, f := range funcs {
        (*f)(scope)
        if scope.skipLeft {
            break
        }
    }
    return scope
}
  • callbacksはinitで登録されている
// Define callbacks for querying
func init() {
    DefaultCallback.Query().Register("gorm:query", queryCallback)
    DefaultCallback.Query().Register("gorm:preload", preloadCallback)
    DefaultCallback.Query().Register("gorm:after_query", afterQueryCallback)
}

queryCallback

  • ここまでの情報をもってqueryCallbackでクエリを組み立ててリクエストを送っている
// queryCallback used to query data from database
func queryCallback(scope *Scope) {
    if _, skip := scope.InstanceGet("gorm:skip_query_callback"); skip {
        return
    }

    //we are only preloading relations, dont touch base model
    if _, skip := scope.InstanceGet("gorm:only_preload"); skip {
        return
    }

    defer scope.trace(scope.db.nowFunc())

    var (
        isSlice, isPtr bool
        resultType     reflect.Type
        results        = scope.IndirectValue()
    )

    if orderBy, ok := scope.Get("gorm:order_by_primary_key"); ok {
        if primaryField := scope.PrimaryField(); primaryField != nil {
            scope.Search.Order(fmt.Sprintf("%v.%v %v", scope.QuotedTableName(), scope.Quote(primaryField.DBName), orderBy))
        }
    }

    if value, ok := scope.Get("gorm:query_destination"); ok {
        results = indirect(reflect.ValueOf(value))
    }

    if kind := results.Kind(); kind == reflect.Slice {
        isSlice = true
        resultType = results.Type().Elem()
        results.Set(reflect.MakeSlice(results.Type(), 0, 0))

        if resultType.Kind() == reflect.Ptr {
            isPtr = true
            resultType = resultType.Elem()
        }
    } else if kind != reflect.Struct {
        scope.Err(errors.New("unsupported destination, should be slice or struct"))
        return
    }

    scope.prepareQuerySQL()

    if !scope.HasError() {
        scope.db.RowsAffected = 0
        if str, ok := scope.Get("gorm:query_option"); ok {
            scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
        }

        if rows, err := scope.SQLDB().Query(scope.SQL, scope.SQLVars...); scope.Err(err) == nil {
            defer rows.Close()

            columns, _ := rows.Columns()
            for rows.Next() {
                scope.db.RowsAffected++

                elem := results
                if isSlice {
                    elem = reflect.New(resultType).Elem()
                }

                scope.scan(rows, columns, scope.New(elem.Addr().Interface()).Fields())

                if isSlice {
                    if isPtr {
                        results.Set(reflect.Append(results, elem.Addr()))
                    } else {
                        results.Set(reflect.Append(results, elem))
                    }
                }
            }

            if err := rows.Err(); err != nil {
                scope.Err(err)
            } else if scope.db.RowsAffected == 0 && !isSlice {
                scope.Err(ErrRecordNotFound)
            }
        }
    }
}

気が向いたらもう少し追います