LoginSignup
1
1

More than 1 year has passed since last update.

Goのdatabase/sqlパッケージとGORMのコードリーディング

Posted at

今日やること

database/sqlパッケージを素朴に使う。
その上でGORMのコードを読む。

Version

  • Mysql 8.X
  • Go 1.18

database/sqlパッケージを使う。

データベースとテーブル作成


CREATE DATABASE example_go_sql;
CREATE TABLE users (
  id   SERIAL,
  name VARCHAR(20)
);

main.go

  • 複数行取得 func queryRows
  • 一行取得 func queryRow
  • インサート func create
  • トランザクションの使用(Update) func txUpdate

を行います。

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db := open()
	defer db.Close()

	configDB(db)

	ctx := context.TODO()
	conn := connection(db, ctx)
	defer conn.Close()

	queryRows(conn)
	queryRow(conn)
	create(conn, "hey")
	txUpdate(conn)
}

func open() *sql.DB {
	db, err := sql.Open("mysql", "root:sample@tcp(127.0.0.1:3306)/example_go_sql")
	if err != nil {
		log.Fatalln(err)
	}
	return db
}

func configDB(db *sql.DB) {
	db.SetConnMaxLifetime(0)
	db.SetMaxIdleConns(50)
	db.SetMaxOpenConns(50)
}

func connection(db *sql.DB, ctx context.Context) *sql.Conn {
	conn, err := db.Conn(ctx)
	if err != nil {
		log.Fatalln(err)
	}
	return conn
}

func queryRows(conn *sql.Conn) {
	ctx := context.TODO()
	rows, err := conn.QueryContext(ctx, "SELECT * FROM users WHERE id > ?", 0)
	if err != nil {
		log.Fatalln(err)
	}
	defer rows.Close()

	for rows.Next() {
		var id, name string
		err := rows.Scan(&id, &name)
		if err != nil {
			log.Fatalln(err)
		}

		fmt.Println(id, name)
	}
	if err := rows.Err(); err != nil {
		log.Fatalln(err)
	}
}

func queryRow(conn *sql.Conn) {
	ctx := context.TODO()
	row := conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id > ?", 0)
	var id, name string
	err := row.Scan(&id, &name)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(id, name)
}

func create(conn *sql.Conn, name string) {
	ctx := context.TODO()
	res, err := conn.ExecContext(ctx, "INSERT INTO users (name) VALUES(?)", name)
	if err != nil {
		log.Fatalln(err)
	}
	updated, err := res.RowsAffected()
	if err != nil {
		log.Fatalln(err)
	}
	inserted, err := res.LastInsertId()
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(inserted, updated)

}

func txUpdate(conn *sql.Conn) {
	ctx := context.TODO()
	tx, err := conn.BeginTx(ctx, nil)
	if err != nil {
		log.Fatalln(err)
	}

	_, execErr := tx.ExecContext(ctx, "UPDATE users SET name = ? WHERE id = ?", "updated", 1)
	if execErr != nil {
		if rollbackErr := tx.Rollback(); rollbackErr != nil {
			log.Fatalf("update failed: %v, unable to rollback: %v\n", execErr, rollbackErr)
		}
		log.Fatalf("update failed: %v", execErr)
	}
	if err := tx.Commit(); err != nil {
		log.Fatal(err)
	}
}

database/sqlを使用した感想

	row := conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", 1)
	var id, name string
	row.Scan(&id, &name)

変数へのアサインがめんどくさいです。
上のコードスニペットの様に、
(愚直にやると)カラムの分だけ人間が変数を定義する羽目になります。

一方で普段使用しているGORMは次のコードスニペットの様に実行できて、構造体を指定するだけです。

	var user User
	db.WithContext(ctx).First(&user)

GORM便利!!

GORMのコードを読む

GORMが便利ということはわかりました。
あとはGORMがこの便利なインタフェースを、どう実現しているか知れれば完璧ですね。

今回はタグv1.23.8のコードを読みます

Scanで検索

database/sqlではScanを使用して変数にアサインをしました。
このScanメソッドは定義を見ると
任意の数のパラメーターを渡すことができます。

GORMでは当然 Scan(slice...) のように呼び出されていると推測し

Scan(.+...) で検索します

$ ag "Scan\(.+\.\.\."
interfaces.go
81:	Scan(dest ...interface{}) error

scan.go
67:	db.AddError(rows.Scan(values...))
125:			db.AddError(rows.Scan(values...))
145:			db.AddError(rows.Scan(values...))

結果2つのファイルで合計4行ヒットしました。
このうちinterface.goのものは明らかにinterface定義なのでscan.goを見ていきます。

scan.goでは次のメソッドがScanを呼び出していました。
func scanIntoStruct
func Scan

メソッド名的に scanIntoStruct 今回探していた関数です。

func (db *DB) scanIntoStruct(rows Rows, reflectValue reflect.Value, values []interface{}, fields []*schema.Field, joinFields [][2]*schema.Field) {
	for idx, field := range fields {
		if field != nil {
			values[idx] = field.NewValuePool.Get()
		} else if len(fields) == 1 {
			if reflectValue.CanAddr() {
				values[idx] = reflectValue.Addr().Interface()
			} else {
				values[idx] = reflectValue.Interface()
			}
		}
	}

	db.RowsAffected++
	db.AddError(rows.Scan(values...))

	joinedSchemaMap := make(map[*schema.Field]interface{}, 0)
	for idx, field := range fields {
		if field != nil {
			if len(joinFields) == 0 || joinFields[idx][0] == nil {
				db.AddError(field.Set(db.Statement.Context, reflectValue, values[idx]))
			} else {
				joinSchema := joinFields[idx][0]
				relValue := joinSchema.ReflectValueOf(db.Statement.Context, reflectValue)
				if relValue.Kind() == reflect.Ptr {
					if _, ok := joinedSchemaMap[joinSchema]; !ok {
						if value := reflect.ValueOf(values[idx]).Elem(); value.Kind() == reflect.Ptr && value.IsNil() {
							continue
						}

						relValue.Set(reflect.New(relValue.Type().Elem()))
						joinedSchemaMap[joinSchema] = nil
					}
				}
				db.AddError(joinFields[idx][1].Set(db.Statement.Context, relValue, values[idx]))
			}

			// release data to pool
			field.NewValuePool.Put(values[idx])
		}
	}
}

scanIntoStructメソッド内では values に読み込んだ行データをセットしています。

このvaluesに入った値は field.Set の引数に渡されています。

このvaluesが何かは一旦宿題としてfield.Set を見ていきます。

	Set                    func(context.Context, reflect.Value, interface{}) error

schema.Field 構造体にSetという名前で関数型のフィールドがありました。

このSetフィールドはsetupValuerAndSetterメソッドでセットされています。

func (field *Field) setupValuerAndSetter() {

fieldに応じた型の種類によってswitch文でSetフィールドをセットしています。
Setフィールドにセットされる関数は SetBool などの型に応じたメソッドを呼び
構造体に値をセットしています。

	switch field.FieldType.Kind() {
	case reflect.Bool:
		field.Set = func(ctx context.Context, value reflect.Value, v interface{}) error {
			switch data := v.(type) {
			case **bool:
				if data != nil && *data != nil {
					field.ReflectValueOf(ctx, value).SetBool(**data)
				} else {
					field.ReflectValueOf(ctx, value).SetBool(false)
				}
			case bool:
				field.ReflectValueOf(ctx, value).SetBool(data)
			case int64:
				field.ReflectValueOf(ctx, value).SetBool(data > 0)
			case string:
				b, _ := strconv.ParseBool(data)
				field.ReflectValueOf(ctx, value).SetBool(b)
			default:
				return fallbackSetter(ctx, value, v, field.Set)
			}
			return nil
		}

あとは scanIntoStruct メソッドに渡ってくる fields がどう初期化されているかですが
これも一旦宿題とします。

reflectパッケージについてちょっとだけ

reflectパッケージのValue構造体にある SetXXX メソッドについてコードで補足します。

type (
       User struct {
               Name string
       }
)

func main() {
       var u User

       v := reflect.ValueOf(&u)
       f := reflect.Indirect(v).Field(0)
       f.SetString("nishi")
       fmt.Println(u.Name) // nishi
}

このようにreflectパッケージを使えば、構造体のフィールド名を使用せずに
構造体のフィールドを変更できます。

私もreflectパッケージについてはあまり詳しくないので、いつか調べてまた記事にします。

まとめ

今回は database/sql パッケージを触りました。
普段使う便利ライブラリもGoの標準パッケージを内部的に使用していることがほとんどです。
GORMのような便利ライブラリがどう標準パッケージを利用しているかをコードを読んで理解しました。(ざっくり)
まだまだ知らないことがいっぱいあって、記事のネタに困らなくて良いですね。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1