Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GoでORMを自作してみる

 GoでORMを自作してみる

第56回情報科学若手の会で発表したスライドです
自作したGoのORMのその仕組みについて話します

YingZhi "Harrison" Huang

October 07, 2023
Tweet

More Decks by YingZhi "Harrison" Huang

Other Decks in Technology

Transcript

  1. 黄 英智(Harrison Huang/ HarrisonEagle) • 東京都板橋区出身 • 在日華僑三世 • 早稲田大学基幹理工学部情報通信学科卒

    • 株式会社DeNA所属(23卒) ◦ 現在はSREとして自社機械学習基盤の開発と運用を担当 • 〇〇自作はよくやる ◦ RustでWebフレームワーク自作したこともある ◦ 最近はJVM自作と仮想DOM自作にも挑んだりする • GitHub: HarrisonEagle • Twitter: @harris0n3ag1e
  2. 使用例 // equal to: SELECT id, name, email, created_at, updated_at

    FROM users WHERE (id = "userId" OR name = "testname") ORDER BY id ASC LIMIT 1 var users []User testDB.Select(&User{}).WhereOr(&User{ID: "userId", Name: "testname"}).First(&users) insertTest := []User{ {ID: "userid1", Name: "user1", Email: "[email protected]"}, {ID: "userid2", Name: "user2", Email: "[email protected]"}, } // equal to: INSERT INTO users (id, name, email) VALUES ("userid1", "user1", "[email protected]"), ("userid2", "user2", "[email protected]") res, err := testDB.Insert(&User{}).SetData(insertTest).Exec()
  3. そもそもORMはどう動いているのか? オブジェク ト(Goの構 造体) Whereな どの条件 (HawkORM ではこちらも構 造体) 構造体情

    報の読み 込み オブジェク ト(Goの構 造体) SQLの生 成 SQLの実 行&結果 の返却 SQL結果を オブジェク トにマッピ ング ORM SELECTする場合 に行われる
  4. 実行時の構造体情報の取得 • 多くのプログラミング言語では、実行時に変数や関数の情報を取得することができる リフレクション機能が 提供されている ◦ プログラムの実行時にプログラムの構造や構成要素(クラス、メソッド、関数など)についての情報を取得したり、 プログラムの動作を動的に変更したりすることを リフレクションと言う •

    Goの場合、リフレクション機能は標準パッケージの reflectに含まれている。 ◦ 実際Gormの場合は、標準の reflectパッケージで渡された構造体の解析と値のマッピングができる ◦ reflectパッケージで変数を解析する際は、変数を interface{}型にキャストする必要がある ▪ interface{}型はTypeScriptのany型と同様になんでも受け取れるので、こういった特殊な要件ではなけ れば使わない方が良い
  5. リフレクションでとれる構造体の情報 • 構造体の名前 • 各フィールドの名前 • 各フィールドにセットされた値 • 各フィールドのタグ情報 ◦

    Gormと同様に、フィールドの TagにPrimaryKeyであることを明示したら PrimaryKeyとして処理できる • 各フィールドのポインタとアドレス : ◦ Selectした結果のオブジェクトマッピングに使用できる • 各フィールドの型情報 ◦ 構造体なのか、それとも配列なのかの判定に使用
  6. 例: 構造体のフィールドをSQLで指定するカラムに変換    typeInfo := reflect.TypeOf(model) // 構造体内の各フィールドの型情報を取得 valueInfo := reflect.ValueOf(model) //

    構造体内の各フィールドの値を取得 var columns []string for i := 0; i < typeInfo.NumField(); i++ { fieldType := typeInfo.Field(i).Type.Kind() // 構造体内の各フィールドの型を取得 value := valueInfo.Field(i) if notZeroOnly && value.IsZero() { continue }        // 型をチェックしてテーブルのフィールドなのか判断する if !(fieldType == reflect.Struct && typeInfo.Field(i).Type.String() != "time.Time") && fieldType != reflect.Array {            // 構造体の各フィールド名を SQLに入れるカラム情報として処理 columns = append(columns, p.getColumnName(typeInfo.Field(i).Name)) } } return columns
  7. SQL構築に必要な情報の準備&実行 SELECT id, name FROM users WHERE users.id IN (1,

    2, 3) ORDER BY users.id ASC LIMIT 10 SQL句は、どの位置になんの情報を入れるか、どういう順番で SQL 文を構築していくのか文法が決まっている 選択するカラム(最初に来る) データソース(カラムの後に置かれる) 選択条件(データソースの後に置かれる) どのソースを元に並べるのか 昇順か降順 件数の制限
  8. SQL構築に必要な情報の準備&実行 type MySQLSelectClause struct { dbpool *sql.DB Processor *utils.Processor primaryKey

    string tableName string columns []string whereConditions *WhereCondition orderBy []string limit int } SELECT id, name FROM users WHERE users.id IN (1, 2, 3) LIMIT 10 赤の部分が、リフレクションを通し て取得できたオブジェクトの情報で ある SQL句の構造をパターン化して、リフレクション から得られた情報を Clause構造体にまとめて管 理する。 Clause構造体 からSQL句をビルド
  9. SQL構築に必要な情報の準備&実行 • DBによってSQLの構文が微妙に違うこと があるので、SQL操作を抽象化する • メソッドチェーン方式で、条件設定などの 操作を行う度に、リフレクションを通して Clause構造体に必要な情報がセットして SQLを構築する ◦

    Gormも似たような方式 • 最終的に、ビルドしたSQL句を、DBクライ アント(今回はdatabase/sql)に渡して実行 する type SelectClause interface { Limit(number int) SelectClause OrderBy(orderBy []string) SelectClause Where(condition interface{}) SelectClause WhereNot(condition interface{}) SelectClause WhereOr(condition interface{}) SelectClause All(target interface{}) error First(target interface{}) error Last(target interface{}) error }
  10. SELECTした結果をオブジェクトにマッピングする • SQLの実行結果は行ごとのデータが帰ってくる • 返却すべきデータの構造は、最初の構造体情報を読みこむ段階で記憶しておく ◦ HawkORMではまだ対応していないが、 Go1.18から使用可能になった Genericsを使う とより安全な方法で処理に使用した型を記憶できそう

    • リフレクションを利用して、指定した構造体の型を元にマッピング先の構造体の初期化とポイ ンタの取得を行う • SQL実行結果の各行をスキャンする際に、カラムとフィールドの順番を元に リフレクションで取 得した構造体の各フィールドのポインタを渡すと、結果が構造体にマッピングできる
  11. SELECTした結果をオブジェクトにマッピングする for rows.Next() { var columns []interface{} result := reflect.New(typeinf).Elem().Addr().Interface()

    // マッピング用構造体の初期化 s.assignFromArgs(result, &columns) // 各カラムを構造体のフィールドに紐付ける err := rows.Scan(columns...) // SQL実行結果を各カラムに対応するフィールドにマッピング if value.Kind() == reflect.Slice { // 構造体と配列の判定 value.Set(reflect.Append(value, reflect.Indirect(reflect.ValueOf(result)))) } else if value.Kind() == reflect.Struct { // reflect.Indirectでスキャンした結果を構造体にセット value.Set(reflect.Indirect(reflect.ValueOf(result))) } if err != nil { return err } } ここまで来たらORMとして一通り成り立つようになる
  12. 考察1: ORMには限界がある • ORMには限界がある ◦ 例えばSELECT…FROMに設定したデータソースは、テーブルではなく他のサブクエリに 設定したい場合は、 SQLのパターン化が複雑化し、 ORMだけで表現するのは難しくなる ▪

    ORM使うより生のSQL書いた方が早いケース ◦ 型安全とデータオブジェクトの柔軟性を両立させる設計はプログラミング言語によって 工夫が必要 ▪ 例えば、Joinでオブジェクトに含める内容が複数テーブルを跨いだ場合、型安全 な設計でやろうとすると、プログラミング言語の特性によっては複雑化して開発者 体験が悪くなりやすい ▪ ActiveRecordやPrismaのようなDXをGoで完全再現するのは難しい
  13. 考察2: Reflectionは意外と危ない • interface{}型とReflectionはちゃんと型チェックしたり、動的に構造体を変更する時に設計上 のミスを防ぐ仕組みが乏しい ◦ 実際結構使われている ORMライブラリでも、特殊なデータ構造を Reflectionで変更する 際にnilポインタに引っ掛かって落ちるトラブルが起こることがある

    ◦ Genericsを導入すればある程度型安全になるが、万能ではない • Reflectionは実行時にメモリ上の変数を触ることなので、そもそも性能的によろしく無かったり する ◦ 特に構造体の情報の解析が計算量が多い実装になっている場合 • より型安全な方法で実現したいのであれば、 Code Generationによる型を自動生成を活用し てReflectionを最小限に抑えるのが良さそう ◦ Metaのentはこの方式 ◦ gormもCode Generationで型安全にしようとしている試みをしている
  14. 参考文献 • The Laws of Reflection: https://go.dev/blog/laws-of-reflection • reflect: https://pkg.go.dev/reflect

    • go-gorm/gorm: https://github.com/go-gorm/gorm • go-gorm/gen: https://github.com/go-gorm/gen • ent/ent: https://github.com/ent/ent
  15. Q&A