Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
GoでORMを自作してみる
Search
YingZhi "Harrison" Huang
October 07, 2023
Technology
5
2.3k
GoでORMを自作してみる
第56回情報科学若手の会で発表したスライドです
自作したGoのORMのその仕組みについて話します
YingZhi "Harrison" Huang
October 07, 2023
Tweet
Share
More Decks by YingZhi "Harrison" Huang
See All by YingZhi "Harrison" Huang
RustでWebフロント作れるらしい
harrisoneagle
0
380
Next.js 14+Cognito +DynamoDB+Amplifyで認証付きのCRUDアプリを構築してみる
harrisoneagle
0
910
Other Decks in Technology
See All in Technology
で、ValhallaのValue Classってどうなったの?
skrb
1
600
リファクタリングへの耐性が高いモデルベースの統合テストの紹介 / Model-Base Integration Test for Refactoring
yuitosato
5
1.5k
Aurora_BlueGreenDeploymentsやってみた
tsukasa_ishimaru
1
120
Jr. Championsになって、強く連携しながらAWSをもっと使いたい!~AWSに対する期待と行動~
amixedcolor
0
100
急成長中のWINTICKETにおける品質と開発スピードと向き合ったQA戦略と今後の展望 / winticket-autify
cyberagentdevelopers
PRO
1
150
CyberAgent 生成AI Deep Dive with Amazon Web Services / genai-aws
cyberagentdevelopers
PRO
1
410
Nix入門パラダイム編
asa1984
2
170
Data Migration on Rails
ohbarye
7
4.9k
Kubernetes Summit 2024 Keynote:104 在 GitOps 大規模實踐中的甜蜜與苦澀
yaosiang
0
280
ABEMA のコンテンツ制作を最適化!生成 AI x クラウド映像編集システム / abema-ai-editor
cyberagentdevelopers
PRO
1
160
APIテスト自動化の勘所
yokawasa
6
3.2k
Apple/Google/Amazonの決済システムの違いを踏まえた定期購読課金システムの構築 / abema-billing-system
cyberagentdevelopers
PRO
1
190
Featured
See All Featured
Large-scale JavaScript Application Architecture
addyosmani
510
110k
A Tale of Four Properties
chriscoyier
156
23k
No one is an island. Learnings from fostering a developers community.
thoeni
19
3k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
41
9.2k
Building Adaptive Systems
keathley
38
2.2k
Mobile First: as difficult as doing things right
swwweet
222
8.9k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
355
29k
Fireside Chat
paigeccino
32
3k
Gamification - CAS2011
davidbonilla
80
5k
How to Think Like a Performance Engineer
csswizardry
19
1.1k
GraphQLの誤解/rethinking-graphql
sonatard
66
9.9k
GraphQLとの向き合い方2022年版
quramy
43
13k
Transcript
GoでORMを自作してみる 第56回情報科学若手の会 株式会社DeNA 黄 英智
黄 英智(Harrison Huang/ HarrisonEagle) • 東京都板橋区出身 • 在日華僑三世 • 早稲田大学基幹理工学部情報通信学科卒
• 株式会社DeNA所属(23卒) ◦ 現在はSREとして自社機械学習基盤の開発と運用を担当 • 〇〇自作はよくやる ◦ RustでWebフレームワーク自作したこともある ◦ 最近はJVM自作と仮想DOM自作にも挑んだりする • GitHub: HarrisonEagle • Twitter: @harris0n3ag1e
これから話す内容 • 自作したGoのORM HawkORMの実装と内部の仕組みについて話します ◦ リポジトリ: https://github.com/HarrisonEagle/HawkORM • 特に下記の内容を中心に話ます ◦
ORMの中身はどうなっているのか ◦ 実行時にGoの構造体の情報を取得&変更する技術
こんな人にとっては面白いかも • 下記のようにDBのライブラリの内部実装に疑問を持っている人 ◦ なんでSQL書かずにDB操作できるんだろ? ◦ 色んなクラス&構造体の情報はどうやって SQLに変換されているのだろ ◦ そもそもDBライブラリの実装はどんなんだろ?セキュリティは担保されているのか?
◦ ORMのようなDBライブラリの限界はなんなのか?
こんな人にとっては面白くないかも... • DB操作は生のSQLで行っている人 ◦ ORM使うよりむしろSQLそのまま書いた方がやりやすいなど ▪ 「ORMばっか使ってると痛い目に遭うこともあるよ! 」 ◦ なんでORM使うより生のSQL書いた方が早いケースがあるのかはこのセッション通して理解でき
ると思います
そもそもORMってなんぞ? • プログラミング言語で記述されたオブジェクトを、データベース上にある非互換なデータとマッピングする 手法である。 オブジェクト関係マッピング とも呼ばれる。 • 本来DB上のデータを取得、あるいは変更を加える場合は、 SQL文を書いてDBエンジンに実行させる必 要があるが、これらの処理と、オブジェクトへの変換はすべてプログラミング言語だけで完結できる
• 代表ライブラリ: Ruby on railsのActiveRecord、TSのPrisma、GoのGORMなど
HawkORMができること • Goの構造体をSQLに変換し、DBエンジンで実行 ◦ 内部はdatabase/sqlを使用 ◦ 現在はMySQLだけサポート • WhereやLimitなどの条件を指定した SQLクエリの生成
• 基本的なCRUD • Select Join, TransactionとPreloadはWIP
使用例 // 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()
では実際どう動いているの?
そもそもORMはどう動いているのか? オブジェク ト(Goの構 造体) Whereな どの条件 (HawkORM ではこちらも構 造体) 構造体情
報の読み 込み オブジェク ト(Goの構 造体) SQLの生 成 SQLの実 行&結果 の返却 SQL結果を オブジェク トにマッピ ング ORM SELECTする場合 に行われる
実行時の構造体情報の取得 • 多くのプログラミング言語では、実行時に変数や関数の情報を取得することができる リフレクション機能が 提供されている ◦ プログラムの実行時にプログラムの構造や構成要素(クラス、メソッド、関数など)についての情報を取得したり、 プログラムの動作を動的に変更したりすることを リフレクションと言う •
Goの場合、リフレクション機能は標準パッケージの reflectに含まれている。 ◦ 実際Gormの場合は、標準の reflectパッケージで渡された構造体の解析と値のマッピングができる ◦ reflectパッケージで変数を解析する際は、変数を interface{}型にキャストする必要がある ▪ interface{}型はTypeScriptのany型と同様になんでも受け取れるので、こういった特殊な要件ではなけ れば使わない方が良い
リフレクションでとれる構造体の情報 • 構造体の名前 • 各フィールドの名前 • 各フィールドにセットされた値 • 各フィールドのタグ情報 ◦
Gormと同様に、フィールドの TagにPrimaryKeyであることを明示したら PrimaryKeyとして処理できる • 各フィールドのポインタとアドレス : ◦ Selectした結果のオブジェクトマッピングに使用できる • 各フィールドの型情報 ◦ 構造体なのか、それとも配列なのかの判定に使用
例: 構造体のフィールドを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
SQL構築に必要な情報の準備&実行 SELECT id, name FROM users WHERE users.id IN (1,
2, 3) ORDER BY users.id ASC LIMIT 10 SQL句は、どの位置になんの情報を入れるか、どういう順番で SQL 文を構築していくのか文法が決まっている 選択するカラム(最初に来る) データソース(カラムの後に置かれる) 選択条件(データソースの後に置かれる) どのソースを元に並べるのか 昇順か降順 件数の制限
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句をビルド
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 }
SELECTした結果をオブジェクトにマッピングする • SQLの実行結果は行ごとのデータが帰ってくる • 返却すべきデータの構造は、最初の構造体情報を読みこむ段階で記憶しておく ◦ HawkORMではまだ対応していないが、 Go1.18から使用可能になった Genericsを使う とより安全な方法で処理に使用した型を記憶できそう
• リフレクションを利用して、指定した構造体の型を元にマッピング先の構造体の初期化とポイ ンタの取得を行う • SQL実行結果の各行をスキャンする際に、カラムとフィールドの順番を元に リフレクションで取 得した構造体の各フィールドのポインタを渡すと、結果が構造体にマッピングできる
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として一通り成り立つようになる
HawkORMの今後 • Selectに関してはJoinに対応させる • 外部キーなどで親子関係が結ばれているオブジェクトについては Preloadに対応させる • Genericsに対応してより型安全な方法で処理するように改良 • トランザクションの対応
実際作ってみてどうだったの?
考察1: ORMには限界がある • ORMには限界がある ◦ 例えばSELECT…FROMに設定したデータソースは、テーブルではなく他のサブクエリに 設定したい場合は、 SQLのパターン化が複雑化し、 ORMだけで表現するのは難しくなる ▪
ORM使うより生のSQL書いた方が早いケース ◦ 型安全とデータオブジェクトの柔軟性を両立させる設計はプログラミング言語によって 工夫が必要 ▪ 例えば、Joinでオブジェクトに含める内容が複数テーブルを跨いだ場合、型安全 な設計でやろうとすると、プログラミング言語の特性によっては複雑化して開発者 体験が悪くなりやすい ▪ ActiveRecordやPrismaのようなDXをGoで完全再現するのは難しい
考察2: Reflectionは意外と危ない • interface{}型とReflectionはちゃんと型チェックしたり、動的に構造体を変更する時に設計上 のミスを防ぐ仕組みが乏しい ◦ 実際結構使われている ORMライブラリでも、特殊なデータ構造を Reflectionで変更する 際にnilポインタに引っ掛かって落ちるトラブルが起こることがある
◦ Genericsを導入すればある程度型安全になるが、万能ではない • Reflectionは実行時にメモリ上の変数を触ることなので、そもそも性能的によろしく無かったり する ◦ 特に構造体の情報の解析が計算量が多い実装になっている場合 • より型安全な方法で実現したいのであれば、 Code Generationによる型を自動生成を活用し てReflectionを最小限に抑えるのが良さそう ◦ Metaのentはこの方式 ◦ gormもCode Generationで型安全にしようとしている試みをしている
考察3: 〇〇自作は結構学びになる • 車輪の再発明による成果は実用できるかどうかは置いといて、低レイヤーとライブラリ内部の 仕組みを知ることは割と力になる ◦ 現代のソフトウェア開発はライブラリとフレームワークを頼ることが多いが、その設計思 想と仕組みを理解すれば、より柔軟性の高い設計に挑んだり、ライブラリの不具合に対 し適切な解決策を出しやすくなる
まとめ • オブジェクトマッピングと、構造体の情報取得は、リフレクションを使えば実現できる • SQL文をパターン化してリフレクションで取得した情報を元にビルドして実行することにより、 ORMを実装できる • リフレクションにはリスクがあり、使うのであれば最低限に抑えつつ、 ORM的なものを作る際 はGenericsや型の自動生成なども活用した方が良い
• ORMには限界がある • 〇〇自作はめっちゃ楽しいし、役に立つ知見も頭の中に入ってくる
参考文献 • 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
ご清聴ありがとうございました!
Q&A