Slide 1

Slide 1 text

© 2024 ANDPAD All Rights Reserved. database/sql/driverを理解して カスタムデータベースドライバーを作る 小島 夏海 1

Slide 2

Slide 2 text

© 2024 ANDPAD All Rights Reserved. 2 自己紹介 小島 夏海 (こじま なつみ) : replu : replu5 ● アンドパッドで社内向けの通知基盤の開発・運用 ● ブースがあるのでぜひ来てね

Slide 3

Slide 3 text

© 2024 ANDPAD All Rights Reserved. セッション内容 3 ● セッションで得られるもの ○ database/sql パッケージ と database/sql/driver パッケージの関係 ○ database/sql/driverについての理解 ○ 既存のドライバーをカスタマイズしたドライバーを作る方法 ○ 自作ドライバーの実装が問題ないか確認する方法 ● セッションで得られないもの ○ 0からドライバーを実装する方法

Slide 4

Slide 4 text

© 2024 ANDPAD All Rights Reserved. database/sql/driverを理解して カスタムデータベースドライバーを作る 小島 夏海 4

Slide 5

Slide 5 text

© 2024 ANDPAD All Rights Reserved. カスタムドライバーってなに 5 ● 既存のデータベースドライバーに機能を追加したドライバー ○ 例えば ■ DB操作時にログを出力したい ■ クエリによってwriter/readerどちらにクエリを発行するか切り替えたい ● DBとやりとりする部分は基本的に既存ドライバーに任せる database/sql 既存ドライバー DB カスタムドライバー

Slide 6

Slide 6 text

© 2024 ANDPAD All Rights Reserved. なぜカスタムドライバーを作りたいのか 6 ● DB操作すべてを起因として実行したい処理は共通化して管理したい ○ アプリケーション側のDB操作を実行するすべての箇所で実装するのは面倒 ● 既存のドライバー・DBライブラリがサポートしていない機能を ドライバーで実行したい ○ 例えばsqlcを使用し、MySQLに対してリクエスト実行時にログを出力したい ■ MySQLのドライバーはログ出力をサポートしていない ■ sqlc pluginという手段もあるがgoのコード生成部分はinternal ディレクトリ内 にあり、既存コードが活用できない ● 既存のドライバー・DBライブラリがサポートしていない機能を 追加したい時にどうするか ○ PRを送る ■ 汎用的な需要がある機能ならよさそう ● 既存機能への影響がある場合などはマージが難しいこともありえる ○ 既存ドライバーに機能を追加したカスタムドライバーを作成する ■ ニッチな課題やドメイン固有の課題の解決には向いている ●

Slide 7

Slide 7 text

© 2024 ANDPAD All Rights Reserved. どうやって既存ドライバーに機能を追加していくか 7 ● forkする ○ 本体に追従するコストが大きい ○ ドライバー本体に対する深い理解が必要 ● 既存のドライバーをラップしたドライバー(カスタムドライバー)を作成する ○ DBとやりとりする部分は基本的に既存ドライバーに任せる ○ ラップ部分は共通のため複数のドライバーに対応可能 ○ DB操作に関してのテストを既存ドライバーとカスタムドライバーの両方で 実行することで既存ドライバーのバージョンアップの影響がないかは確認可能 database/sql 既存ドライバー DB カスタムドライバー

Slide 8

Slide 8 text

© 2024 ANDPAD All Rights Reserved. GoにおけるDB操作の構成 8 db, err := sql.Open("mysql", os.Getenv("DataSource")) rows, err := db.QueryContext(ctx, "SELECT name FROM users") db, err := sql.Open("postgres", os.Getenv("DataSource")) rows, err := db.QueryContext(ctx, "SELECT name FROM users") db, err := sql.Open("sqlite3", os.Getenv("DataSource")) rows, err := db.QueryContext(ctx, "SELECT name FROM users") go-sql-driver/mysql の場合 mattn/go-sqlite3 の場合 lib/pq の場合 8

Slide 9

Slide 9 text

© 2024 ANDPAD All Rights Reserved. GoにおけるDB操作の構成 >> go tool doc database/sql Open package sql // import "database/sql" func Open(driverName, dataSourceName string) (*DB, error) Open opens a database specified by its database driver name and a driver-specific data source name, usually consisting of at least a database name and connection information.  日本語訳 Open関数は、データベースドライバ名と、ドライバ固有のデータソース名(通常、少なくとも データベース名と接続情報で構成されます)を指定することで、データベースを開きます。 9

Slide 10

Slide 10 text

© 2024 ANDPAD All Rights Reserved. GoにおけるDB操作の構成 10 ああ Application Code ああ database/sql ああ go-sql-driver/mysql ああ lib/pq ああ mattn/go-sqlite3

Slide 11

Slide 11 text

© 2024 ANDPAD All Rights Reserved. GoにおけるDB操作の構成 11 ああ Application Code ああ database/sql ああ go-sql-driver/mysql ああ lib/pq ああ mattn/go-sqlite3 database/sql/driver パッケージに interfaceが定義されている

Slide 12

Slide 12 text

© 2024 ANDPAD All Rights Reserved. database/sql パッケージとは 12 ● データベース操作の統一的な抽象化レイヤーを提供 ● コネクションプール管理 ● 型変換(driver.Value ⇔ Go型) ● 詳しくは Go Conference 2022 Springでsivchariさんが発表している 「database/sqlパッケージを理解する」[1]を参照 [1] https://gocon.jp/2022spring/sessions/a8-s/

Slide 13

Slide 13 text

© 2024 ANDPAD All Rights Reserved. database/sql/driver パッケージとは 13 ● データベース ドライバが実装すべきインターフェイスを定義 ● database/sql パッケージはこのインターフェイスを介してドライバと やり取りする ああ database/sql ああ go-sql-driver/mysql ああ lib/pq ああ mattn/go-sqlite3 database/sql/driver パッケージに interfaceが定義されている

Slide 14

Slide 14 text

© 2024 ANDPAD All Rights Reserved. database/sql/driver パッケージのgo docをみていく 14 https://pkg.go.dev/database/sql/driver

Slide 15

Slide 15 text

© 2024 ANDPAD All Rights Reserved. >> go doc database/sql/driver | grep type | grep interface database/sql/driver に定義されている interface 15 type ColumnConverter interface{ ... } type Conn interface{ ... } type ConnBeginTx interface{ ... } type ConnPrepareContext interface{ ... } type Connector interface{ ... } type Driver interface{ ... } type DriverContext interface{ ... } type Execer interface{ ... } type ExecerContext interface{ ... } type NamedValueChecker interface{ ... } type Pinger interface{ ... } type Queryer interface{ ... } type QueryerContext interface{ ... } type Result interface{ ... } type Rows interface{ ... } type RowsColumnTypeDatabaseTypeName interface{ ... } type RowsColumnTypeLength interface{ ... } type RowsColumnTypeNullable interface{ ... } type RowsColumnTypePrecisionScale interface{ ... } type RowsColumnTypeScanType interface{ ... } type RowsNextResultSet interface{ ... } type SessionResetter interface{ ... } type Stmt interface{ ... } type StmtExecContext interface{ ... } type StmtQueryContext interface{ ... } type Tx interface{ ... } type Validator interface{ ... } type ValueConverter interface{ ... } type Valuer interface{ ... }

Slide 16

Slide 16 text

© 2024 ANDPAD All Rights Reserved. >> go doc database/sql/driver | grep type | grep interface database/sql/driver に定義されている interface 16 type ColumnConverter interface{ ... } type Conn interface{ ... } type ConnBeginTx interface{ ... } type ConnPrepareContext interface{ ... } type Connector interface{ ... } type Driver interface{ ... } type DriverContext interface{ ... } type Execer interface{ ... } type ExecerContext interface{ ... } type NamedValueChecker interface{ ... } type Pinger interface{ ... } type Queryer interface{ ... } type QueryerContext interface{ ... } type Result interface{ ... } type Rows interface{ ... } type RowsColumnTypeDatabaseTypeName interface{ ... } type RowsColumnTypeLength interface{ ... } type RowsColumnTypeNullable interface{ ... } type RowsColumnTypePrecisionScale interface{ ... } type RowsColumnTypeScanType interface{ ... } type RowsNextResultSet interface{ ... } type SessionResetter interface{ ... } type Stmt interface{ ... } type StmtExecContext interface{ ... } type StmtQueryContext interface{ ... } type Tx interface{ ... } type Validator interface{ ... } type ValueConverter interface{ ... } type Valuer interface{ ... } 非推奨

Slide 17

Slide 17 text

© 2024 ANDPAD All Rights Reserved. いつから定義が存在していたか 17 interface名 追加された バージョン Conn 1 ConnBeginTx 1.8 ConnPrepareContext 1.8 Connector 1.10 Driver 1 DriverContext 1.10 ExecerContext 1.8 NamedValueChecker 1.9 Pinger 1.8 QueryerContext 1.8 Result 1 Rows 1 interface名 追加された バージョン RowsColumnTypeDatabaseTypeName 1.8 RowsColumnTypeLength 1.8 RowsColumnTypeNullable 1.8 RowsColumnTypePrecisionScale 1.8 RowsColumnTypeScanType 1.8 RowsNextResultSet 1.8 SessionResetter 1.10 Stmt 1 StmtExecContext 1.8 StmtQueryContext 1.8 Tx 1 Validator 1.15 ValueConverter 1 Valuer 1

Slide 18

Slide 18 text

© 2024 ANDPAD All Rights Reserved. いつから定義が存在していたか 18 interface名 追加された バージョン Conn 1 ConnBeginTx 1.8 ConnPrepareContext 1.8 Connector 1.10 Driver 1 DriverContext 1.10 ExecerContext 1.8 NamedValueChecker 1.9 Pinger 1.8 QueryerContext 1.8 Result 1 Rows 1 interface名 追加された バージョン RowsColumnTypeDatabaseTypeName 1.8 RowsColumnTypeLength 1.8 RowsColumnTypeNullable 1.8 RowsColumnTypePrecisionScale 1.8 RowsColumnTypeScanType 1.8 RowsNextResultSet 1.8 SessionResetter 1.10 Stmt 1 StmtExecContext 1.8 StmtQueryContext 1.8 Tx 1 Validator 1.15 ValueConverter 1 Valuer 1

Slide 19

Slide 19 text

© 2024 ANDPAD All Rights Reserved. DB操作に関わるもの ● Driver : ドライバーの起点、DSN文字列から接続を1本生成するエントリポイント ● Conn : DBとの接続を表す、ステートメント準備・トランザクション開始・切断を担う ● Stmt : プリペアドステートメントの実行と結果取得 ● Result : INSERT/UPDATE/DELETE の実行結果 ● Rows : 結果セットのイテレーション ● Tx : トランザクションのコミット・ロールバック 型変換に関わるもの ● ValueConverter : 任意の値を driver.Value に変換 ● Valuer : ユーザー定義型が自身を driver.Value に変換するために実装 database/sql/driver のinterface (go1) 19

Slide 20

Slide 20 text

© 2024 ANDPAD All Rights Reserved. 各interface間の関係 20 Driver Conn Tx Stmt Result Rows Open Begin Prepare Exec Query sql.Open

Slide 21

Slide 21 text

© 2024 ANDPAD All Rights Reserved. 各interface間の関係 21 Driver Conn Tx Stmt Result Rows Open Begin Prepare Exec Query Exec Query Connector OpenConnector Connect sql.Open sql.OpenDB

Slide 22

Slide 22 text

© 2024 ANDPAD All Rights Reserved. Driver interfaceに関連するinterface 22 Drivers should implement Connector and DriverContext interfaces. If a Driver implements DriverContext, then database/sql.DB will call OpenConnector to obtain a Connector and then invoke that Connector's Connect method to obtain each needed connection, instead of invoking the Driver's Open method for each connection. A Connector can be passed to database/sql.OpenDB, to allow drivers to implement their own database/sql.DB constructors, or returned by DriverContext's OpenConnector method, to allow drivers access to context and to avoid repeated parsing of driver configuration.

Slide 23

Slide 23 text

© 2024 ANDPAD All Rights Reserved. Driver interfaceに関連するinterface 23 日本語約 ドライバーは、Connector interface とDriverContext interfaceを実装す る必要があります。 もしドライバーがDriverContextを実装している場合、database/sql.DBは OpenConnectorを呼び出してConnectorを取得し、そのConnectorの Connectメソッドを使って必要な接続をそれぞれ確立します。この際、ドラ イバーのOpenメソッドが各接続で呼び出されることはありません。 Connectorは、database/sql.OpenDB に渡すことで、ドライバが独自の database/sql.DB コンストラクタを実装できるようになります。また、 DriverContext の OpenConnector メソッドから返されることで、ドライ バがコンテキストにアクセスできるようになり、ドライバ設定の繰り返し解 析を回避できます。

Slide 24

Slide 24 text

© 2024 ANDPAD All Rights Reserved. Driver interfaceに関連するinterface まとめ 24 ● Driver ○ sql.Openで使用される ○ DSN文字列から接続を1本生成するエントリポイント ● DriverContext ○ sql.Openで使用される ○ sql.Openの中でOpenConnectorメソッドを呼び出し、Driver.Connectorを取得し、 そのConnectorのConnectメソッドを使って必要な接続を確立する ○ ドライバが接続時にコンテキストにアクセスできるようになり、ドライバ設定の解析が 一度だけで済むようにできる ● Connector ○ sql.OpenDBで使用される ○ Driverの実装次第で接続方法としてDSN文字列以外もサポートできる ○ ドライバが接続時にコンテキストにアクセスできるようになり、ドライバ設定の解析が 一度だけで済むようにできる

Slide 25

Slide 25 text

© 2024 ANDPAD All Rights Reserved. Conn interfaceに関連するinterface 25 All Conn implementations should implement the following interfaces: Pinger, SessionResetter, and Validator. If named parameters or context are supported, the driver's Conn should implement: ExecerContext, QueryerContext, ConnPrepareContext, and ConnBeginTx. To support custom data types, implement NamedValueChecker.

Slide 26

Slide 26 text

© 2024 ANDPAD All Rights Reserved. Conn interfaceに関連するinterface 26 日本語約 すべてのConn実装は、Pinger、SessionResetter、Validatorの各 interfaceを実装する必要があります。 名前付きパラメータまたはコンテキストをサポートしている場合、ドライバ のConnはExecerContext、QueryerContext、ConnPrepareContext、 ConnBeginTxを実装する必要があります。 カスタムデータ型をサポートするには、NamedValueCheckerを実装してく ださい。

Slide 27

Slide 27 text

© 2024 ANDPAD All Rights Reserved. Conn interfaceに関連するinterface まとめ 27 ● Conn: DBとの接続を表す、ステートメント準備・トランザクション開始・ 切断を担う ● 実装が必須のもの ○ Pinger: sql.DB.Ping・sql.DB.PingContext をサポート ○ SessionResetter: 接続に関連付けられたセッションのリセットをサポート ○ Validator: 接続が有効か・破棄する必要があるかを確認し、結果をドライバーが通知可能 に ● オプショナル ○ ExecerContext: コンテキストサポートのExec ○ QueryerContext: コンテキストサポートのQuery ○ ConnPrepareContext: コンテキストサポートのPrepare ○ ConnBeginTx: コンテキストサポートのTx ○ NamedValueChecker: カスタムデータ型をサポート

Slide 28

Slide 28 text

© 2024 ANDPAD All Rights Reserved. Stmt interfaceに関連するinterface 28 To support custom data types, implement NamedValueChecker. StmtExecContext enhances the Stmt interface by providing Exec with context. StmtQueryContext enhances the Stmt interface by providing Query with context.

Slide 29

Slide 29 text

© 2024 ANDPAD All Rights Reserved. Stmt interfaceに関連するinterface 29 日本語訳 カスタムデータ型をサポートするには、NamedValueCheckerを実装してく ださい。 StmtExecContext は、Exec メソッドにコンテキストを提供することで、 Stmt interfaceを拡張します。 StmtQueryContext は、Query メソッドにコンテキストを提供すること で、Stmt interfaceを拡張します。

Slide 30

Slide 30 text

© 2024 ANDPAD All Rights Reserved. Stmt interfaceに関連するinterface まとめ 30 ● Stmt : プリペアドステートメントの実行と結果取得 ● オプショナル ○ StmtExecContext: コンテキストサポートのExec ○ StmtQueryContext: コンテキストサポートのQuery ○ NamedValueChecker: カスタムデータ型をサポート

Slide 31

Slide 31 text

© 2024 ANDPAD All Rights Reserved. Rows interfaceに関連するinterface 31 If multiple result sets are supported, Rows should implement RowsNextResultSet. If the driver knows how to describe the types present in the returned result it should implement the following interfaces: RowsColumnTypeScanType, RowsColumnTypeDatabaseTypeName, RowsColumnTypeLength, RowsColumnTypeNullable, and RowsColumnTypePrecisionScale. A given row value may also return a Rows type, which may represent a database cursor value.

Slide 32

Slide 32 text

© 2024 ANDPAD All Rights Reserved. Rows interfaceに関連するinterface 32 日本語訳 複数の結果セットがサポートされている場合、RowsはRowsNextResultSet を実装する必要があります。返された結果に含まれる型をドライバーが認識 している場合は、RowsColumnTypeScanType、 RowsColumnTypeDatabaseTypeName、RowsColumnTypeLength、 RowsColumnTypeNullable、およびRowsColumnTypePrecisionScaleの interfaceを実装する必要があります。また、特定の行値はRows型を返すこ とがあり、これはデータベースのカーソル値を表す場合があります。

Slide 33

Slide 33 text

© 2024 ANDPAD All Rights Reserved. Rows interfaceに関連するinterface まとめ 33 ● Rows : 結果セットのイテレーション ● オプショナル ○ RowsNextResultSet: 複数結果セットサポート ○ 結果に含まれる型に関するもの ■ RowsColumnTypeScanType: 型をスキャンするために使用する適切なGoの型を返す ■ RowsColumnTypeDatabaseTypeName: DB固有の型名(例: "BIGINT")を返す ■ RowsColumnTypeLength: 可変長カラムの最大長を返す ■ RowsColumnTypeNullable: カラムがNULL許容かを返す ■ RowsColumnTypePrecisionScale: DECIMAL等の精度・スケールを返す

Slide 34

Slide 34 text

© 2024 ANDPAD All Rights Reserved. 拡張されていないinterface まとめ 34 ● Result : INSERT/UPDATE/DELETE の実行結果 ● Tx : トランザクションのコミット・ロールバック ● ValueConverter : 任意の値を driver.Value に変換 ● Valuer : ユーザー定義型が自身を driver.Value に変換するために実装

Slide 35

Slide 35 text

© 2024 ANDPAD All Rights Reserved. 35 database/sqlはinterfaceの拡張をどう扱っているか 1 func (db *DB) queryDC(...) (*Rows, error) { 2 // Conn がQueryerContext・Queryerを実装しているか確認 3 queryerCtx, ok := dc.ci.(driver.QueryerContext) 4 var queryer driver.Queryer 5 if !ok { 6 queryer, ok = dc.ci.(driver.Queryer) 7 } 8 if ok { 9 // QueryerContext or Queryer interfaceが実装されている場合 10 // Prepare を省略して直接クエリ実行 11 rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs) 12 } 13 // フォールバック: Prepare → Stmt.Query → Stmt.Close 14 si, err = ctxDriverPrepare(ctx, dc.ci, query) 15 }

Slide 36

Slide 36 text

© 2024 ANDPAD All Rights Reserved. 36 database/sqlはinterfaceの拡張をどう扱っているか 1 func (db *DB) queryDC(...) (*Rows, error) { 2 // Conn がQueryerContext・Queryerを実装しているか確認 3 queryerCtx, ok := dc.ci.(driver.QueryerContext) 4 var queryer driver.Queryer 5 if !ok { 6 queryer, ok = dc.ci.(driver.Queryer) 7 } 8 if ok { 9 // QueryerContext or Queryer interfaceが実装されている場合 10 // Prepare を省略して直接クエリ実行 11 rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs) 12 } 13 // フォールバック: Prepare → Stmt.Query → Stmt.Close 14 si, err = ctxDriverPrepare(ctx, dc.ci, query) 15 }

Slide 37

Slide 37 text

© 2024 ANDPAD All Rights Reserved. ここまでのまとめ 37 ● database/sql パッケージとdatabase/sql/driver パッケージの関係 ○ database/sqlパッケージはdatabase/sql/driver パッケージに定義されてい interfaceを介してドライバーとやりとり ● database/sql/driverの各interfaceの関係

Slide 38

Slide 38 text

© 2024 ANDPAD All Rights Reserved. カスタムドライバーを作っていく 38 ● クエリの実行時にログをだす ○ DB操作時にログを出したいので処理を動かしたい箇所Driver・Conn・Stmt・Tx database/sql 既存ドライバー DB カスタムドライバー

Slide 39

Slide 39 text

© 2024 ANDPAD All Rights Reserved. この範囲は既存 ドライバーを そのまま使用する 今回ラップしたい範囲 基本方針 39 Driver Conn Tx Stmt Result Rows Open Begin Prepare Exec Query Exec Query Connector OpenConnector Connect sql.Open sql.OpenDB

Slide 40

Slide 40 text

© 2024 ANDPAD All Rights Reserved. ドライバーをラップしていく 40 1. type CustomDriver struct{} 2. 3. func (d *CustomDriver) Open(name string) (driver.Conn, error) { 4. mysqlDriver := &mysql.MySQLDriver{} 5. conn, err := mysqlDriver.Open(name) 6. 7. return &customConn{conn}, nil 8. } 9. 10. type customConn struct { 11. conn driver.Conn 12. } 13. 14. func (c *customConn) Prepare(query string) (driver.Stmt, error) { 15. stmt, err := c.conn.Prepare(query) 16. 17. return &customStmt{stmt: stmt}, nil 18. } 19. 20. func (c *customConn) Close() error { 21. return c.conn.Close() 22. }

Slide 41

Slide 41 text

© 2024 ANDPAD All Rights Reserved. ドライバーをラップしていく 41 1. type CustomDriver struct{} 2. 3. func (d *CustomDriver) Open(name string) (driver.Conn, error) { 4. mysqlDriver := &mysql.MySQLDriver{} 5. conn, err := mysqlDriver.Open(name) 6. 7. return &customConn{conn}, nil 8. } 9. 10. type customConn struct { 11. conn driver.Conn 12. } 13. 14. func (c *customConn) Prepare(query string) (driver.Stmt, error) { 15. stmt, err := c.conn.Prepare(query) 16. 17. return &customStmt{stmt: stmt}, nil 18. } 19. 20. func (c *customConn) Close() error { 21. return c.conn.Close() 22. }

Slide 42

Slide 42 text

© 2024 ANDPAD All Rights Reserved. ドライバーをラップしていく 42 1. type customStmt struct { 2. stmt driver.Stmt 3. } 4. 5. func (s *customStmt) Close() error { 6. return s.stmt.Close() 7. } 8. 9. func (s *customStmt) QueryContext( 10. ctx context.Context, 11. args []driver.NamedValue) (driver.Rows, error) { 12. casted, ok := s.stmt.(driver.StmtQueryContext); 13. if !ok { 14. // fallback 15. dargs := make([]driver.Value, len(args)) 16. for i, nv := range args { dargs[i] = nv.Value } 17. 18. return s.Query(dargs) 19. } 20. 21. return casted.QueryContext(ctx, args) 22. }

Slide 43

Slide 43 text

© 2024 ANDPAD All Rights Reserved. ドライバーをラップしていく 43 1. type customStmt struct { 2. stmt driver.Stmt 3. } 4. 5. func (s *customStmt) Close() error { 6. return s.stmt.Close() 7. } 8. 9. func (s *customStmt) QueryContext( 10. ctx context.Context, 11. args []driver.NamedValue) (driver.Rows, error) { 12. casted, ok := s.stmt.(driver.StmtQueryContext); 13. if !ok { 14. // fallback 15. dargs := make([]driver.Value, len(args)) 16. for i, nv := range args { dargs[i] = nv.Value } 17. 18. return s.Query(dargs) 19. } 20. 21. return casted.QueryContext(ctx, args) 22. }

Slide 44

Slide 44 text

© 2024 ANDPAD All Rights Reserved. loggerの埋め込み 44 1. func main() { 2. sql.Register("custom-driver", NewCustomDriver(logger)) 3. db, err := sql.Open("custom-driver", os.Getenv("DataSource")) 4. } 5. 6. func NewCustomDriver(logger *slog.Logger) *CustomDriver { 7. return &CustomDriver{logger: logger} 8. } 9. 10. func (d *CustomDriver) Open(name string) (driver.Conn, error) { 11. mysqlDriver := &mysql.MySQLDriver{} 12. conn, err := mysqlDriver.Open(name) 13. 14. return &customConn{ 15. conn: conn, 16. logger: d.logger, 17. }, nil 18. }

Slide 45

Slide 45 text

© 2024 ANDPAD All Rights Reserved. loggerの埋め込み 45 1. func main() { 2. sql.Register("custom-driver", NewCustomDriver(logger)) 3. db, err := sql.Open("custom-driver", os.Getenv("DataSource")) 4. } 5. 6. func NewCustomDriver(logger *slog.Logger) *CustomDriver { 7. return &CustomDriver{logger: logger} 8. } 9. 10. func (d *CustomDriver) Open(name string) (driver.Conn, error) { 11. mysqlDriver := &mysql.MySQLDriver{} 12. conn, err := mysqlDriver.Open(name) 13. 14. return &customConn{ 15. conn: conn, 16. logger: d.logger, 17. }, nil 18. }

Slide 46

Slide 46 text

© 2024 ANDPAD All Rights Reserved. loggerの埋め込み 46 1. func (s *customStmt) QueryContext( 2. ctx context.Context, 3. args []driver.NamedValue) (driver.Rows, error) { 4. casted, ok := s.stmt.(driver.StmtQueryContext) 5. if !ok { 6. // fallback 7. dargs := make([]driver.Value, len(args)) 8. for i, nv := range args { dargs[i] = nv.Value } 9. return s.Query(dargs) 10. } 11. 12. rows, err := casted.QueryContext(ctx, args) 13. if err != nil { 14. s.logger.Error("query failed", slog.String("query", s.query), 15. slog.Any("args", args), slog.Any("error", err)) 16. } else { 17. s.logger.Info("queried success", slog.String("query", s.query), 18. slog.Any("args", args)) 19. } 20. 21. return rows, err 22. }

Slide 47

Slide 47 text

© 2024 ANDPAD All Rights Reserved. 複数ドライバー対応 47 1. func main(){ 2. sql.Register("custom-logging", NewLoggingDriver(&pq.Driver{}, logger)) 3. db, err := sql.Open("custom-logging", os.Env("DataSource")) 4. } 5. 6. type CustomDriver struct { 7. driver driver.Driver 8. logger *slog.Logger 9. } 10. 11. func NewLoggingDriver(drv driver.Driver, logger *slog.Logger) *CustomDriver { 12. return &CustomDriver{driver: drv, logger: logger} 13. } 14. 15. func (d *CustomDriver) Open(name string) (driver.Conn, error) { 16. conn, err := d.driver.Open(name) 17. 18. return &customConn{ 19. conn: conn, 20. logger: d.logger, 21. }, nil 22. }

Slide 48

Slide 48 text

© 2024 ANDPAD All Rights Reserved. 複数ドライバー対応 48 1. func main(){ 2. sql.Register("custom-logging", NewLoggingDriver(&pq.Driver{}, logger)) 3. db, err := sql.Open("custom-logging", os.Env("DataSource")) 4. } 5. 6. type CustomDriver struct { 7. driver driver.Driver 8. logger *slog.Logger 9. } 10. 11. func NewLoggingDriver(drv driver.Driver, logger *slog.Logger) *CustomDriver { 12. return &CustomDriver{driver: drv, logger: logger} 13. } 14. 15. func (d *CustomDriver) Open(name string) (driver.Conn, error) { 16. conn, err := d.driver.Open(name) 17. 18. return &customConn{ 19. conn: conn, 20. logger: d.logger, 21. }, nil 22. }

Slide 49

Slide 49 text

© 2024 ANDPAD All Rights Reserved. 複数ドライバー対応 49 1. func main(){ 2. connector, err := mysql.MySQLDriver{}.OpenConnector(os.Env("DataSource")) 3. db := sql.OpenDB(NewCustomConnector(connector, logger)) 4. } 5. 6. func NewCustomConnector( 7. connector driver.Connector, 8. logger *slog.Logger) *CustomConnector { 9. 10. return &CustomConnector{ 11. connector: connector, 12. driver: &CustomDriver{driver: connector.Driver(), logger: logger}, 13. logger: logger, 14. } 15. } 16. 17. func (cc *CustomConnector) Connect(ctx context.Context) (driver.Conn, error) { 18. conn, err := cc.connector.Connect(ctx) 19. 20. return &customConn{conn: conn, logger: cc.logger}, nil 21. }

Slide 50

Slide 50 text

© 2024 ANDPAD All Rights Reserved. 複数ドライバー対応 50 1. func main(){ 2. connector, err := mysql.MySQLDriver{}.OpenConnector(os.Env("DataSource")) 3. db := sql.OpenDB(NewCustomConnector(connector, logger)) 4. } 5. 6. func NewCustomConnector( 7. connector driver.Connector, 8. logger *slog.Logger) *CustomConnector { 9. 10. return &CustomConnector{ 11. connector: connector, 12. driver: &CustomDriver{driver: connector.Driver(), logger: logger}, 13. logger: logger, 14. } 15. } 16. 17. func (cc *CustomConnector) Connect(ctx context.Context) (driver.Conn, error) { 18. conn, err := cc.connector.Connect(ctx) 19. 20. return &customConn{conn: conn, logger: cc.logger}, nil 21. }

Slide 51

Slide 51 text

© 2024 ANDPAD All Rights Reserved. driver.ErrSkip 51 一部のオプショナル interface は driver.ErrSkip を返すことで database/sql パッケージでフォールバック処理が走る 対象はdatabase/sql/driver パッケージのgo docに書かれており下記の5つ ● Connのオプション ○ Execer.Exec(非推奨) ○ ExecerContext.ExecContext ○ Queryer.Query(非推奨) ○ QueryerContext.QueryContext ● ConnとStmtのオプション ○ NamedValueChecker.CheckNamedValue

Slide 52

Slide 52 text

© 2024 ANDPAD All Rights Reserved. driver.ErrSkip 52 1. func (c *customConn) QueryContext(...) (driver.Rows, error) { 2. queryerCtx, ok := c.conn.(driver.QueryerContext) 3. if !ok { 4. return nil, driver.ErrSkip 5. } 6. 7. rows, err := queryerCtx.QueryContext(ctx, query, args) 8. // そのまま処理してもErrSkipを返せるが、無駄にログがでるので確認し早期リターン 9. if err == driver.ErrSkip { 10. return nil, driver.ErrSkip 11. } 12. 13. // ログ出力 14. 15. return rows, err 16. }

Slide 53

Slide 53 text

© 2024 ANDPAD All Rights Reserved. driver.ErrSkipを返せない箇所で実装されていない場合 53 ● Pinger, SessionResetter, Validator などは実装が必須なため ErrSkip による委譲の仕組みがdatabase/sql パッケージにない (インターフェイスを実装していない場合はdatabase/sql パッケージで最低限の対応が動く) ● カスタムドライバーで interface を宣言したら自分で責任を持つ 必要がある ○ 基本的にはラップするドライバー側で実装されているので問題になることはないが

Slide 54

Slide 54 text

© 2024 ANDPAD All Rights Reserved. driver.ErrSkipを返せない箇所で実装されていない場合 54 1. func (c *customConn) Ping(ctx context.Context) error { 2. if pinger, ok := c.conn.(driver.Pinger); ok { 3. return pinger.Ping(ctx) 4. } 5. 6. // フォールバック: 軽量クエリで疎通確認 7. rows, err := c.QueryContext(ctx, "SELECT 1", nil) 8. if err == nil { err = rows.Close() } 9. return err 10. } 11. 12. func (c *customConn) ResetSession(ctx context.Context) error { 13. if resetter, ok := c.conn.(driver.SessionResetter); ok { 14. return resetter.ResetSession(ctx) 15. } 16. 17. // フォールバック: リセット不要とみなす 18. return nil 19. }

Slide 55

Slide 55 text

© 2024 ANDPAD All Rights Reserved. 実行してみる 55 1. func main() { 2. ctx := context.Background() 3. 4. logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 5. Level: slog.LevelInfo, 6. })) 7. 8. connector, err := mysql.MySQLDriver{}.OpenConnector(os.GetEnv("DataSource")) 9. db := sql.OpenDB(NewCustomConnector(connector, logger)) 10. 11. queries := sqlc.New(db) 12. res, err = queries.GetUserByName(ctx, new("foo")) 13. logger.Info(fmt.Sprintf("id: %d, name: %q", res.ID, res.Name)) 14. }

Slide 56

Slide 56 text

© 2024 ANDPAD All Rights Reserved. カスタムドライバーのテストの観点 56 ● database/sql/driverのinterfaceを実装できているか ○ コンパイル時にinterfaceに適合しているかアサーションする 1. var ( 2. _ driver.Driver = (*CustomDriver)(nil) 3. _ driver.DriverContext = (*CustomDriver)(nil) 4. )

Slide 57

Slide 57 text

© 2024 ANDPAD All Rights Reserved. カスタムドライバーのテストの観点 57 ● カスタム元のドライバーに機能が移譲できているか ○ bradfitz/go-sql-test[2]を使用する ■ Goのdatabase/sql パッケージ用の様々なデータベースドライバーを テストするためのプロジェクト ■ 最終更新は12年前 ● 更新されていないため新しい機能のテストは存在しない ■ 標準的なSQL操作を網羅的にテスト可能 ■ Go Moduleに対応していないため実行するのひと手間かかる ○ 独自にテストを作成する ■ オリジナルのドライバーとカスタムドライバーそれぞれ対して同じテストを実行 して結果が一致すればよい ● 追加した機能が意図した挙動をしているか ○ 今回の例でいうと意図したログがでているかテストする [2]https://github.com/bradfitz/go-sql-test

Slide 58

Slide 58 text

© 2024 ANDPAD All Rights Reserved. パフォーマンス 58 ● go-sql-driver/mysql・lib/pqと作成したカスタムドライバーで性能を比較 ● 1ドライバあたりStmtを経由するパターンとしないパターンの2パターン

Slide 59

Slide 59 text

© 2024 ANDPAD All Rights Reserved. まとめ 59 ● database/sql/driver パッケージ ○ データベース ドライバが実装すべきインターフェイスを定義 ○ database/sql パッケージはこのインターフェイスを介してドライバとやり取りする ● DBとのやり取りは既存のドライバーに任せつつ、追加の機能を独自に 実装するドライバー(カスタムドライバー)の作り方を紹介 ○ サンプルとしてDB操作時にログを出力するドライバーの例を示した ○ カスタムドライバーと既存ドライバーのパフォーマンスを比較 ■ 処理時間はほぼ変わらないがメモリの使用量が1.5倍ほど (追加する機能によって大きく変化する)

Slide 60

Slide 60 text

© 2024 ANDPAD All Rights Reserved. 60 appendix

Slide 61

Slide 61 text

© 2024 ANDPAD All Rights Reserved. 決まった値を返すだけのドライバーを作ってみる 61 Driver Conn Tx Stmt Result Rows Open Begin Prepare Exec Query

Slide 62

Slide 62 text

© 2024 ANDPAD All Rights Reserved. 実用的ではないが最低限のドライバーの実装 62 1. func init() { sql.Register("custom-driver", &Driver{}) } 2. 3. type Driver struct{} 4. 5. func (d *Driver) Open(name string) (driver.Conn, error) { 6. return &Conn{}, nil 7. } 8. 9. type Conn struct{} 10. 11. func (c *Conn) Prepare(query string) (driver.Stmt, error) { 12. return &Stmt{}, nil 13. } 14. 15. func (c *Conn) Close() error { return nil } 16. 17. func (c *Conn) Begin() (driver.Tx, error) { return nil, nil }

Slide 63

Slide 63 text

© 2024 ANDPAD All Rights Reserved. 実用的ではないが最低限のドライバーの実装 63 1. type Stmt struct {} 2. 3. func (s *Stmt) Close() error { return nil } 4. 5. func (s *Stmt) NumInput() int { return -1} 6. 7. func (s *Stmt) Exec(args []driver.Value) (driver.Result, error) { 8. return nil, nil 9. } 10. 11. func (s *Stmt) Query(args []driver.Value) (driver.Rows, error) { 12. return &Rows{ 13. index: 0, 14. data: [][]driver.Value{ {1, "Alice"}, {2, "Bob"} }, 15. }, nil 16. }

Slide 64

Slide 64 text

© 2024 ANDPAD All Rights Reserved. 実用的ではないが最低限のドライバーの実装 64 1. type Rows struct { 2. index int 3. data [][]driver.Value 4. } 5. 6. func (r *Rows) Columns() []string { return []string{ "id", "name" }} 7. 8. func (r *Rows) Close() error { return nil } 9. 10. func (r *Rows) Next(dest []driver.Value) error { 11. if r.index >= len(r.data) { return io.EOF } 12. copy(dest, r.data[r.index]) 13. r.index++ 14. return nil 15. }

Slide 65

Slide 65 text

© 2024 ANDPAD All Rights Reserved. 実際に最低限のドライバーを使ってみる 65 1. func main(){ 2. db, err := sql.Open("custom-driver", "") 3. stmt, err := db.Prepare("") 4. rows, err := stmt.Query("") 5. for rows.Next() { 6. var id int 7. var name string 8. rows.Scan(&id, &name) 9. fmt.Println(id, name) 10. } 11. }

Slide 66

Slide 66 text

© 2024 ANDPAD All Rights Reserved. ラップ時の注意 66 ラップ元のドライバーが実装しているinterfaceの機能すべてに対して ラップをしないと機能が劣化していまう 例えばラップ元のドライバーのConnがQueryerContextを実装している場合 ラップ側でQueryerContextを実装しないと database/sql パッケージから はQueryerContextは実装されていないことになる ● 複数のドライバーを対象とする場合 ○ 使用する可能性のあるインターフェイスは基本的にすべて実装すべき ● 特定のドライバーを対象とする場合 ○ ラップ元のドライバーが実装しているinterfaceだけを実装すればよい

Slide 67

Slide 67 text

© 2024 ANDPAD All Rights Reserved. 既存ドライバーがどのinterfaceを実装しているか確認 67 ● driver側の具象型が公開されているパターン ○ コンパイル時にinterfaceに適合しているかアサーションする ● driver側の具象型が公開されていないパターン ○ コードを確認する ■ 元のコードの中でコンパイル時にinterfaceに適合しているかアサーションして いる場合はそこを確認するだけでよい ○ DB接続して返ってきた型に対してリフレクションを使って確認する ○ linkname directiveで非公開の具象型を参照できないか? ■ linkname directiveの対象は変数と関数であり構造体は対象にできない[3] [3]https://pkg.go.dev/cmd/compile#hdr-Linkname_Directive

Slide 68

Slide 68 text

© 2024 ANDPAD All Rights Reserved. 既存ドライバーがどのinterfaceを実装しているか確認 68 ● driver側の具象型が公開されていないパターン ○ DB接続して返ってきた型に対してリフレクションを使って確認する 1. conn, err := mysql.MySQLDriver{}.Open(os.Getenv("DataSource")) 2. t := reflect.TypeOf(conn) 3. it := reflect.TypeOf((*driver.Conn)(nil)).Elem() 4. if t.Implements(it) { 5. fmt.Print("✓ driver.Conn") 6. } else { 7. fmt.Println("✗ driver.Conn") 8. }

Slide 69

Slide 69 text

© 2024 ANDPAD All Rights Reserved. go-sql-driver/mysqlに対して確認した結果 69 ● mysql.MySQLDriver ✓ driver.Driver ✓ driver.DriverContext ● *mysql.connector ✓ driver.Connector ● *mysql.mysqlConn ✓ driver.Conn ✓ driver.Pinger ✓ driver.SessionResetter ✓ driver.Validator ✓ driver.ExecerContext ✓ driver.QueryerContext ✓ driver.ConnPrepareContext ✓ driver.ConnBeginTx ✓ driver.NamedValueChecker ● *mysql.mysqlStmt ✓ driver.Stmt ✓ driver.StmtExecContext ✓ driver.StmtQueryContext ✓ driver.NamedValueChecker ● *mysql.binaryRows,*mysql.textRows ✓ driver.Rows ✓ driver.RowsNextResultSet ✓ driver.RowsColumnTypeDatabaseTypeName ✘ driver.RowsColumnTypeLength ✓ driver.RowsColumnTypeNullable ✓ driver.RowsColumnTypePrecisionScale ✓ driver.RowsColumnTypeScanType

Slide 70

Slide 70 text

© 2024 ANDPAD All Rights Reserved. lib/pqに対して確認した結果 70 ● pq.Driver ✓ driver.Driver ✘ driver.DriverContext ● *pq.Connector ✓ driver.Connector ● *pq.conn ✓ driver.Conn ✓ driver.Pinger ✓ driver.SessionResetter ✓ driver.Validator ✓ driver.ExecerContext ✓ driver.QueryerContext ✓ driver.ConnPrepareContext ✓ driver.ConnBeginTx ✓ driver.NamedValueChecker ● *pq.stmt ✓ driver.Stmt ✓ driver.StmtExecContext ✓ driver.StmtQueryContext ✘ driver.NamedValueChecker ● *pq.rows ✓ driver.Rows ✓ driver.RowsNextResultSet ✓ driver.RowsColumnTypeDatabaseTypeName ✓ driver.RowsColumnTypeLength ✘ driver.RowsColumnTypeNullable ✓ driver.RowsColumnTypePrecisionScale ✓ driver.RowsColumnTypeScanType