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

Other Decks in Technology

Transcript

  1. GoでORMを自作してみる
    第56回情報科学若手の会
    株式会社DeNA 黄 英智

    View Slide

  2. 黄 英智(Harrison Huang/ HarrisonEagle)
    ● 東京都板橋区出身
    ● 在日華僑三世
    ● 早稲田大学基幹理工学部情報通信学科卒
    ● 株式会社DeNA所属(23卒)
    ○ 現在はSREとして自社機械学習基盤の開発と運用を担当
    ● 〇〇自作はよくやる
    ○ RustでWebフレームワーク自作したこともある
    ○ 最近はJVM自作と仮想DOM自作にも挑んだりする
    ● GitHub: HarrisonEagle
    ● Twitter: @harris0n3ag1e

    View Slide

  3. これから話す内容
    ● 自作したGoのORM HawkORMの実装と内部の仕組みについて話します
    ○ リポジトリ: https://github.com/HarrisonEagle/HawkORM
    ● 特に下記の内容を中心に話ます
    ○ ORMの中身はどうなっているのか
    ○ 実行時にGoの構造体の情報を取得&変更する技術

    View Slide

  4. こんな人にとっては面白いかも
    ● 下記のようにDBのライブラリの内部実装に疑問を持っている人
    ○ なんでSQL書かずにDB操作できるんだろ?
    ○ 色んなクラス&構造体の情報はどうやって
    SQLに変換されているのだろ
    ○ そもそもDBライブラリの実装はどんなんだろ?セキュリティは担保されているのか?
    ○ ORMのようなDBライブラリの限界はなんなのか?

    View Slide

  5. こんな人にとっては面白くないかも...
    ● DB操作は生のSQLで行っている人
    ○ ORM使うよりむしろSQLそのまま書いた方がやりやすいなど
    ■ 「ORMばっか使ってると痛い目に遭うこともあるよ!

    ○ なんでORM使うより生のSQL書いた方が早いケースがあるのかはこのセッション通して理解でき
    ると思います

    View Slide

  6. そもそもORMってなんぞ?
    ● プログラミング言語で記述されたオブジェクトを、データベース上にある非互換なデータとマッピングする
    手法である。 オブジェクト関係マッピング
    とも呼ばれる。
    ● 本来DB上のデータを取得、あるいは変更を加える場合は、
    SQL文を書いてDBエンジンに実行させる必
    要があるが、これらの処理と、オブジェクトへの変換はすべてプログラミング言語だけで完結できる
    ● 代表ライブラリ: Ruby on railsのActiveRecord、TSのPrisma、GoのGORMなど

    View Slide

  7. HawkORMができること
    ● Goの構造体をSQLに変換し、DBエンジンで実行
    ○ 内部はdatabase/sqlを使用
    ○ 現在はMySQLだけサポート
    ● WhereやLimitなどの条件を指定した SQLクエリの生成
    ● 基本的なCRUD
    ● Select Join, TransactionとPreloadはWIP

    View Slide

  8. 使用例
    // 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()

    View Slide

  9. では実際どう動いているの?

    View Slide

  10. そもそもORMはどう動いているのか?
    オブジェク
    ト(Goの構
    造体)
    Whereな
    どの条件
    (HawkORM
    ではこちらも構
    造体)
    構造体情
    報の読み
    込み
    オブジェク
    ト(Goの構
    造体)
    SQLの生

    SQLの実
    行&結果
    の返却
    SQL結果を
    オブジェク
    トにマッピ
    ング
    ORM
    SELECTする場合
    に行われる

    View Slide

  11. 実行時の構造体情報の取得
    ● 多くのプログラミング言語では、実行時に変数や関数の情報を取得することができる
    リフレクション機能が
    提供されている
    ○ プログラムの実行時にプログラムの構造や構成要素(クラス、メソッド、関数など)についての情報を取得したり、
    プログラムの動作を動的に変更したりすることを リフレクションと言う
    ● Goの場合、リフレクション機能は標準パッケージの
    reflectに含まれている。
    ○ 実際Gormの場合は、標準の reflectパッケージで渡された構造体の解析と値のマッピングができる
    ○ reflectパッケージで変数を解析する際は、変数を interface{}型にキャストする必要がある
    ■ interface{}型はTypeScriptのany型と同様になんでも受け取れるので、こういった特殊な要件ではなけ
    れば使わない方が良い

    View Slide

  12. リフレクションでとれる構造体の情報
    ● 構造体の名前
    ● 各フィールドの名前
    ● 各フィールドにセットされた値
    ● 各フィールドのタグ情報
    ○ Gormと同様に、フィールドの TagにPrimaryKeyであることを明示したら PrimaryKeyとして処理できる
    ● 各フィールドのポインタとアドレス
    :
    ○ Selectした結果のオブジェクトマッピングに使用できる
    ● 各フィールドの型情報
    ○ 構造体なのか、それとも配列なのかの判定に使用

    View Slide

  13. 例: 構造体のフィールドを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

    View Slide

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

    View Slide

  15. 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句をビルド

    View Slide

  16. 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
    }

    View Slide

  17. SELECTした結果をオブジェクトにマッピングする
    ● SQLの実行結果は行ごとのデータが帰ってくる
    ● 返却すべきデータの構造は、最初の構造体情報を読みこむ段階で記憶しておく
    ○ HawkORMではまだ対応していないが、 Go1.18から使用可能になった Genericsを使う
    とより安全な方法で処理に使用した型を記憶できそう
    ● リフレクションを利用して、指定した構造体の型を元にマッピング先の構造体の初期化とポイ
    ンタの取得を行う
    ● SQL実行結果の各行をスキャンする際に、カラムとフィールドの順番を元に リフレクションで取
    得した構造体の各フィールドのポインタを渡すと、結果が構造体にマッピングできる

    View Slide

  18. 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として一通り成り立つようになる

    View Slide

  19. HawkORMの今後
    ● Selectに関してはJoinに対応させる
    ● 外部キーなどで親子関係が結ばれているオブジェクトについては Preloadに対応させる
    ● Genericsに対応してより型安全な方法で処理するように改良
    ● トランザクションの対応

    View Slide

  20. 実際作ってみてどうだったの?

    View Slide

  21. 考察1: ORMには限界がある
    ● ORMには限界がある
    ○ 例えばSELECT…FROMに設定したデータソースは、テーブルではなく他のサブクエリに
    設定したい場合は、 SQLのパターン化が複雑化し、 ORMだけで表現するのは難しくなる
    ■ ORM使うより生のSQL書いた方が早いケース
    ○ 型安全とデータオブジェクトの柔軟性を両立させる設計はプログラミング言語によって
    工夫が必要
    ■ 例えば、Joinでオブジェクトに含める内容が複数テーブルを跨いだ場合、型安全
    な設計でやろうとすると、プログラミング言語の特性によっては複雑化して開発者
    体験が悪くなりやすい
    ■ ActiveRecordやPrismaのようなDXをGoで完全再現するのは難しい

    View Slide

  22. 考察2: Reflectionは意外と危ない
    ● interface{}型とReflectionはちゃんと型チェックしたり、動的に構造体を変更する時に設計上
    のミスを防ぐ仕組みが乏しい
    ○ 実際結構使われている ORMライブラリでも、特殊なデータ構造を Reflectionで変更する
    際にnilポインタに引っ掛かって落ちるトラブルが起こることがある
    ○ Genericsを導入すればある程度型安全になるが、万能ではない
    ● Reflectionは実行時にメモリ上の変数を触ることなので、そもそも性能的によろしく無かったり
    する
    ○ 特に構造体の情報の解析が計算量が多い実装になっている場合
    ● より型安全な方法で実現したいのであれば、 Code Generationによる型を自動生成を活用し
    てReflectionを最小限に抑えるのが良さそう
    ○ Metaのentはこの方式
    ○ gormもCode Generationで型安全にしようとしている試みをしている

    View Slide

  23. 考察3: 〇〇自作は結構学びになる
    ● 車輪の再発明による成果は実用できるかどうかは置いといて、低レイヤーとライブラリ内部の
    仕組みを知ることは割と力になる
    ○ 現代のソフトウェア開発はライブラリとフレームワークを頼ることが多いが、その設計思
    想と仕組みを理解すれば、より柔軟性の高い設計に挑んだり、ライブラリの不具合に対
    し適切な解決策を出しやすくなる

    View Slide

  24. まとめ
    ● オブジェクトマッピングと、構造体の情報取得は、リフレクションを使えば実現できる
    ● SQL文をパターン化してリフレクションで取得した情報を元にビルドして実行することにより、
    ORMを実装できる
    ● リフレクションにはリスクがあり、使うのであれば最低限に抑えつつ、 ORM的なものを作る際
    はGenericsや型の自動生成なども活用した方が良い
    ● ORMには限界がある
    ● 〇〇自作はめっちゃ楽しいし、役に立つ知見も頭の中に入ってくる

    View Slide

  25. 参考文献
    ● 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

    View Slide

  26. ご清聴ありがとうございました!

    View Slide

  27. Q&A

    View Slide